@assistant-ui/react-google-adk 0.0.26 → 0.0.28

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.
@@ -0,0 +1,236 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, render, waitFor } from "@testing-library/react";
4
+ import { type FC } from "react";
5
+ import { describe, expect, it, vi } from "vitest";
6
+ import {
7
+ AssistantRuntimeProvider,
8
+ useAssistantTool,
9
+ } from "@assistant-ui/core/react";
10
+ import type {
11
+ AssistantRuntime,
12
+ RemoteThreadListAdapter,
13
+ ToolCallMessagePart,
14
+ } from "@assistant-ui/core";
15
+ import { useAdkRuntime } from "./useAdkRuntime";
16
+ import type { AdkEvent } from "./types";
17
+
18
+ const makeThreadListAdapter = (): RemoteThreadListAdapter => ({
19
+ list: vi.fn(async () => ({
20
+ threads: [
21
+ {
22
+ status: "regular" as const,
23
+ remoteId: "adk-1",
24
+ externalId: "adk-1",
25
+ title: "Existing ADK session",
26
+ },
27
+ ],
28
+ })),
29
+ initialize: vi.fn(async () => ({ remoteId: "adk-1", externalId: "adk-1" })),
30
+ rename: vi.fn(async () => {}),
31
+ archive: vi.fn(async () => {}),
32
+ unarchive: vi.fn(async () => {}),
33
+ delete: vi.fn(async () => {}),
34
+ generateTitle: vi.fn(async () => new ReadableStream() as never),
35
+ fetch: vi.fn(async () => ({
36
+ status: "regular" as const,
37
+ remoteId: "adk-1",
38
+ externalId: "adk-1",
39
+ title: "Existing ADK session",
40
+ })),
41
+ });
42
+
43
+ const toolCalls = (runtime: AssistantRuntime): ToolCallMessagePart[] =>
44
+ runtime.thread
45
+ .getState()
46
+ .messages.flatMap((message) => message.content as readonly unknown[])
47
+ .filter(
48
+ (part): part is ToolCallMessagePart =>
49
+ (part as ToolCallMessagePart).type === "tool-call",
50
+ );
51
+
52
+ const GATED_CALL = {
53
+ id: "adk-original-1",
54
+ name: "delete_file",
55
+ args: { path: "/tmp/a" },
56
+ };
57
+
58
+ const callEvent = (longRunningToolIds?: string[]): AdkEvent => ({
59
+ id: "ev-1",
60
+ invocationId: "inv-1",
61
+ author: "agent",
62
+ content: { role: "model", parts: [{ functionCall: GATED_CALL }] },
63
+ ...(longRunningToolIds && { longRunningToolIds }),
64
+ });
65
+
66
+ const confirmationEvent = (): AdkEvent => ({
67
+ id: "ev-2",
68
+ invocationId: "inv-1",
69
+ author: "agent",
70
+ content: {
71
+ role: "model",
72
+ parts: [
73
+ {
74
+ functionCall: {
75
+ id: "adk-confirmation-1",
76
+ name: "adk_request_confirmation",
77
+ args: {
78
+ originalFunctionCall: GATED_CALL,
79
+ toolConfirmation: { hint: "Delete /tmp/a?" },
80
+ },
81
+ },
82
+ },
83
+ ],
84
+ },
85
+ longRunningToolIds: ["adk-confirmation-1"],
86
+ });
87
+
88
+ const serverResultEvent = (): AdkEvent => ({
89
+ id: "ev-2",
90
+ invocationId: "inv-1",
91
+ author: "agent",
92
+ content: {
93
+ role: "user",
94
+ parts: [
95
+ {
96
+ functionResponse: {
97
+ id: "adk-original-1",
98
+ name: "delete_file",
99
+ response: { server: "deleted" },
100
+ },
101
+ },
102
+ ],
103
+ },
104
+ });
105
+
106
+ /**
107
+ * Streams `first`, hands control back so the window between the two events is
108
+ * observable, then streams `second` once the returned `release` is called.
109
+ */
110
+ const renderStreamingAdk = async (first: AdkEvent, second: AdkEvent) => {
111
+ let release!: () => void;
112
+ const held = new Promise<void>((r) => (release = r));
113
+ let firstDelivered!: () => void;
114
+ const firstDeliveredP = new Promise<void>((r) => (firstDelivered = r));
115
+
116
+ const stream = vi.fn(async function* (): AsyncGenerator<AdkEvent> {
117
+ yield first;
118
+ firstDelivered();
119
+ await held;
120
+ yield second;
121
+ });
122
+
123
+ const execute = vi.fn(async () => ({ deleted: true }));
124
+ const capture: { runtime: AssistantRuntime | null } = { runtime: null };
125
+
126
+ const DeleteFileTool = () => {
127
+ useAssistantTool({
128
+ toolName: "delete_file",
129
+ description: "delete a file",
130
+ parameters: {
131
+ type: "object" as const,
132
+ properties: { path: { type: "string" } },
133
+ },
134
+ execute,
135
+ });
136
+ return null;
137
+ };
138
+
139
+ const Inner: FC = () => {
140
+ const runtime = useAdkRuntime({
141
+ stream: stream as never,
142
+ sessionAdapter: makeThreadListAdapter(),
143
+ });
144
+ capture.runtime = runtime;
145
+ return (
146
+ <AssistantRuntimeProvider runtime={runtime}>
147
+ <DeleteFileTool />
148
+ </AssistantRuntimeProvider>
149
+ );
150
+ };
151
+
152
+ await act(async () => {
153
+ render(<Inner />);
154
+ });
155
+ await waitFor(() => expect(capture.runtime).not.toBeNull());
156
+ await act(async () => {
157
+ await capture.runtime!.threads.switchToThread("adk-1");
158
+ });
159
+ await act(async () => {
160
+ await new Promise((r) => setTimeout(r, 0));
161
+ });
162
+
163
+ await act(async () => {
164
+ void capture.runtime!.thread.append({
165
+ role: "user",
166
+ content: [{ type: "text", text: "delete it" }],
167
+ });
168
+ await firstDeliveredP;
169
+ });
170
+ await act(async () => {
171
+ await new Promise((r) => setTimeout(r, 0));
172
+ });
173
+
174
+ const settle = async () => {
175
+ await act(async () => {
176
+ release();
177
+ await new Promise((r) => setTimeout(r, 0));
178
+ });
179
+ await act(async () => {
180
+ await new Promise((r) => setTimeout(r, 0));
181
+ });
182
+ };
183
+
184
+ return { capture, execute, settle };
185
+ };
186
+
187
+ describe("useAdkRuntime tool ownership", () => {
188
+ it("does not run a frontend execute on a call ADK is about to gate", async () => {
189
+ const { capture, execute, settle } = await renderStreamingAdk(
190
+ callEvent(),
191
+ confirmationEvent(),
192
+ );
193
+
194
+ const inWindow = toolCalls(capture.runtime!)[0]!;
195
+ expect(inWindow.argsText).toBe('{"path":"/tmp/a"}');
196
+ expect(inWindow.approval).toBeUndefined();
197
+ expect(inWindow.result).toBeUndefined();
198
+ expect(execute).not.toHaveBeenCalled();
199
+
200
+ await settle();
201
+
202
+ const gated = toolCalls(capture.runtime!).find(
203
+ (part) => part.toolCallId === "adk-original-1",
204
+ )!;
205
+ expect(gated.approval).toEqual({ id: "adk-confirmation-1" });
206
+ expect(gated.result).toBeUndefined();
207
+ expect(execute).not.toHaveBeenCalled();
208
+ });
209
+
210
+ it("does not run a frontend execute on a call ADK resolves itself", async () => {
211
+ const { capture, execute, settle } = await renderStreamingAdk(
212
+ callEvent(),
213
+ serverResultEvent(),
214
+ );
215
+
216
+ expect(toolCalls(capture.runtime!)[0]!.result).toBeUndefined();
217
+ expect(execute).not.toHaveBeenCalled();
218
+
219
+ await settle();
220
+
221
+ expect(toolCalls(capture.runtime!)[0]!.result).toBe('{"server":"deleted"}');
222
+ expect(execute).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it("runs a frontend execute on a call ADK marks long-running", async () => {
226
+ const { capture, execute } = await renderStreamingAdk(
227
+ callEvent(["adk-original-1"]),
228
+ serverResultEvent(),
229
+ );
230
+
231
+ await waitFor(() => expect(execute).toHaveBeenCalledTimes(1));
232
+ await waitFor(() =>
233
+ expect(toolCalls(capture.runtime!)[0]!.result).toBeDefined(),
234
+ );
235
+ });
236
+ });
@@ -1,6 +1,12 @@
1
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
1
  import {
3
- getExternalStoreMessages,
2
+ useCallback,
3
+ useEffect,
4
+ useInsertionEffect,
5
+ useMemo,
6
+ useRef,
7
+ useState,
8
+ } from "react";
9
+ import {
4
10
  pickExternalStoreSharedOptions,
5
11
  type AttachmentAdapter,
6
12
  type DictationAdapter,
@@ -9,11 +15,14 @@ import {
9
15
  type RealtimeVoiceAdapter,
10
16
  type SpeechSynthesisAdapter,
11
17
  type AppendMessage,
12
- type ThreadMessage,
18
+ type ToolCallMessagePart,
13
19
  type ToolExecutionStatus,
14
20
  generateId,
15
21
  } from "@assistant-ui/core";
16
- import { httpUrlPattern, parseDataUrl } from "@assistant-ui/core/internal";
22
+ import {
23
+ createAbortableThreadLoad,
24
+ createCloudThreadListAdapterCreateFallback,
25
+ } from "@assistant-ui/core/internal";
17
26
  import {
18
27
  useCloudThreadListAdapter,
19
28
  useRemoteThreadListRuntime,
@@ -37,139 +46,17 @@ import {
37
46
  convertAdkMessage,
38
47
  createAdkMessageConverter,
39
48
  } from "./convertAdkMessages";
49
+ import {
50
+ getMessageContent,
51
+ getPendingCancellations,
52
+ toAdkUserMessage,
53
+ truncateAdkMessages,
54
+ } from "./convertToAdkMessages";
40
55
  import {
41
56
  projectAdkToolApprovals,
42
57
  toAdkToolConfirmationReply,
43
58
  } from "./adkToolApproval";
44
59
  import { adkExtras } from "./adkExtras";
45
- import { v4 as uuidv4 } from "uuid";
46
-
47
- /** @internal — exported for unit tests. */
48
- export const getMessageContent = (msg: AppendMessage) => {
49
- const allContent = [
50
- ...msg.content,
51
- ...(msg.attachments?.flatMap((a) => a.content) ?? []),
52
- ];
53
- const content = allContent.flatMap((part) => {
54
- const type = part.type;
55
- switch (type) {
56
- case "text":
57
- return { type: "text" as const, text: part.text };
58
- case "image":
59
- return { type: "image_url" as const, url: part.image };
60
- case "file":
61
- if (part.sourceType === "url" || httpUrlPattern.test(part.data)) {
62
- return {
63
- type: "file_url" as const,
64
- url: part.data,
65
- mimeType: part.mimeType,
66
- };
67
- }
68
- return {
69
- type: "file" as const,
70
- mimeType: part.mimeType,
71
- // Lands in Gemini `inlineData.data`, which takes bare base64, so a
72
- // data URL envelope is stripped rather than forwarded.
73
- data: parseDataUrl(part.data)?.data ?? part.data,
74
- ...(part.filename != null && { filename: part.filename }),
75
- };
76
- case "audio": {
77
- const parsed = parseDataUrl(part.audio.data);
78
- return {
79
- type: "file" as const,
80
- mimeType: `audio/${part.audio.format}`,
81
- data: parsed?.data ?? part.audio.data,
82
- };
83
- }
84
- case "data":
85
- return [];
86
-
87
- case "tool-call":
88
- throw new Error("Tool call appends are not supported.");
89
-
90
- default: {
91
- const _exhaustiveCheck: "reasoning" | "source" | "generative-ui" = type;
92
- throw new Error(
93
- `Unsupported append message part type: ${_exhaustiveCheck}`,
94
- );
95
- }
96
- }
97
- });
98
-
99
- if (content.length === 1 && content[0]?.type === "text") {
100
- return content[0].text ?? "";
101
- }
102
-
103
- return content;
104
- };
105
-
106
- /** @internal — exported for unit tests. */
107
- export const getPendingToolCalls = (messages: AdkMessage[]) => {
108
- const pending = new Map<string, { id: string; name: string }>();
109
- for (const msg of messages) {
110
- if (msg.type === "ai" && msg.tool_calls) {
111
- for (const tc of msg.tool_calls) {
112
- pending.set(tc.id, tc);
113
- }
114
- }
115
- if (msg.type === "tool") {
116
- pending.delete(msg.tool_call_id);
117
- }
118
- }
119
- return [...pending.values()];
120
- };
121
-
122
- /**
123
- * @internal — exported for unit tests.
124
- *
125
- * Returns `{cancelled: true}` tool responses for pending tool calls when the
126
- * user sends a new turn, EXCEPT for HITL interrupts marked via
127
- * `long_running_tool_ids` (`adk_request_input`, `adk_request_confirmation`,
128
- * `adk_request_credential`). Those must be answered through a dedicated tool
129
- * UI + submit helper, not auto-cancelled.
130
- */
131
- export const getPendingCancellations = (
132
- messages: AdkMessage[],
133
- longRunningToolIds: readonly string[],
134
- ): Array<AdkMessage & { type: "tool" }> => {
135
- const longRunningSet = new Set(longRunningToolIds);
136
- return getPendingToolCalls(messages)
137
- .filter((t) => !longRunningSet.has(t.id))
138
- .map(
139
- (t) =>
140
- ({
141
- id: uuidv4(),
142
- type: "tool",
143
- name: t.name,
144
- tool_call_id: t.id,
145
- content: JSON.stringify({ cancelled: true }),
146
- status: "error",
147
- }) satisfies AdkMessage & { type: "tool" },
148
- );
149
- };
150
-
151
- const truncateAdkMessages = (
152
- threadMessages: readonly ThreadMessage[],
153
- parentId: string | null,
154
- ): AdkMessage[] => {
155
- if (parentId === null) return [];
156
- const parentIndex = threadMessages.findIndex((m) => m.id === parentId);
157
- if (parentIndex === -1) return [];
158
- const truncated: AdkMessage[] = [];
159
- for (let i = 0; i <= parentIndex && i < threadMessages.length; i++) {
160
- truncated.push(...getExternalStoreMessages<AdkMessage>(threadMessages[i]!));
161
- }
162
- return truncated;
163
- };
164
-
165
- const toAdkUserMessage = (
166
- msg: AppendMessage,
167
- id = generateId(),
168
- ): AdkMessage & { type: "human"; id: string } => ({
169
- id,
170
- type: "human",
171
- content: getMessageContent(msg),
172
- });
173
60
 
174
61
  export type UseAdkRuntimeOptions = ExternalStoreSharedOptions & {
175
62
  stream: AdkStreamCallback;
@@ -256,14 +143,14 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
256
143
  });
257
144
 
258
145
  const loadRef = useRef(load);
259
- loadRef.current = load;
260
- const loadControllerRef = useRef<{
261
- controller: AbortController;
262
- purpose: "initial" | "reload";
263
- promise?: Promise<void> | undefined;
264
- } | null>(null);
146
+ useInsertionEffect(() => {
147
+ loadRef.current = load;
148
+ }, [load]);
149
+ const loadController = useMemo(createAbortableThreadLoad, []);
265
150
  const messagesRef = useRef(messages);
266
- messagesRef.current = messages;
151
+ useInsertionEffect(() => {
152
+ messagesRef.current = messages;
153
+ }, [messages]);
267
154
  const [isLoadingThread, setIsLoadingThread] = useState(
268
155
  () =>
269
156
  load !== undefined && aui.threadListItem.getState().externalId != null,
@@ -278,7 +165,9 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
278
165
  );
279
166
  const effectiveIsRunning = isRunning || hasExecutingTools;
280
167
  const isRunningRef = useRef(effectiveIsRunning);
281
- isRunningRef.current = effectiveIsRunning;
168
+ useInsertionEffect(() => {
169
+ isRunningRef.current = effectiveIsRunning;
170
+ }, [effectiveIsRunning]);
282
171
 
283
172
  const handleSendMessage = async (
284
173
  msgs: AdkMessage[],
@@ -294,9 +183,26 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
294
183
 
295
184
  const { approvals: toolApprovals, key: toolApprovalsKey } =
296
185
  projectAdkToolApprovals(messages);
186
+ // The messageConverter memo below reads this during render, where the ref
187
+ // must carry the same render's approvals; a commit-scoped write would feed
188
+ // the memo the previous commit's approvals whenever the key changes. No
189
+ // callback reads it — approval replies project from the committed messages.
297
190
  const toolApprovalsRef = useRef(toolApprovals);
298
191
  toolApprovalsRef.current = toolApprovals;
299
192
 
193
+ const longRunningToolIdsRef = useRef(longRunningToolIds);
194
+ useInsertionEffect(() => {
195
+ longRunningToolIdsRef.current = longRunningToolIds;
196
+ }, [longRunningToolIds]);
197
+ // ADK resolves every call it did not mark long-running itself, and yields
198
+ // that call to the client one or more events before its own response, so
199
+ // only a long-running call is the client's to execute.
200
+ const isClientToolCall = useCallback(
201
+ (toolCall: ToolCallMessagePart) =>
202
+ longRunningToolIdsRef.current.includes(toolCall.toolCallId),
203
+ [],
204
+ );
205
+
300
206
  const messageConverter = useMemo(
301
207
  () =>
302
208
  toolApprovalsKey === ""
@@ -312,10 +218,17 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
312
218
  });
313
219
 
314
220
  const threadMessagesRef = useRef(threadMessages);
315
- threadMessagesRef.current = threadMessages;
221
+ useInsertionEffect(() => {
222
+ threadMessagesRef.current = threadMessages;
223
+ }, [threadMessages]);
316
224
 
225
+ // Staging assigns adkMessagesRef.current directly, so the effect must key on
226
+ // the committed messages alone; a dep-less publication would clobber the
227
+ // optimistic value on any unrelated commit.
317
228
  const adkMessagesRef = useRef(messages);
318
- adkMessagesRef.current = messages;
229
+ useInsertionEffect(() => {
230
+ adkMessagesRef.current = messages;
231
+ }, [messages]);
319
232
 
320
233
  const stagedMessagesRef = useRef(
321
234
  new Map<
@@ -373,26 +286,17 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
373
286
 
374
287
  // The initial load is already fetching what a refetch would ask for, and
375
288
  // taking it over strands the thread's history if the refetch then fails.
376
- if (
377
- purpose === "reload" &&
378
- loadControllerRef.current?.purpose === "initial"
379
- )
380
- return loadControllerRef.current.promise ?? Promise.resolve();
381
-
382
- loadControllerRef.current?.controller.abort();
383
- const controller = new AbortController();
384
- const record: NonNullable<typeof loadControllerRef.current> = {
385
- controller,
289
+ // Aborting a load the runtime no longer needs is not a failure.
290
+ // A refetch reports the failure to whoever awaited it; the initial load
291
+ // has no caller to tell.
292
+ return loadController.run({
386
293
  purpose,
387
- };
388
- loadControllerRef.current = record;
294
+ load: async (signal) => {
295
+ const messagesAtLoadStart = messagesRef.current;
296
+ if (purpose === "initial") setIsLoadingThread(true);
389
297
 
390
- const messagesAtLoadStart = messagesRef.current;
391
- if (purpose === "initial") setIsLoadingThread(true);
392
-
393
- const task = loadFn(externalId, { signal: controller.signal })
394
- .then((snapshot) => {
395
- if (controller.signal.aborted) return;
298
+ const snapshot = await loadFn(externalId, { signal });
299
+ if (signal.aborted) return;
396
300
  // A snapshot the session assembled before a run cannot speak for what
397
301
  // that run has since produced, and an ADK id cannot correlate a
398
302
  // message sent optimistically with the one the session stored for it,
@@ -406,29 +310,16 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
406
310
  )
407
311
  return;
408
312
  applySnapshot(snapshot);
409
- })
410
- .catch((error: unknown) => {
411
- // Aborting a load the runtime no longer needs is not a failure.
412
- if (controller.signal.aborted) return;
413
- throw error;
414
- })
415
- .finally(() => {
416
- if (loadControllerRef.current?.controller === controller) {
417
- loadControllerRef.current = null;
418
- }
419
- if (controller.signal.aborted) return;
313
+ },
314
+ onSettled: () => {
420
315
  setIsLoadingThread(false);
421
- });
422
- record.promise = task;
423
-
424
- // A refetch reports the failure to whoever awaited it; the initial load
425
- // has no caller to tell.
426
- if (purpose === "reload") return task;
427
- return task.catch((e: unknown) => {
428
- console.warn("Failed to load ADK session:", e);
316
+ },
317
+ onInitialError: (error) => {
318
+ console.warn("Failed to load ADK session:", error);
319
+ },
429
320
  });
430
321
  },
431
- [threadListItem, applySnapshot],
322
+ [threadListItem, loadController, applySnapshot],
432
323
  );
433
324
 
434
325
  useEffect(() => {
@@ -436,17 +327,18 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
436
327
  return () => {
437
328
  // Whatever is current, not this effect's own controller: a refetch swaps
438
329
  // the ref, and one in flight at unmount must be aborted too.
439
- loadControllerRef.current?.controller.abort();
330
+ loadController.abort();
440
331
  setIsLoadingThread(false);
441
332
  };
442
- }, [runLoad]);
333
+ }, [loadController, runLoad]);
443
334
 
444
335
  const runtime = useExternalStoreRuntime({
445
336
  ...pickExternalStoreSharedOptions(options),
446
- isRunning: effectiveIsRunning,
337
+ isRunning,
447
338
  isLoading: isLoadingThread,
448
339
  messages: threadMessages,
449
340
  unstable_enableToolInvocations: true,
341
+ unstable_isClientToolCall: isClientToolCall,
450
342
  setToolStatuses,
451
343
  adapters: { attachments, dictation, feedback, speech, voice },
452
344
  extras: adkExtras.provide({
@@ -475,7 +367,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
475
367
  [
476
368
  ...cancellations,
477
369
  {
478
- id: uuidv4(),
370
+ id: generateId(),
479
371
  type: "human",
480
372
  content: getMessageContent(msg),
481
373
  },
@@ -509,7 +401,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
509
401
  return handleSendMessage(
510
402
  [
511
403
  {
512
- id: uuidv4(),
404
+ id: generateId(),
513
405
  type: "human",
514
406
  content: getMessageContent(msg),
515
407
  },
@@ -564,7 +456,7 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
564
456
  await handleSendMessage(
565
457
  [
566
458
  {
567
- id: uuidv4(),
459
+ id: generateId(),
568
460
  type: "tool",
569
461
  name: toolName,
570
462
  tool_call_id: toolCallId,
@@ -578,7 +470,12 @@ const useAdkRuntimeImpl = (options: UseAdkRuntimeOptions) => {
578
470
  },
579
471
  onRespondToToolApproval: async (options) => {
580
472
  await handleSendMessage(
581
- [toAdkToolConfirmationReply(options, toolApprovalsRef.current)],
473
+ [
474
+ toAdkToolConfirmationReply(
475
+ options,
476
+ projectAdkToolApprovals(adkMessagesRef.current).approvals,
477
+ ),
478
+ ],
582
479
  {},
583
480
  );
584
481
  },
@@ -606,11 +503,10 @@ export const useAdkRuntime = ({
606
503
  const aui = useAui();
607
504
  const cloudAdapter = useCloudThreadListAdapter({
608
505
  cloud,
609
- create: async () => {
610
- if (create) return create();
611
- if (aui.threadListItem.source) return aui.threadListItem.initialize();
612
- return { externalId: undefined };
613
- },
506
+ create: createCloudThreadListAdapterCreateFallback(
507
+ create,
508
+ aui.threadListItem,
509
+ ),
614
510
  delete: deleteFn,
615
511
  });
616
512