@agent-native/toolkit 0.226.0 → 0.226.1-nightly-20261107004051
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/agentkit/react/components.d.ts +11 -9
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +115 -111
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/stop-button.js +5 -5
- package/dist/app/agentkit/react/stop-button.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +19 -48
- package/dist/app/auth/AuthPage.d.ts.map +1 -1
- package/dist/app/auth/AuthPage.js +7 -5
- package/dist/app/auth/AuthPage.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +7 -2
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/integrations/IntegrationDetailPage.d.ts.map +1 -1
- package/dist/app/integrations/IntegrationDetailPage.js +43 -1
- package/dist/app/integrations/IntegrationDetailPage.js.map +1 -1
- package/dist/app/integrations/IntegrationsPage.d.ts.map +1 -1
- package/dist/app/integrations/IntegrationsPage.js +16 -7
- package/dist/app/integrations/IntegrationsPage.js.map +1 -1
- package/dist/app/integrations/connected-server-id.d.ts +17 -0
- package/dist/app/integrations/connected-server-id.d.ts.map +1 -0
- package/dist/app/integrations/connected-server-id.js +26 -0
- package/dist/app/integrations/connected-server-id.js.map +1 -0
- package/dist/composer/TiptapComposer.d.ts +5 -3
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +1 -1
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/package.json +4 -4
- package/src/app/agentkit/react/components.interaction.spec.tsx +112 -0
- package/src/app/agentkit/react/components.tsx +178 -192
- package/src/app/agentkit/react/lifecycle.spec.tsx +5 -16
- package/src/app/agentkit/react/stop-button.spec.tsx +1 -1
- package/src/app/agentkit/react/stop-button.tsx +5 -5
- package/src/app/agentkit/react/styles.css +19 -48
- package/src/app/agentkit/react/work-handoff.spec.tsx +202 -69
- package/src/app/auth/AuthPage.interaction.spec.tsx +17 -1
- package/src/app/auth/AuthPage.tsx +12 -4
- package/src/app/chat/AgentKitAssistantChat.tsx +6 -2
- package/src/app/integrations/IntegrationDetailPage.spec.tsx +30 -0
- package/src/app/integrations/IntegrationDetailPage.tsx +85 -1
- package/src/app/integrations/IntegrationsPage.spec.tsx +74 -0
- package/src/app/integrations/IntegrationsPage.tsx +47 -20
- package/src/app/integrations/connected-server-id.ts +34 -0
- package/src/composer/TiptapComposer.spec.ts +7 -7
- package/src/composer/TiptapComposer.tsx +12 -13
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +24 -1
|
@@ -9,10 +9,10 @@ import {
|
|
|
9
9
|
useAgentThread,
|
|
10
10
|
} from "./context.js";
|
|
11
11
|
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// primaries.
|
|
15
|
-
const
|
|
12
|
+
// During voice chat, End voice chat holds the primary slot beside Stop (it
|
|
13
|
+
// renders as the send-button slot), so Stop steps down to secondary and the
|
|
14
|
+
// row never shows two identical primaries. A draft hides Stop instead.
|
|
15
|
+
const STOP_BESIDE_PRIMARY =
|
|
16
16
|
"[[data-agent-composer-slot=toolbar]:has([data-agent-composer-slot=send-button])_&]:bg-secondary [[data-agent-composer-slot=toolbar]:has([data-agent-composer-slot=send-button])_&]:text-secondary-foreground";
|
|
17
17
|
|
|
18
18
|
// Stands in for the run key while a submission has no run id yet, so the
|
|
@@ -89,7 +89,7 @@ export function useAgentKitStopButton(options: {
|
|
|
89
89
|
aria-busy={stopping || undefined}
|
|
90
90
|
disabled={stopping}
|
|
91
91
|
data-agent-composer-slot="stop-button"
|
|
92
|
-
className={`flex size-[var(--agentkit-composer-primary-control-size,2rem)] shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-[opacity,background-color,color] duration-150 hover:opacity-90 disabled:cursor-default disabled:opacity-35 ${
|
|
92
|
+
className={`flex size-[var(--agentkit-composer-primary-control-size,2rem)] shrink-0 items-center justify-center rounded-full bg-primary text-primary-foreground transition-[opacity,background-color,color] duration-150 hover:opacity-90 disabled:cursor-default disabled:opacity-35 ${STOP_BESIDE_PRIMARY}`}
|
|
93
93
|
>
|
|
94
94
|
<IconPlayerStopFilled aria-hidden="true" className="size-4" />
|
|
95
95
|
</button>
|
|
@@ -1257,49 +1257,6 @@
|
|
|
1257
1257
|
grid-row: 2;
|
|
1258
1258
|
}
|
|
1259
1259
|
|
|
1260
|
-
/* A live finish: the row's old place above the reply closes while the row
|
|
1261
|
-
fades in on the footer line. At rest the placeholder takes no space, so a
|
|
1262
|
-
host that drops the animation never keeps a gap. 180ms matches
|
|
1263
|
-
AGENT_WORK_HANDOFF_MS. */
|
|
1264
|
-
.agentkit-work-handoff {
|
|
1265
|
-
block-size: 0;
|
|
1266
|
-
margin-block: 0;
|
|
1267
|
-
overflow: hidden;
|
|
1268
|
-
opacity: 0;
|
|
1269
|
-
pointer-events: none;
|
|
1270
|
-
animation: agentkit-work-handoff-leave 180ms ease-out both;
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
.agentkit-message-work-host
|
|
1274
|
-
.agentkit-activities[data-handoff="enter"]
|
|
1275
|
-
.agentkit-activities-row {
|
|
1276
|
-
animation: agentkit-work-handoff-enter 180ms ease-out both;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
@keyframes agentkit-work-handoff-leave {
|
|
1280
|
-
from {
|
|
1281
|
-
block-size: var(--agentkit-work-handoff-size);
|
|
1282
|
-
margin-block: var(--agentkit-run-segment-gap, 0.75rem);
|
|
1283
|
-
opacity: 1;
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
@keyframes agentkit-work-handoff-enter {
|
|
1288
|
-
from {
|
|
1289
|
-
opacity: 0;
|
|
1290
|
-
translate: 0 -0.25rem;
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
@media (prefers-reduced-motion: reduce) {
|
|
1295
|
-
.agentkit-work-handoff,
|
|
1296
|
-
.agentkit-message-work-host
|
|
1297
|
-
.agentkit-activities[data-handoff="enter"]
|
|
1298
|
-
.agentkit-activities-row {
|
|
1299
|
-
animation: none;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
1260
|
/* The last icon's glyph, not its 24px hit area, ends on the text edge. */
|
|
1304
1261
|
.agentkit-message-actions[data-role="assistant"] .agentkit-message-toolbar {
|
|
1305
1262
|
grid-column: 2;
|
|
@@ -1465,17 +1422,25 @@
|
|
|
1465
1422
|
pointer-events: auto;
|
|
1466
1423
|
}
|
|
1467
1424
|
|
|
1468
|
-
.agentkit-message
|
|
1469
|
-
|
|
1425
|
+
.agentkit-message
|
|
1426
|
+
.agentkit-message-actions[data-streaming="true"]:not(
|
|
1427
|
+
:has(.agentkit-activities)
|
|
1428
|
+
),
|
|
1429
|
+
.agentkit-message:hover
|
|
1430
|
+
.agentkit-message-actions[data-streaming="true"]:not(
|
|
1431
|
+
:has(.agentkit-activities)
|
|
1432
|
+
),
|
|
1470
1433
|
.agentkit-message:focus-within
|
|
1471
|
-
.agentkit-message-actions[data-streaming="true"]
|
|
1434
|
+
.agentkit-message-actions[data-streaming="true"]:not(
|
|
1435
|
+
:has(.agentkit-activities)
|
|
1436
|
+
) {
|
|
1472
1437
|
opacity: 0;
|
|
1473
1438
|
visibility: hidden;
|
|
1474
1439
|
pointer-events: none;
|
|
1475
1440
|
}
|
|
1476
1441
|
|
|
1477
|
-
/* A footer carrying
|
|
1478
|
-
older reply's controls wait for hover or focus. */
|
|
1442
|
+
/* A footer carrying its reply's work always shows and takes its space, live
|
|
1443
|
+
or finished; only an older reply's controls wait for hover or focus. */
|
|
1479
1444
|
.agentkit-message-actions[data-role="assistant"]:has(.agentkit-activities) {
|
|
1480
1445
|
position: static;
|
|
1481
1446
|
opacity: 1;
|
|
@@ -1483,6 +1448,12 @@
|
|
|
1483
1448
|
pointer-events: auto;
|
|
1484
1449
|
}
|
|
1485
1450
|
|
|
1451
|
+
.agentkit-message-actions[data-role="assistant"][data-streaming="true"]
|
|
1452
|
+
.agentkit-message-toolbar {
|
|
1453
|
+
visibility: hidden;
|
|
1454
|
+
pointer-events: none;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1486
1457
|
.agentkit-message-actions[data-role="assistant"]:has(
|
|
1487
1458
|
.agentkit-activities
|
|
1488
1459
|
):not([data-latest="true"], [data-expanded="true"])
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
type AgentThreadState,
|
|
9
9
|
} from "@agent-native/agentkit/client";
|
|
10
10
|
import type { AgentEvent } from "@agent-native/agentkit/protocol";
|
|
11
|
-
import { act } from "react";
|
|
11
|
+
import { act, useEffect } from "react";
|
|
12
12
|
import { createRoot, type Root } from "react-dom/client";
|
|
13
13
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
14
14
|
|
|
@@ -150,68 +150,43 @@ function stubReducedMotion(reduce: boolean) {
|
|
|
150
150
|
|
|
151
151
|
const work = () =>
|
|
152
152
|
container.querySelector<HTMLElement>(".agentkit-activities")!;
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
vi.spyOn(HTMLElement.prototype, "offsetHeight", "get").mockReturnValue(40);
|
|
161
|
-
});
|
|
153
|
+
const follows = (later: Node, earlier: Node) =>
|
|
154
|
+
Boolean(
|
|
155
|
+
earlier.compareDocumentPosition(later) & Node.DOCUMENT_POSITION_FOLLOWING,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
describe("Worked for row moving into the reply footer", () => {
|
|
159
|
+
beforeEach(() => stubReducedMotion(false));
|
|
162
160
|
|
|
163
161
|
afterEach(async () => {
|
|
164
162
|
await act(async () => root.unmount());
|
|
165
163
|
container.remove();
|
|
166
164
|
vi.unstubAllGlobals();
|
|
167
|
-
vi.restoreAllMocks();
|
|
168
165
|
});
|
|
169
166
|
|
|
170
|
-
it("
|
|
167
|
+
it("lets the streaming reply be the focus, then puts the row in its footer", async () => {
|
|
171
168
|
const live = reduce(liveEvents);
|
|
172
169
|
const observable = observableController(snapshotOf(live, 0));
|
|
173
170
|
await render(observable.controller);
|
|
174
|
-
|
|
175
|
-
//
|
|
176
|
-
|
|
177
|
-
work()
|
|
178
|
-
.querySelector<HTMLButtonElement>(".agentkit-disclosure-summary")!
|
|
179
|
-
.click(),
|
|
180
|
-
);
|
|
181
|
-
expect(work().dataset.open).toBe("true");
|
|
171
|
+
const reply = () => container.querySelector('[data-message-id="reply"]')!;
|
|
172
|
+
// oracle-change: the reply's text is the newest output, so no work row shows while it streams, and the finish no longer animates a placeholder out of the space above the reply.
|
|
173
|
+
expect(container.querySelector(".agentkit-activities")).toBeNull();
|
|
182
174
|
|
|
183
175
|
await act(async () =>
|
|
184
176
|
observable.update(snapshotOf(reduce(finishEvents, live), 1)),
|
|
185
177
|
);
|
|
186
178
|
await flush();
|
|
187
179
|
|
|
188
|
-
|
|
189
|
-
expect(settled.closest(".agentkit-message-actions")).not.toBeNull();
|
|
190
|
-
expect(settled.dataset.handoff).toBe("enter");
|
|
191
|
-
// It closes as it moves, so its steps never open below the reply.
|
|
192
|
-
expect(settled.dataset.open).toBe("false");
|
|
193
|
-
const leaving = placeholder();
|
|
194
|
-
expect(leaving).not.toBeNull();
|
|
195
|
-
expect(leaving!.getAttribute("aria-hidden")).toBe("true");
|
|
180
|
+
expect(container.querySelectorAll(".agentkit-activities")).toHaveLength(1);
|
|
196
181
|
expect(
|
|
197
|
-
|
|
198
|
-
).toBe(
|
|
199
|
-
expect(
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
expect(
|
|
203
|
-
leaving!.compareDocumentPosition(reply) &
|
|
204
|
-
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
205
|
-
).toBeTruthy();
|
|
206
|
-
|
|
207
|
-
await act(async () => {
|
|
208
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
209
|
-
});
|
|
210
|
-
expect(placeholder()).toBeNull();
|
|
211
|
-
expect(work().dataset.handoff).toBeUndefined();
|
|
182
|
+
reply().contains(work().closest(".agentkit-message-work-host")),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
expect(work().textContent).toContain("Worked for");
|
|
185
|
+
// Nothing is left holding a place above the reply.
|
|
186
|
+
expect(reply().previousElementSibling).toBeNull();
|
|
212
187
|
});
|
|
213
188
|
|
|
214
|
-
it("
|
|
189
|
+
it("moves a row it saw running when the reply lands whole", async () => {
|
|
215
190
|
const running = reduce(liveEvents.slice(0, 2));
|
|
216
191
|
const observable = observableController(snapshotOf(running, 0));
|
|
217
192
|
await render(observable.controller);
|
|
@@ -235,33 +210,17 @@ describe("Worked for handoff to the reply footer", () => {
|
|
|
235
210
|
),
|
|
236
211
|
);
|
|
237
212
|
await flush();
|
|
213
|
+
expect(container.querySelectorAll(".agentkit-activities")).toHaveLength(1);
|
|
238
214
|
expect(work().closest(".agentkit-message-actions")).not.toBeNull();
|
|
239
|
-
expect(work().dataset.handoff).toBe("enter");
|
|
240
|
-
expect(placeholder()).not.toBeNull();
|
|
241
215
|
});
|
|
242
216
|
|
|
243
|
-
it("
|
|
217
|
+
it("renders a thread that loads finished with its row in the footer", async () => {
|
|
244
218
|
const finished = reduce(finishEvents, reduce(liveEvents));
|
|
245
219
|
const observable = observableController(snapshotOf(finished, 0));
|
|
246
220
|
await render(observable.controller);
|
|
247
221
|
await flush();
|
|
222
|
+
expect(container.querySelectorAll(".agentkit-activities")).toHaveLength(1);
|
|
248
223
|
expect(work().closest(".agentkit-message-actions")).not.toBeNull();
|
|
249
|
-
expect(work().dataset.handoff).toBeUndefined();
|
|
250
|
-
expect(placeholder()).toBeNull();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("moves at once with reduced motion", async () => {
|
|
254
|
-
stubReducedMotion(true);
|
|
255
|
-
const live = reduce(liveEvents);
|
|
256
|
-
const observable = observableController(snapshotOf(live, 0));
|
|
257
|
-
await render(observable.controller);
|
|
258
|
-
await act(async () =>
|
|
259
|
-
observable.update(snapshotOf(reduce(finishEvents, live), 1)),
|
|
260
|
-
);
|
|
261
|
-
await flush();
|
|
262
|
-
expect(work().closest(".agentkit-message-actions")).not.toBeNull();
|
|
263
|
-
expect(work().dataset.handoff).toBeUndefined();
|
|
264
|
-
expect(placeholder()).toBeNull();
|
|
265
224
|
});
|
|
266
225
|
});
|
|
267
226
|
|
|
@@ -340,20 +299,153 @@ describe("a reply that narrates between steps", () => {
|
|
|
340
299
|
vi.unstubAllGlobals();
|
|
341
300
|
});
|
|
342
301
|
|
|
343
|
-
|
|
302
|
+
const textOf = (text: string): Node => {
|
|
303
|
+
const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
|
|
304
|
+
for (let node = walker.nextNode(); node; node = walker.nextNode()) {
|
|
305
|
+
if (node.textContent?.includes(text)) return node;
|
|
306
|
+
}
|
|
307
|
+
throw new Error(`No text "${text}" on screen`);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
it("keeps a saved reply's leading reasoning in the live row", async () => {
|
|
311
|
+
const working = reduce([
|
|
312
|
+
event(1, { type: "run.started" }),
|
|
313
|
+
event(2, {
|
|
314
|
+
type: "message.delta",
|
|
315
|
+
messageId: "reply",
|
|
316
|
+
role: "assistant",
|
|
317
|
+
text: "Looking it up.",
|
|
318
|
+
}),
|
|
319
|
+
search(3, "first", "running"),
|
|
320
|
+
]);
|
|
321
|
+
const saved: AgentThreadState = {
|
|
322
|
+
...working,
|
|
323
|
+
messages: [
|
|
324
|
+
{
|
|
325
|
+
id: "reply",
|
|
326
|
+
role: "assistant",
|
|
327
|
+
status: "complete",
|
|
328
|
+
parts: [
|
|
329
|
+
{ type: "reasoning", text: "Weighing the two bikes." },
|
|
330
|
+
{ type: "text", text: "Looking it up." },
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
],
|
|
334
|
+
};
|
|
335
|
+
await render(observableController(snapshotOf(saved, 0)).controller);
|
|
336
|
+
await flush();
|
|
337
|
+
|
|
338
|
+
expect(rows()).toHaveLength(1);
|
|
339
|
+
expect(rows()[0]!.dataset.running).toBe("true");
|
|
340
|
+
expect(rows()[0]!.textContent).toContain("Weighing the two bikes.");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("shows a live row for a step whose only output is a result card", async () => {
|
|
344
|
+
function SearchResult({ value }: { value: { id: string } }) {
|
|
345
|
+
return <div data-search-result={value.id}>Results</div>;
|
|
346
|
+
}
|
|
347
|
+
const carded = reduce(narrated.slice(0, 5));
|
|
348
|
+
container = document.createElement("div");
|
|
349
|
+
document.body.appendChild(container);
|
|
350
|
+
root = createRoot(container);
|
|
351
|
+
await act(async () => {
|
|
352
|
+
root.render(
|
|
353
|
+
<AgentKitRoot
|
|
354
|
+
controller={observableController(snapshotOf(carded, 0)).controller}
|
|
355
|
+
threadId={threadId}
|
|
356
|
+
load="manual"
|
|
357
|
+
registry={{ tools: { "web-search": SearchResult } }}
|
|
358
|
+
>
|
|
359
|
+
<AgentKitChat composer={false} />
|
|
360
|
+
</AgentKitRoot>,
|
|
361
|
+
);
|
|
362
|
+
await Promise.resolve();
|
|
363
|
+
});
|
|
364
|
+
await flush();
|
|
365
|
+
|
|
366
|
+
const reply = container.querySelector('[data-message-id="reply"]')!;
|
|
367
|
+
expect(
|
|
368
|
+
follows(reply, container.querySelector('[data-search-result="first"]')!),
|
|
369
|
+
).toBe(true);
|
|
370
|
+
expect(rows()).toHaveLength(1);
|
|
371
|
+
expect(rows()[0]!.dataset.running).toBe("true");
|
|
372
|
+
expect(follows(rows()[0]!, textOf("Looking"))).toBe(true);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("keeps a row opened to watch the steps open across narration", async () => {
|
|
376
|
+
const searching = reduce(narrated.slice(0, 5));
|
|
377
|
+
const observable = observableController(snapshotOf(searching, 0));
|
|
378
|
+
await render(observable.controller);
|
|
379
|
+
await flush();
|
|
380
|
+
const visible = () => rows().filter((row) => !row.hidden);
|
|
381
|
+
expect(visible()).toHaveLength(1);
|
|
382
|
+
await act(async () =>
|
|
383
|
+
visible()[0]!
|
|
384
|
+
.querySelector<HTMLButtonElement>(".agentkit-disclosure-summary")!
|
|
385
|
+
.click(),
|
|
386
|
+
);
|
|
387
|
+
expect(visible()[0]!.dataset.open).toBe("true");
|
|
388
|
+
|
|
389
|
+
const narrating = reduce(narrated.slice(5, 6), searching);
|
|
390
|
+
await act(async () => observable.update(snapshotOf(narrating, 1)));
|
|
391
|
+
await flush();
|
|
392
|
+
expect(visible()).toHaveLength(0);
|
|
393
|
+
|
|
394
|
+
await act(async () =>
|
|
395
|
+
observable.update(snapshotOf(reduce(narrated.slice(6), narrating), 2)),
|
|
396
|
+
);
|
|
397
|
+
await flush();
|
|
398
|
+
expect(visible()).toHaveLength(1);
|
|
399
|
+
expect(visible()[0]!.dataset.open).toBe("true");
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
it("keeps a row opened to watch the steps open when the run finishes", async () => {
|
|
344
403
|
const live = reduce(narrated);
|
|
345
404
|
const observable = observableController(snapshotOf(live, 0));
|
|
346
405
|
await render(observable.controller);
|
|
347
406
|
await flush();
|
|
407
|
+
await act(async () =>
|
|
408
|
+
rows()[0]!
|
|
409
|
+
.querySelector<HTMLButtonElement>(".agentkit-disclosure-summary")!
|
|
410
|
+
.click(),
|
|
411
|
+
);
|
|
412
|
+
expect(rows()[0]!.dataset.open).toBe("true");
|
|
413
|
+
|
|
414
|
+
await act(async () =>
|
|
415
|
+
observable.update(snapshotOf(reduce(answered, live), 1)),
|
|
416
|
+
);
|
|
417
|
+
await flush();
|
|
348
418
|
|
|
349
419
|
expect(rows()).toHaveLength(1);
|
|
350
|
-
expect(rows()[0]!.
|
|
420
|
+
expect(rows()[0]!.closest(".agentkit-message-actions")).not.toBeNull();
|
|
421
|
+
expect(rows()[0]!.textContent).toContain("Worked for");
|
|
422
|
+
expect(rows()[0]!.dataset.open).toBe("true");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it("shows no row while narration after a step streams", async () => {
|
|
426
|
+
const observable = observableController(
|
|
427
|
+
snapshotOf(reduce(narrated.slice(0, 6)), 0),
|
|
428
|
+
);
|
|
429
|
+
await render(observable.controller);
|
|
430
|
+
await flush();
|
|
431
|
+
|
|
432
|
+
expect(rows()).toHaveLength(0);
|
|
433
|
+
expect(textOf("Checking")).toBeTruthy();
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it("keeps one live row below the reply's latest text while the run works", async () => {
|
|
437
|
+
const live = reduce(narrated);
|
|
438
|
+
const observable = observableController(snapshotOf(live, 0));
|
|
439
|
+
await render(observable.controller);
|
|
440
|
+
await flush();
|
|
441
|
+
|
|
442
|
+
expect(rows()).toHaveLength(1);
|
|
443
|
+
const row = rows()[0]!;
|
|
444
|
+
expect(row.dataset.running).toBe("true");
|
|
351
445
|
expect(container.textContent).not.toContain("Worked for");
|
|
446
|
+
expect(follows(row, textOf("Checking"))).toBe(true);
|
|
352
447
|
const reply = container.querySelector('[data-message-id="reply"]')!;
|
|
353
|
-
expect(
|
|
354
|
-
rows()[0]!.compareDocumentPosition(reply) &
|
|
355
|
-
Node.DOCUMENT_POSITION_FOLLOWING,
|
|
356
|
-
).toBeTruthy();
|
|
448
|
+
expect(follows(row, reply)).toBe(true);
|
|
357
449
|
|
|
358
450
|
await act(async () =>
|
|
359
451
|
observable.update(snapshotOf(reduce(answered, live), 1)),
|
|
@@ -362,6 +454,47 @@ describe("a reply that narrates between steps", () => {
|
|
|
362
454
|
|
|
363
455
|
expect(rows()).toHaveLength(1);
|
|
364
456
|
expect(rows()[0]!.closest(".agentkit-message-actions")).not.toBeNull();
|
|
457
|
+
expect(follows(rows()[0]!, textOf("Buy the Trek."))).toBe(true);
|
|
365
458
|
expect(container.textContent?.match(/Worked for/g)).toHaveLength(1);
|
|
366
459
|
});
|
|
460
|
+
|
|
461
|
+
it("keeps a step's result card above the reply through the finish", async () => {
|
|
462
|
+
const mounts = vi.fn();
|
|
463
|
+
function SearchResult({ value }: { value: { id: string } }) {
|
|
464
|
+
useEffect(() => mounts(value.id), [value.id]);
|
|
465
|
+
return <div data-search-result={value.id}>Results</div>;
|
|
466
|
+
}
|
|
467
|
+
const live = reduce(narrated);
|
|
468
|
+
const observable = observableController(snapshotOf(live, 0));
|
|
469
|
+
container = document.createElement("div");
|
|
470
|
+
document.body.appendChild(container);
|
|
471
|
+
root = createRoot(container);
|
|
472
|
+
await act(async () => {
|
|
473
|
+
root.render(
|
|
474
|
+
<AgentKitRoot
|
|
475
|
+
controller={observable.controller}
|
|
476
|
+
threadId={threadId}
|
|
477
|
+
load="manual"
|
|
478
|
+
registry={{ tools: { "web-search": SearchResult } }}
|
|
479
|
+
>
|
|
480
|
+
<AgentKitChat composer={false} />
|
|
481
|
+
</AgentKitRoot>,
|
|
482
|
+
);
|
|
483
|
+
await Promise.resolve();
|
|
484
|
+
});
|
|
485
|
+
await flush();
|
|
486
|
+
|
|
487
|
+
const reply = container.querySelector('[data-message-id="reply"]')!;
|
|
488
|
+
const card = () => container.querySelector('[data-search-result="first"]')!;
|
|
489
|
+
expect(follows(reply, card())).toBe(true);
|
|
490
|
+
expect(follows(rows()[0]!, reply)).toBe(true);
|
|
491
|
+
|
|
492
|
+
await act(async () =>
|
|
493
|
+
observable.update(snapshotOf(reduce(answered, live), 1)),
|
|
494
|
+
);
|
|
495
|
+
await flush();
|
|
496
|
+
|
|
497
|
+
expect(follows(reply, card())).toBe(true);
|
|
498
|
+
expect(mounts.mock.calls.filter(([id]) => id === "first")).toHaveLength(1);
|
|
499
|
+
});
|
|
367
500
|
});
|
|
@@ -74,19 +74,33 @@ describe("AuthPage interactions", () => {
|
|
|
74
74
|
trackingApp="design"
|
|
75
75
|
googleOnly
|
|
76
76
|
showGoogle
|
|
77
|
+
signupLegalNotice={{
|
|
78
|
+
termsUrl: "https://example.com/terms",
|
|
79
|
+
privacyUrl: "https://example.com/privacy",
|
|
80
|
+
}}
|
|
77
81
|
identitySsoEnabled={false}
|
|
78
82
|
/>,
|
|
79
83
|
);
|
|
80
84
|
});
|
|
81
85
|
expect(
|
|
82
86
|
container.textContent?.includes(
|
|
83
|
-
"
|
|
87
|
+
"You also create a Builder.io account and agree to Builder’s",
|
|
84
88
|
),
|
|
85
89
|
).toBe(variant === "automatic");
|
|
86
90
|
expect(
|
|
87
91
|
container.querySelector<HTMLButtonElement>("#google-btn")?.disabled,
|
|
88
92
|
).toBe(false);
|
|
93
|
+
const footer = container.querySelector(".legal-note");
|
|
89
94
|
if (variant === "automatic") {
|
|
95
|
+
expect(footer?.textContent).not.toContain(
|
|
96
|
+
"By signing up, you accept our",
|
|
97
|
+
);
|
|
98
|
+
expect(
|
|
99
|
+
container
|
|
100
|
+
.querySelector("#google-btn")
|
|
101
|
+
?.compareDocumentPosition(footer ?? container),
|
|
102
|
+
).toBe(Node.DOCUMENT_POSITION_FOLLOWING);
|
|
103
|
+
expect(footer?.querySelectorAll("a").length).toBe(2);
|
|
90
104
|
expect(
|
|
91
105
|
container.querySelector(
|
|
92
106
|
'a[href="https://www.builder.io/legal/terms"]',
|
|
@@ -97,6 +111,8 @@ describe("AuthPage interactions", () => {
|
|
|
97
111
|
'a[href="https://www.builder.io/legal/privacy"]',
|
|
98
112
|
),
|
|
99
113
|
).not.toBeNull();
|
|
114
|
+
} else {
|
|
115
|
+
expect(footer).toBeNull();
|
|
100
116
|
}
|
|
101
117
|
},
|
|
102
118
|
);
|
|
@@ -304,6 +304,7 @@ function trackAuth(
|
|
|
304
304
|
);
|
|
305
305
|
const legacyProperties = {
|
|
306
306
|
app,
|
|
307
|
+
hostname: window.location.hostname,
|
|
307
308
|
...(app === "design" ? builderSignupTelemetry : {}),
|
|
308
309
|
...properties,
|
|
309
310
|
};
|
|
@@ -2432,9 +2433,10 @@ export function AuthPage(props: AuthPageProps) {
|
|
|
2432
2433
|
</div>
|
|
2433
2434
|
) : null;
|
|
2434
2435
|
const builderLegalNote =
|
|
2436
|
+
view !== "login" &&
|
|
2435
2437
|
builderSignup?.status === "assigned" &&
|
|
2436
2438
|
builderSignup.variant === "automatic" ? (
|
|
2437
|
-
<
|
|
2439
|
+
<span>
|
|
2438
2440
|
{t("builderSignupDisclosure")}{" "}
|
|
2439
2441
|
<a
|
|
2440
2442
|
href="https://www.builder.io/legal/terms"
|
|
@@ -2452,7 +2454,7 @@ export function AuthPage(props: AuthPageProps) {
|
|
|
2452
2454
|
{t("legalPrivacy")}
|
|
2453
2455
|
</a>
|
|
2454
2456
|
{t("legalSuffix")}
|
|
2455
|
-
</
|
|
2457
|
+
</span>
|
|
2456
2458
|
) : null;
|
|
2457
2459
|
const legalNote = signupLegalNotice ? (
|
|
2458
2460
|
<p className="legal-note">
|
|
@@ -2481,7 +2483,10 @@ export function AuthPage(props: AuthPageProps) {
|
|
|
2481
2483
|
<span data-i18n="legalSuffix">
|
|
2482
2484
|
{signupLegalNotice.suffix ?? t("legalSuffix")}
|
|
2483
2485
|
</span>
|
|
2486
|
+
{builderLegalNote && <> {builderLegalNote}</>}
|
|
2484
2487
|
</p>
|
|
2488
|
+
) : builderLegalNote ? (
|
|
2489
|
+
<p className="legal-note">{builderLegalNote}</p>
|
|
2485
2490
|
) : null;
|
|
2486
2491
|
const signupForm = (
|
|
2487
2492
|
<AuthForm
|
|
@@ -2684,7 +2689,6 @@ export function AuthPage(props: AuthPageProps) {
|
|
|
2684
2689
|
{t("googleNotConfigured")}
|
|
2685
2690
|
</p>
|
|
2686
2691
|
) : null}
|
|
2687
|
-
{view !== "login" && builderLegalNote}
|
|
2688
2692
|
{showGoogle ? (
|
|
2689
2693
|
<div className="google-signin" id="google-signin">
|
|
2690
2694
|
<button
|
|
@@ -2847,7 +2851,11 @@ export function AuthPage(props: AuthPageProps) {
|
|
|
2847
2851
|
</button>
|
|
2848
2852
|
</div>
|
|
2849
2853
|
) : null}
|
|
2850
|
-
{!googleOnly ?
|
|
2854
|
+
{!googleOnly ? (
|
|
2855
|
+
signupForm
|
|
2856
|
+
) : builderLegalNote ? (
|
|
2857
|
+
<p className="legal-note">{builderLegalNote}</p>
|
|
2858
|
+
) : null}
|
|
2851
2859
|
<div
|
|
2852
2860
|
id="verification-step"
|
|
2853
2861
|
className={`form verification-step ${view === "verification" ? "active" : ""}`}
|
|
@@ -6851,7 +6851,7 @@ function AgentKitRunFailure({
|
|
|
6851
6851
|
)
|
|
6852
6852
|
}
|
|
6853
6853
|
/>
|
|
6854
|
-
{continueFailed ? (
|
|
6854
|
+
{continueFailed && !superseded ? (
|
|
6855
6855
|
<ComposerNotice
|
|
6856
6856
|
role="alert"
|
|
6857
6857
|
noticeId="continue-failed"
|
|
@@ -6912,8 +6912,12 @@ function AgentKitRunFailure({
|
|
|
6912
6912
|
: undefined
|
|
6913
6913
|
}
|
|
6914
6914
|
onContinue={continueStoppedRun}
|
|
6915
|
+
// A newer run carries the turn now, so a refused Continue is stale and
|
|
6916
|
+
// would contradict the Stop button that run shows.
|
|
6915
6917
|
continueError={
|
|
6916
|
-
continueFailed
|
|
6918
|
+
continueFailed && !superseded
|
|
6919
|
+
? t("agentChat.recovery.continueUnavailable")
|
|
6920
|
+
: null
|
|
6917
6921
|
}
|
|
6918
6922
|
onRetry={() => void retryFailedTurn()}
|
|
6919
6923
|
onRetryWithoutAttachments={retryWithoutAttachments}
|
|
@@ -522,6 +522,36 @@ describe("IntegrationDetailPage", () => {
|
|
|
522
522
|
expect(row?.querySelector("button")?.textContent).toBe("Remove");
|
|
523
523
|
});
|
|
524
524
|
|
|
525
|
+
it("opens a custom connected server by its slug, with Reconnect and Remove", async () => {
|
|
526
|
+
// The tile links a custom integration (one added by URL, with no catalog entry) to a
|
|
527
|
+
// slug of its name; this is that slug resolving back to the server, so the page has
|
|
528
|
+
// the facts and the actions a built-in's page has.
|
|
529
|
+
servers([
|
|
530
|
+
{
|
|
531
|
+
id: "custom-1",
|
|
532
|
+
scope: "user",
|
|
533
|
+
name: "Internal Docs",
|
|
534
|
+
url: "https://mcp.example.com/mcp",
|
|
535
|
+
authMode: "headers",
|
|
536
|
+
createdAt: 1,
|
|
537
|
+
mergedId: "user:custom-1",
|
|
538
|
+
status: { state: "error", error: "the server refused the token" },
|
|
539
|
+
},
|
|
540
|
+
]);
|
|
541
|
+
await render("internal-docs");
|
|
542
|
+
const detail = container.querySelector("[data-connected-server-detail]");
|
|
543
|
+
expect(detail).not.toBeNull();
|
|
544
|
+
expect(detail?.textContent).toContain("Internal Docs");
|
|
545
|
+
expect(detail?.textContent).toContain("https://mcp.example.com/mcp");
|
|
546
|
+
expect(detail?.textContent).toContain("Connection error");
|
|
547
|
+
expect(detail?.textContent).toContain("the server refused the token");
|
|
548
|
+
const labels = Array.from(detail?.querySelectorAll("button") ?? []).map(
|
|
549
|
+
(button) => button.textContent?.trim(),
|
|
550
|
+
);
|
|
551
|
+
expect(labels).toContain("Reconnect");
|
|
552
|
+
expect(labels).toContain("Remove");
|
|
553
|
+
});
|
|
554
|
+
|
|
525
555
|
it("marks a beta integration", async () => {
|
|
526
556
|
await render("grafana");
|
|
527
557
|
expect(rowText("status")).toContain("Beta");
|