@agent-native/toolkit 0.209.2 → 0.209.3-nightly-20261016183441

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/app/extensions/EmbeddedExtension.d.ts.map +1 -1
  2. package/dist/app/extensions/EmbeddedExtension.js +11 -3
  3. package/dist/app/extensions/EmbeddedExtension.js.map +1 -1
  4. package/dist/app/extensions/ExtensionViewer.d.ts.map +1 -1
  5. package/dist/app/extensions/ExtensionViewer.js +12 -2
  6. package/dist/app/extensions/ExtensionViewer.js.map +1 -1
  7. package/dist/app/extensions/InlineExtensionFrame.d.ts.map +1 -1
  8. package/dist/app/extensions/InlineExtensionFrame.js +10 -2
  9. package/dist/app/extensions/InlineExtensionFrame.js.map +1 -1
  10. package/dist/app/extensions/open-extension-link.d.ts +15 -0
  11. package/dist/app/extensions/open-extension-link.d.ts.map +1 -0
  12. package/dist/app/extensions/open-extension-link.js +76 -0
  13. package/dist/app/extensions/open-extension-link.js.map +1 -0
  14. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
  15. package/dist/composer/ComposerContextPickerDialog.js +56 -14
  16. package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
  17. package/dist/composer/index.d.ts +1 -0
  18. package/dist/composer/index.d.ts.map +1 -1
  19. package/dist/composer/index.js +1 -0
  20. package/dist/composer/index.js.map +1 -1
  21. package/dist/composer/useComposerContextPicker.d.ts +5 -1
  22. package/dist/composer/useComposerContextPicker.d.ts.map +1 -1
  23. package/dist/composer/useComposerContextPicker.js.map +1 -1
  24. package/package.json +4 -4
  25. package/src/app/extensions/EmbeddedExtension.tsx +19 -3
  26. package/src/app/extensions/ExtensionViewer.spec.tsx +100 -2
  27. package/src/app/extensions/ExtensionViewer.tsx +20 -2
  28. package/src/app/extensions/InlineExtensionFrame.spec.tsx +136 -0
  29. package/src/app/extensions/InlineExtensionFrame.tsx +18 -2
  30. package/src/app/extensions/open-extension-link.spec.ts +141 -0
  31. package/src/app/extensions/open-extension-link.ts +94 -0
  32. package/src/composer/ComposerContextPickerDialog.spec.tsx +229 -0
  33. package/src/composer/ComposerContextPickerDialog.tsx +61 -17
  34. package/src/composer/index.ts +1 -0
  35. package/src/composer/useComposerContextPicker.ts +8 -1
@@ -9,6 +9,8 @@ import {
9
9
  ComposerContextMenu,
10
10
  type ComposerContextPickerConfig,
11
11
  } from "./ComposerContextMenu.js";
12
+ import { ComposerContextPickerDialog } from "./ComposerContextPickerDialog.js";
13
+ import { ComposerRuntimeAdaptersProvider } from "./runtime-adapters.js";
12
14
 
13
15
  let container: HTMLDivElement;
14
16
  let root: ReturnType<typeof createRoot>;
@@ -24,6 +26,7 @@ beforeEach(() => {
24
26
  afterEach(async () => {
25
27
  await act(async () => root.unmount());
26
28
  container.remove();
29
+ vi.useRealTimers();
27
30
  vi.restoreAllMocks();
28
31
  vi.unstubAllGlobals();
29
32
  });
@@ -88,6 +91,25 @@ async function render(
88
91
  ),
89
92
  );
90
93
  }
