@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.
- package/dist/app/extensions/EmbeddedExtension.d.ts.map +1 -1
- package/dist/app/extensions/EmbeddedExtension.js +11 -3
- package/dist/app/extensions/EmbeddedExtension.js.map +1 -1
- package/dist/app/extensions/ExtensionViewer.d.ts.map +1 -1
- package/dist/app/extensions/ExtensionViewer.js +12 -2
- package/dist/app/extensions/ExtensionViewer.js.map +1 -1
- package/dist/app/extensions/InlineExtensionFrame.d.ts.map +1 -1
- package/dist/app/extensions/InlineExtensionFrame.js +10 -2
- package/dist/app/extensions/InlineExtensionFrame.js.map +1 -1
- package/dist/app/extensions/open-extension-link.d.ts +15 -0
- package/dist/app/extensions/open-extension-link.d.ts.map +1 -0
- package/dist/app/extensions/open-extension-link.js +76 -0
- package/dist/app/extensions/open-extension-link.js.map +1 -0
- package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
- package/dist/composer/ComposerContextPickerDialog.js +56 -14
- package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
- package/dist/composer/index.d.ts +1 -0
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -0
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/useComposerContextPicker.d.ts +5 -1
- package/dist/composer/useComposerContextPicker.d.ts.map +1 -1
- package/dist/composer/useComposerContextPicker.js.map +1 -1
- package/package.json +4 -4
- package/src/app/extensions/EmbeddedExtension.tsx +19 -3
- package/src/app/extensions/ExtensionViewer.spec.tsx +100 -2
- package/src/app/extensions/ExtensionViewer.tsx +20 -2
- package/src/app/extensions/InlineExtensionFrame.spec.tsx +136 -0
- package/src/app/extensions/InlineExtensionFrame.tsx +18 -2
- package/src/app/extensions/open-extension-link.spec.ts +141 -0
- package/src/app/extensions/open-extension-link.ts +94 -0
- package/src/composer/ComposerContextPickerDialog.spec.tsx +229 -0
- package/src/composer/ComposerContextPickerDialog.tsx +61 -17
- package/src/composer/index.ts +1 -0
- 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
|
|
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 &&
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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 ||
|
|
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={
|
|
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"
|
package/src/composer/index.ts
CHANGED
|
@@ -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?:
|
|
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,
|