94
+ async function renderPickerDialog(
95
+ config: ComposerContextPickerConfig,
96
+ onClose: () => void = () => {},
97
+ ) {
98
+ await act(async () =>
99
+ root.render(
100
+ <TooltipProvider>
101
+ <ComposerRuntimeAdaptersProvider adapters={{ translate: (key) => key }}>
102
+ <ComposerContextPickerDialog
103
+ config={config}
104
+ title="Pick a presentation"
105
+ onClose={onClose}
106
+ onRestoreFocus={() => {}}
107
+ />
108
+ </ComposerRuntimeAdaptersProvider>
109
+ </TooltipProvider>,
110
+ ),
111
+ );
112
+ }
91
113
  async function tick() {
92
114
  await act(async () => {
93
115
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -162,6 +184,213 @@ function deferred<T>() {
162
184
  }
163
185
 
164
186
  describe("shared context URL dialog", () => {
187
+ it("supports gallery previews with immediate single selection", async () => {
188
+ const onSelect = vi.fn().mockResolvedValue(undefined);
189
+ const onClose = vi.fn();
190
+ const config = {
191
+ presentation: {
192
+ type: "dialog" as const,
193
+ mode: "single" as const,
194
+ layout: "gallery" as const,
195
+ },
196
+ searchPlaceholder: "Search presentations",
197
+ items: [
198
+ {
199
+ id: "campaign",
200
+ title: "Campaign launch",
201
+ preview: (
202
+ <img src="https://example.com/campaign.png" alt="Campaign slide" />
203
+ ),
204
+ },
205
+ {
206
+ id: "quarterly",
207
+ title: "Quarterly review",
208
+ preview: (
209
+ <img
210
+ src="https://example.com/quarterly.png"
211
+ alt="Quarterly slide"
212
+ />
213
+ ),
214
+ },
215
+ ],
216
+ onSelect,
217
+ } satisfies ComposerContextPickerConfig;
218
+
219
+ await renderPickerDialog(config, onClose);
220
+ expect(
221
+ dialog()!.querySelectorAll(".agent-template-library-card"),
222
+ ).toHaveLength(2);
223
+ expect(dialog()!.querySelectorAll("img")).toHaveLength(2);
224
+
225
+ await type("Campaign", 'input[type="search"]');
226
+ expect(
227
+ dialog()!.querySelectorAll(".agent-template-library-card"),
228
+ ).toHaveLength(1);
229
+ const card = dialog()!.querySelector<HTMLElement>("[aria-pressed]")!;
230
+ await act(async () => card.click());
231
+ await tick();
232
+
233
+ expect(onSelect).toHaveBeenCalledWith(
234
+ expect.objectContaining({
235
+ id: "campaign",
236
+ title: "Campaign launch",
237
+ disabled: false,
238
+ preview: expect.anything(),
239
+ }),
240
+ expect.objectContaining({ signal: expect.any(AbortSignal) }),
241
+ );
242
+ expect(onClose).toHaveBeenCalledOnce();
243
+ });
244
+
245
+ it("debounces remote gallery searches and disables stale results", async () => {
246
+ const nextResults = deferred<{
247
+ items: Array<{ id: string; title: string }>;
248
+ hasMore: boolean;
249
+ }>();
250
+ const load = vi.fn(({ search }: { search: string }) =>
251
+ search
252
+ ? nextResults.promise
253
+ : Promise.resolve({
254
+ items: [{ id: "old", title: "Old result" }],
255
+ hasMore: false,
256
+ }),
257
+ );
258
+ const onSelect = vi.fn();
259
+ const config = {
260
+ presentation: {
261
+ type: "dialog" as const,
262
+ mode: "single" as const,
263
+ layout: "gallery" as const,
264
+ },
265
+ searchPlaceholder: "Search decks",
266
+ load,
267
+ onSelect,
268
+ } satisfies ComposerContextPickerConfig;
269
+
270
+ await renderPickerDialog(config);
271
+ await tick();
272
+ expect(load).toHaveBeenCalledTimes(1);
273
+
274
+ const primaryCard = () =>
275
+ dialog()!.querySelector<HTMLElement>(".agent-template-library-primary")!;
276
+ expect(primaryCard().getAttribute("aria-disabled")).toBeNull();
277
+
278
+ vi.useFakeTimers();
279
+ await type("re", 'input[type="search"]');
280
+ expect(primaryCard().getAttribute("aria-disabled")).toBe("true");
281
+ await act(async () => {
282
+ await vi.advanceTimersByTimeAsync(100);
283
+ });
284
+
285
+ await type("replacement", 'input[type="search"]');
286
+ expect(primaryCard().getAttribute("aria-disabled")).toBe("true");
287
+ await act(async () => primaryCard().click());
288
+ expect(onSelect).not.toHaveBeenCalled();
289
+ await act(async () => {
290
+ await vi.advanceTimersByTimeAsync(100);
291
+ });
292
+ expect(load).toHaveBeenCalledTimes(1);
293
+ await act(async () => {
294
+ await vi.advanceTimersByTimeAsync(100);
295
+ });
296
+ expect(load).toHaveBeenCalledTimes(2);
297
+ expect(load.mock.calls[1][0].search).toBe("replacement");
298
+
299
+ await act(async () =>
300
+ nextResults.resolve({
301
+ items: [{ id: "new", title: "Replacement result" }],
302
+ hasMore: false,
303
+ }),
304
+ );
305
+ await act(async () => {
306
+ await Promise.resolve();
307
+ });
308
+
309
+ expect(primaryCard().getAttribute("aria-disabled")).toBeNull();
310
+ expect(primaryCard().textContent).toContain("Replacement result");
311
+ });
312
+
313
+ it("retries a failed remote gallery load", async () => {
314
+ const load = vi
315
+ .fn()
316
+ .mockRejectedValueOnce(new Error("Decks are unavailable"))
317
+ .mockResolvedValueOnce({
318
+ items: [{ id: "deck", title: "Campaign deck" }],
319
+ hasMore: false,
320
+ });
321
+ const config = {
322
+ presentation: {
323
+ type: "dialog" as const,
324
+ mode: "single" as const,
325
+ layout: "gallery" as const,
326
+ },
327
+ searchPlaceholder: "Search decks",
328
+ load,
329
+ onSelect: vi.fn(),
330
+ } satisfies ComposerContextPickerConfig;
331
+
332
+ await renderPickerDialog(config);
333
+ await tick();
334
+
335
+ expect(dialog()!.querySelector('[role="alert"]')?.textContent).toContain(
336
+ "Decks are unavailable",
337
+ );
338
+ expect(
339
+ Array.from(dialog()!.querySelectorAll("button")).filter(
340
+ (button) => button.textContent === "agentChat.common.retry",
341
+ ),
342
+ ).toHaveLength(1);
343
+ await click("agentChat.common.retry");
344
+
345
+ expect(load).toHaveBeenCalledTimes(2);
346
+ expect(dialog()!.textContent).toContain("Campaign deck");
347
+ });
348
+
349
+ it("preserves pagination cursors when a search returns to the current query", async () => {
350
+ const load = vi.fn(({ page }: { page: number }) =>
351
+ Promise.resolve({
352
+ items: [{ id: `deck-${page}`, title: `Deck ${page}` }],
353
+ hasMore: page < 3,
354
+ nextCursor: page < 3 ? `cursor-${page + 1}` : undefined,
355
+ }),
356
+ );
357
+ const config = {
358
+ presentation: {
359
+ type: "dialog" as const,
360
+ mode: "single" as const,
361
+ layout: "gallery" as const,
362
+ },
363
+ searchPlaceholder: "Search decks",
364
+ load,
365
+ onSelect: vi.fn(),
366
+ } satisfies ComposerContextPickerConfig;
367
+
368
+ await renderPickerDialog(config);
369
+ await tick();
370
+ await click("agentChat.composer.contextNext");
371
+ await click("agentChat.composer.contextNext");
372
+ expect(load.mock.calls[2][0]).toMatchObject({
373
+ page: 3,
374
+ cursor: "cursor-3",
375
+ });
376
+
377
+ vi.useFakeTimers();
378
+ await type("temporary", 'input[type="search"]');
379
+ await type("", 'input[type="search"]');
380
+ expect(button("agentChat.composer.contextPrevious").disabled).toBe(true);
381
+ await act(async () => {
382
+ await vi.advanceTimersByTimeAsync(200);
383
+ });
384
+ expect(load).toHaveBeenCalledTimes(3);
385
+
386
+ vi.useRealTimers();
387
+ await click("agentChat.composer.contextPrevious");
388
+ expect(load.mock.calls[3][0]).toMatchObject({
389
+ page: 2,
390
+ cursor: "cursor-2",
391
+ });
392
+ });
393
+
165
394
  it("shows distinct preview cards for duplicate titles and retains selections through search", async () => {
166
395
  const onAttach = vi.fn().mockResolvedValue(undefined);
167
396
  const items = [
@@ -39,20 +39,39 @@ export function ComposerContextPickerDialog({
39
39
  const urlOnly = presentation?.mode === "url";
40
40
  const singleSelect = presentation?.mode === "single";
41
41
  const gallery =
42
- presentation?.mode === "multiple" && presentation.layout === "gallery";
42
+ presentation?.mode !== "url" && presentation?.layout === "gallery";
43
43
  const id = useId();
44
44
  const input = useRef<HTMLInputElement>(null);
45
+ const searchDebounce = useRef<ReturnType<typeof setTimeout> | null>(null);
46
+ const [search, setSearch] = useState(location.search);
47
+ const [searchDebouncing, setSearchDebouncing] = useState(false);
48
+ const remoteGallery = gallery && Boolean(config.load);
49
+ const searchPending =
50
+ remoteGallery && (search !== location.search || searchDebouncing);
45
51
  const [touched, setTouched] = useState(false);
46
52
  useEffect(() => {
47
53
  input.current?.focus();
48
54
  }, [stage]);
55
+ useEffect(() => {
56
+ setSearch(location.search);
57
+ }, [location.search]);
58
+ useEffect(
59
+ () => () => {
60
+ if (searchDebounce.current) clearTimeout(searchDebounce.current);
61
+ },
62
+ [],
63
+ );
49
64
  const validation = touched && link.trim() ? picker.linkError : undefined;
50
65
  const error = stage === "link" ? picker.actionError?.message : picker.error;
51
66
  const attach = t("agentChat.composer.contextAttach", {
52
67
  defaultValue: "Attach",
53
68
  });
54
69
  const canAttach =
55
- picker.selectedItems.length > 0 && !loading && !selecting && !error;
70
+ picker.selectedItems.length > 0 &&
71
+ !loading &&
72
+ !searchPending &&
73
+ !selecting &&
74
+ !error;
56
75
  return (
57
76
  <Dialog
58
77
  open
@@ -126,14 +145,29 @@ export function ComposerContextPickerDialog({
126
145
  type="search"
127
146
  aria-label={config.searchPlaceholder}
128
147
  placeholder={config.searchPlaceholder}
129
- value={location.search}
148
+ value={remoteGallery ? search : location.search}
130
149
  onChange={(event) => {
131
- picker.cursors.current = [undefined];
132
- picker.move({
133
- search: event.target.value,
134
- page: 1,
135
- url: location.url,
136
- });
150
+ const value = event.target.value;
151
+ setSearch(value);
152
+ if (searchDebounce.current)
153
+ clearTimeout(searchDebounce.current);
154
+ if (remoteGallery) {
155
+ setSearchDebouncing(true);
156
+ searchDebounce.current = setTimeout(() => {
157
+ searchDebounce.current = null;
158
+ setSearchDebouncing(false);
159
+ if (value === location.search) return;
160
+ picker.cursors.current = [undefined];
161
+ picker.move({
162
+ search: value,
163
+ page: 1,
164
+ url: location.url,
165
+ });
166
+ }, 200);
167
+ } else {
168
+ picker.cursors.current = [undefined];
169
+ picker.move({ search: value, page: 1, url: location.url });
170
+ }
137
171
  }}
138
172
  />
139
173
  {gallery ? (
@@ -142,11 +176,14 @@ export function ComposerContextPickerDialog({
142
176
  items={items.map((item) => ({
143
177
  ...item,
144
178
  disabled:
145
- item.disabled || config.selectedIds?.includes(item.id),
179
+ item.disabled ||
180
+ (!singleSelect &&
181
+ config.selectedIds?.includes(item.id)),
146
182
  }))}
147
- loading={loading}
183
+ loading={loading || searchPending}
148
184
  error={error}
149
- disabled={selecting}
185
+ disabled={selecting || loading || searchPending}
186
+ onRetry={picker.retry}
150
187
  labels={{
151
188
  loading: t("agentChat.composer.contextPending", {
152
189
  defaultValue: "Loading context…",
@@ -166,14 +203,18 @@ export function ComposerContextPickerDialog({
166
203
  (selected) => selected.id === item.id,
167
204
  )
168
205
  }
169
- onSelect={(item) =>
206
+ onSelect={(item) => {
207
+ if (singleSelect) {
208
+ void picker.select(item);
209
+ return;
210
+ }
170
211
  picker.toggleItem(
171
212
  item,
172
213
  !picker.selectedItems.some(
173
214
  (selected) => selected.id === item.id,
174
215
  ),
175
- )
176
- }
216
+ );
217
+ }}
177
218
  renderPreview={(item) => (
178
219
  <div className="pointer-events-none relative h-full overflow-hidden bg-muted">
179
220
  {item.preview}
@@ -287,7 +328,7 @@ export function ComposerContextPickerDialog({
287
328
  <Button
288
329
  type="button"
289
330
  variant="outline"
290
- disabled={loading || selecting}
331
+ disabled={loading || searchPending || selecting}
291
332
  onClick={() =>
292
333
  picker.move({
293
334
  ...location,
@@ -305,7 +346,9 @@ export function ComposerContextPickerDialog({
305
346
  <Button
306
347
  type="button"
307
348
  variant="outline"
308
- disabled={loading || selecting || Boolean(error)}
349
+ disabled={
350
+ loading || searchPending || selecting || Boolean(error)
351
+ }
309
352
  onClick={() => {
310
353
  picker.cursors.current = [
311
354
  ...picker.cursors.current.slice(0, location.page),
@@ -357,6 +400,7 @@ export function ComposerContextPickerDialog({
357
400
  </Button>
358
401
  ))}
359
402
  {error &&
403
+ !gallery &&
360
404
  (stage === "link" ? picker.actionError?.retry : picker.retry) && (
361
405
  <Button
362
406
  type="button"
@@ -20,6 +20,7 @@ export {
20
20
  type ComposerContextPickerSelection,
21
21
  type ComposerContextPickerFooterAction,
22
22
  } from "./ComposerContextMenu.js";
23
+ export { ComposerContextPickerDialog } from "./ComposerContextPickerDialog.js";
23
24
  export {
24
25
  areComposerContextItemsReady,
25
26
  composerContextFits,
@@ -79,7 +79,14 @@ export type ComposerContextPickerConfig = {
79
79
  ) &
80
80
  (
81
81
  | {
82
- presentation?: "submenu" | { type: "dialog"; mode: "single" | "url" };
82
+ presentation?:
83
+ | "submenu"
84
+ | { type: "dialog"; mode: "url" }
85
+ | {
86
+ type: "dialog";
87
+ mode: "single";
88
+ layout?: "list" | "gallery";
89
+ };
83
90
  onSelect: (
84
91
  item: ComposerContextPickerItem,
85
92
  request: ComposerContextPickerRequest,