@copilotkit/vue 1.75.1 → 1.76.0
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/index.cjs +1 -1
- package/dist/index.mjs +272 -157
- package/dist/index.mjs.map +1 -1
- package/dist/use-render-activity-message-BTKmd3f3.cjs +110 -0
- package/dist/use-render-activity-message-BTKmd3f3.cjs.map +1 -0
- package/dist/use-render-activity-message-Eu6R2vGT.js +12801 -0
- package/dist/use-render-activity-message-Eu6R2vGT.js.map +1 -0
- package/dist/v2/components/chat/CopilotChat.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts +8 -152
- package/dist/v2/components/chat/CopilotChatAssistantMessage.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/CopilotChatMessageView.vue.d.ts.map +1 -1
- package/dist/v2/components/chat/copilotChatToolCallItemRender.d.ts.map +1 -1
- package/dist/v2/hooks/use-agent.d.ts.map +1 -1
- package/dist/v2/index.cjs +1 -1
- package/dist/v2/index.d.cts +2 -0
- package/dist/v2/index.d.mts +2 -0
- package/dist/v2/index.d.ts +2 -0
- package/dist/v2/index.d.ts.map +1 -1
- package/dist/v2/index.mjs +187 -72
- package/dist/v2/index.mjs.map +1 -1
- package/package.json +7 -7
- package/src/v2/__tests__/utils/test-helpers.ts +26 -6
- package/src/v2/components/chat/CopilotChat.vue +100 -20
- package/src/v2/components/chat/CopilotChatMessageView.vue +41 -30
- package/src/v2/components/chat/__tests__/CopilotChat.clearOnFresh.test.ts +130 -2
- package/src/v2/components/chat/__tests__/CopilotChat.e2e.test.ts +6 -1
- package/src/v2/components/chat/__tests__/CopilotChatMessageView.slotMetaCost.test.ts +187 -0
- package/src/v2/components/chat/__tests__/CopilotChatToolRendering.e2e.test.ts +22 -40
- package/src/v2/components/chat/__tests__/CopilotChatToolRerenders.e2e.test.ts +12 -24
- package/src/v2/components/chat/copilotChatToolCallItemRender.ts +3 -2
- package/src/v2/hooks/__tests__/use-agent-headers.test.ts +83 -0
- package/src/v2/hooks/__tests__/use-agent-stability.test.ts +2 -0
- package/src/v2/hooks/__tests__/use-agent.e2e.test.ts +5 -3
- package/src/v2/hooks/__tests__/use-frontend-tool.e2e.test.ts +2 -2
- package/src/v2/hooks/use-agent.ts +3 -3
- package/src/v2/index.ts +5 -0
- package/src/v2/providers/__tests__/CopilotKitProvider.renderCustomMessages.e2e.test.ts +6 -4
- package/dist/use-render-activity-message-BOPauH9X.cjs +0 -85
- package/dist/use-render-activity-message-BOPauH9X.cjs.map +0 -1
- package/dist/use-render-activity-message-xvpuaGyf.js +0 -9807
- package/dist/use-render-activity-message-xvpuaGyf.js.map +0 -1
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
RunAgentInput,
|
|
8
8
|
RunAgentParameters,
|
|
9
9
|
RunAgentResult,
|
|
10
|
+
RunStartedEvent,
|
|
11
|
+
RunFinishedEvent,
|
|
10
12
|
} from "@ag-ui/client";
|
|
11
13
|
import { Observable, Subject, from, delay } from "rxjs";
|
|
12
14
|
import { defineComponent, nextTick } from "vue";
|
|
@@ -485,12 +487,30 @@ export function renderWithCopilotKit({
|
|
|
485
487
|
return render(Host);
|
|
486
488
|
}
|
|
487
489
|
|
|
488
|
-
export function runStartedEvent(
|
|
489
|
-
|
|
490
|
+
export function runStartedEvent(
|
|
491
|
+
input: Pick<RunAgentInput, "threadId" | "runId"> = {
|
|
492
|
+
threadId: "test-thread",
|
|
493
|
+
runId: "test-run",
|
|
494
|
+
},
|
|
495
|
+
): RunStartedEvent {
|
|
496
|
+
return {
|
|
497
|
+
type: EventType.RUN_STARTED,
|
|
498
|
+
threadId: input.threadId,
|
|
499
|
+
runId: input.runId,
|
|
500
|
+
};
|
|
490
501
|
}
|
|
491
502
|
|
|
492
|
-
export function runFinishedEvent(
|
|
493
|
-
|
|
503
|
+
export function runFinishedEvent(
|
|
504
|
+
input: Pick<RunAgentInput, "threadId" | "runId"> = {
|
|
505
|
+
threadId: "test-thread",
|
|
506
|
+
runId: "test-run",
|
|
507
|
+
},
|
|
508
|
+
): RunFinishedEvent {
|
|
509
|
+
return {
|
|
510
|
+
type: EventType.RUN_FINISHED,
|
|
511
|
+
threadId: input.threadId,
|
|
512
|
+
runId: input.runId,
|
|
513
|
+
};
|
|
494
514
|
}
|
|
495
515
|
|
|
496
516
|
export function stateSnapshotEvent(snapshot: unknown): BaseEvent {
|
|
@@ -684,7 +704,7 @@ export class SuggestionsProviderAgent extends MockStepwiseAgent {
|
|
|
684
704
|
setTimeout(() => {
|
|
685
705
|
void (async () => {
|
|
686
706
|
const messageId = testId("suggest-msg");
|
|
687
|
-
await this.emit(
|
|
707
|
+
await this.emit(runStartedEvent(_input));
|
|
688
708
|
|
|
689
709
|
await emitSuggestionToolCall(this, {
|
|
690
710
|
toolCallId: testId("tc"),
|
|
@@ -692,7 +712,7 @@ export class SuggestionsProviderAgent extends MockStepwiseAgent {
|
|
|
692
712
|
suggestions: this._suggestions,
|
|
693
713
|
});
|
|
694
714
|
|
|
695
|
-
await this.emit(
|
|
715
|
+
await this.emit(runFinishedEvent(_input));
|
|
696
716
|
await this.complete();
|
|
697
717
|
})();
|
|
698
718
|
}, 0);
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
AGUIConnectNotImplementedError,
|
|
4
|
-
AbstractAgent,
|
|
5
|
-
HttpAgent,
|
|
6
|
-
} from "@ag-ui/client";
|
|
2
|
+
import { AGUIConnectNotImplementedError, AbstractAgent } from "@ag-ui/client";
|
|
7
3
|
import {
|
|
8
4
|
createAttachmentContent,
|
|
9
5
|
DEFAULT_AGENT_ID,
|
|
@@ -21,7 +17,7 @@ import {
|
|
|
21
17
|
useSlots,
|
|
22
18
|
watch,
|
|
23
19
|
} from "vue";
|
|
24
|
-
import { CopilotKitCoreErrorCode } from "@copilotkit/core";
|
|
20
|
+
import { CopilotKitCoreErrorCode, ɵisHttpAgent } from "@copilotkit/core";
|
|
25
21
|
import type { Suggestion } from "@copilotkit/core";
|
|
26
22
|
import CopilotChatConfigurationProvider from "../../providers/CopilotChatConfigurationProvider.vue";
|
|
27
23
|
import { useCopilotChatConfiguration } from "../../providers/useCopilotChatConfiguration";
|
|
@@ -111,9 +107,15 @@ type ActiveConnectCycle = {
|
|
|
111
107
|
inspectorRequestId: string | null;
|
|
112
108
|
abortController: AbortController;
|
|
113
109
|
detached: boolean;
|
|
110
|
+
clearDiscardedBaseline?: () => void;
|
|
114
111
|
};
|
|
115
112
|
|
|
116
113
|
const activeConnectCycle = shallowRef<ActiveConnectCycle | null>(null);
|
|
114
|
+
let lastConnectCycle: ActiveConnectCycle | null = null;
|
|
115
|
+
const detachPromises = new WeakMap<AbstractAgent, Promise<void>>();
|
|
116
|
+
// Agent clones can share cursors. Finish every old reset before connecting again.
|
|
117
|
+
let pendingTeardown = Promise.resolve();
|
|
118
|
+
const baselineThreadIds = new WeakMap<AbstractAgent, string>();
|
|
117
119
|
|
|
118
120
|
const resolvedAgentId = computed(
|
|
119
121
|
() => props.agentId ?? existingConfig.value?.agentId ?? DEFAULT_AGENT_ID,
|
|
@@ -379,6 +381,7 @@ watch(
|
|
|
379
381
|
// React does — makes the ordering unconditional instead of depending on
|
|
380
382
|
// watcher scheduling. Non-explicit threads skip /connect below, but the first
|
|
381
383
|
// runAgent still has to ship the thread the UI is rendering.
|
|
384
|
+
baselineThreadIds.set(currentAgent, currentAgent.threadId);
|
|
382
385
|
currentAgent.threadId = threadId;
|
|
383
386
|
|
|
384
387
|
// When the caller hasn't picked a specific thread, resolvedThreadId is
|
|
@@ -421,10 +424,6 @@ watch(
|
|
|
421
424
|
cycle = existingCycle;
|
|
422
425
|
} else {
|
|
423
426
|
const connectAbortController = new AbortController();
|
|
424
|
-
if (currentAgent instanceof HttpAgent) {
|
|
425
|
-
currentAgent.abortController = connectAbortController;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
427
|
cycle = {
|
|
429
428
|
core: core as object,
|
|
430
429
|
agent: currentAgent,
|
|
@@ -434,11 +433,18 @@ watch(
|
|
|
434
433
|
detached: false,
|
|
435
434
|
};
|
|
436
435
|
activeConnectCycle.value = cycle;
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
436
|
+
lastConnectCycle = cycle;
|
|
437
|
+
|
|
438
|
+
void pendingTeardown
|
|
439
|
+
.then(() => {
|
|
440
|
+
if (cycle.detached) return;
|
|
441
|
+
if (ɵisHttpAgent(currentAgent))
|
|
442
|
+
currentAgent.abortController = connectAbortController;
|
|
443
|
+
return core.connectAgent({ agent: currentAgent });
|
|
444
|
+
})
|
|
440
445
|
.catch((error: unknown) => {
|
|
441
446
|
if (cycle.detached) {
|
|
447
|
+
cycle.clearDiscardedBaseline?.();
|
|
442
448
|
return;
|
|
443
449
|
}
|
|
444
450
|
if (error instanceof AGUIConnectNotImplementedError) {
|
|
@@ -460,6 +466,7 @@ watch(
|
|
|
460
466
|
// can briefly render against an incompletely-laid-out message
|
|
461
467
|
// tree and visibly snap once the last text chunk lands.
|
|
462
468
|
if (cycle.detached) {
|
|
469
|
+
cycle.clearDiscardedBaseline?.();
|
|
463
470
|
return;
|
|
464
471
|
}
|
|
465
472
|
const raf =
|
|
@@ -490,7 +497,17 @@ watch(
|
|
|
490
497
|
|
|
491
498
|
activeCycle.detached = true;
|
|
492
499
|
activeCycle.abortController.abort();
|
|
493
|
-
|
|
500
|
+
const detach = activeCycle.agent
|
|
501
|
+
.detachActiveRun()
|
|
502
|
+
.catch((error: unknown) => {
|
|
503
|
+
console.error("CopilotChat: detachActiveRun failed", error);
|
|
504
|
+
});
|
|
505
|
+
detachPromises.set(activeCycle.agent, detach);
|
|
506
|
+
pendingTeardown = Promise.all([pendingTeardown, detach]).then(() => {});
|
|
507
|
+
void detach.then(() => {
|
|
508
|
+
if (detachPromises.get(activeCycle.agent) === detach)
|
|
509
|
+
detachPromises.delete(activeCycle.agent);
|
|
510
|
+
});
|
|
494
511
|
activeConnectCycle.value = null;
|
|
495
512
|
});
|
|
496
513
|
},
|
|
@@ -502,17 +519,80 @@ watch(
|
|
|
502
519
|
// replay their history via the /connect cycle above, and the very first
|
|
503
520
|
// resolution (mount) or an agent-store swap that keeps the same thread id
|
|
504
521
|
// must never clear — only a real fresh-thread transition does.
|
|
505
|
-
|
|
522
|
+
let lastFreshSelection:
|
|
523
|
+
| { threadId: string; agent: AbstractAgent | null }
|
|
524
|
+
| undefined;
|
|
506
525
|
watch(
|
|
507
526
|
[resolvedThreadId, hasExplicitThreadId, () => agent.value],
|
|
508
|
-
([threadId, isExplicit, currentAgent]) => {
|
|
509
|
-
const previous =
|
|
510
|
-
|
|
527
|
+
([threadId, isExplicit, currentAgent], _old, onCleanup) => {
|
|
528
|
+
const previous = lastFreshSelection;
|
|
529
|
+
const selection = { threadId, agent: currentAgent };
|
|
530
|
+
lastFreshSelection = selection;
|
|
511
531
|
if (!currentAgent) return;
|
|
512
532
|
if (isExplicit) return;
|
|
513
533
|
if (previous === undefined) return;
|
|
514
|
-
if (threadId === previous) return;
|
|
515
|
-
|
|
534
|
+
if (threadId === previous.threadId) return;
|
|
535
|
+
let active = true;
|
|
536
|
+
let resetPending = true;
|
|
537
|
+
onCleanup(() => {
|
|
538
|
+
active = false;
|
|
539
|
+
});
|
|
540
|
+
const discardedThreadId =
|
|
541
|
+
previous.agent === currentAgent
|
|
542
|
+
? previous.threadId
|
|
543
|
+
: baselineThreadIds.get(currentAgent);
|
|
544
|
+
const clearCursor = () => {
|
|
545
|
+
if (!discardedThreadId) return;
|
|
546
|
+
if (
|
|
547
|
+
"clearReplayCursor" in currentAgent &&
|
|
548
|
+
typeof currentAgent.clearReplayCursor === "function"
|
|
549
|
+
)
|
|
550
|
+
currentAgent.clearReplayCursor(discardedThreadId);
|
|
551
|
+
if (
|
|
552
|
+
"clearReconnectCursor" in currentAgent &&
|
|
553
|
+
typeof currentAgent.clearReconnectCursor === "function"
|
|
554
|
+
)
|
|
555
|
+
currentAgent.clearReconnectCursor(discardedThreadId);
|
|
556
|
+
};
|
|
557
|
+
const clearDiscardedBaseline = () => {
|
|
558
|
+
if (
|
|
559
|
+
!active ||
|
|
560
|
+
!resetPending ||
|
|
561
|
+
isUnmounting.value ||
|
|
562
|
+
lastFreshSelection !== selection ||
|
|
563
|
+
agent.value !== currentAgent
|
|
564
|
+
)
|
|
565
|
+
return;
|
|
566
|
+
currentAgent.setMessages([]);
|
|
567
|
+
currentAgent.setState({});
|
|
568
|
+
currentAgent.pendingInterrupts = [];
|
|
569
|
+
clearCursor();
|
|
570
|
+
};
|
|
571
|
+
if (
|
|
572
|
+
lastConnectCycle?.agent === currentAgent &&
|
|
573
|
+
lastConnectCycle.threadId === discardedThreadId
|
|
574
|
+
)
|
|
575
|
+
lastConnectCycle.clearDiscardedBaseline = clearDiscardedBaseline;
|
|
576
|
+
const detach =
|
|
577
|
+
detachPromises.get(currentAgent) ?? currentAgent.detachActiveRun();
|
|
578
|
+
detachPromises.set(currentAgent, detach);
|
|
579
|
+
const finishReset = () => {
|
|
580
|
+
if (!resetPending) return;
|
|
581
|
+
// The cursor belongs to the discarded view, even after a fast reopen.
|
|
582
|
+
clearCursor();
|
|
583
|
+
clearDiscardedBaseline();
|
|
584
|
+
resetPending = false;
|
|
585
|
+
};
|
|
586
|
+
clearDiscardedBaseline();
|
|
587
|
+
pendingTeardown = Promise.all([pendingTeardown, detach])
|
|
588
|
+
.then(finishReset)
|
|
589
|
+
.catch((error: unknown) => {
|
|
590
|
+
console.error("CopilotChat: detachActiveRun failed", error);
|
|
591
|
+
})
|
|
592
|
+
.finally(() => {
|
|
593
|
+
if (detachPromises.get(currentAgent) === detach)
|
|
594
|
+
detachPromises.delete(currentAgent);
|
|
595
|
+
});
|
|
516
596
|
},
|
|
517
597
|
{ immediate: true },
|
|
518
598
|
);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, onMounted, ref, useSlots, watch } from "vue";
|
|
3
|
-
import type { Component } from "vue";
|
|
3
|
+
import type { Component, FunctionalComponent, VNodeChild } from "vue";
|
|
4
4
|
import type {
|
|
5
5
|
ActivityMessage,
|
|
6
6
|
AssistantMessage,
|
|
@@ -262,9 +262,11 @@ function getMeta(
|
|
|
262
262
|
? Math.max(messageIdsInRun.indexOf(message.id), 0)
|
|
263
263
|
: 0;
|
|
264
264
|
const numberOfMessagesInRun = resolvedRunId ? messageIdsInRun.length : 1;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
265
|
+
|
|
266
|
+
// `getStateByRun` deep-clones the whole run state, so resolve it only when
|
|
267
|
+
// a consumer reads `stateSnapshot`, and at most once per meta object.
|
|
268
|
+
let stateSnapshot: unknown;
|
|
269
|
+
let stateSnapshotResolved = false;
|
|
268
270
|
|
|
269
271
|
return {
|
|
270
272
|
runId,
|
|
@@ -272,10 +274,37 @@ function getMeta(
|
|
|
272
274
|
messageIndexInRun,
|
|
273
275
|
numberOfMessagesInRun,
|
|
274
276
|
agentId,
|
|
275
|
-
stateSnapshot
|
|
277
|
+
get stateSnapshot() {
|
|
278
|
+
if (!stateSnapshotResolved) {
|
|
279
|
+
stateSnapshotResolved = true;
|
|
280
|
+
stateSnapshot = resolvedRunId
|
|
281
|
+
? core.getStateByRun(agentId, threadId, resolvedRunId)
|
|
282
|
+
: undefined;
|
|
283
|
+
}
|
|
284
|
+
return stateSnapshot;
|
|
285
|
+
},
|
|
276
286
|
};
|
|
277
287
|
}
|
|
278
288
|
|
|
289
|
+
// `Object.assign` onto the meta object keeps the lazy `stateSnapshot` getter.
|
|
290
|
+
// A spread would call it.
|
|
291
|
+
function getMessageSlotProps(
|
|
292
|
+
message: Message,
|
|
293
|
+
position: MessageMetaProps["position"],
|
|
294
|
+
): MessageMetaProps {
|
|
295
|
+
return Object.assign(getMeta(message), { message, position });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Calls a `#message-before` / `#message-after` slot with the meta object
|
|
299
|
+
// itself. A template `<slot v-bind>` inside `v-for` goes through
|
|
300
|
+
// `mergeProps`, which reads every key and so would clone the run state for
|
|
301
|
+
// slots that never read `stateSnapshot`.
|
|
302
|
+
const MessageMetaSlot: FunctionalComponent<{
|
|
303
|
+
render: (props: MessageMetaProps) => unknown;
|
|
304
|
+
slotProps: MessageMetaProps;
|
|
305
|
+
}> = ({ render, slotProps }) => render(slotProps) as VNodeChild;
|
|
306
|
+
MessageMetaSlot.props = ["render", "slotProps"];
|
|
307
|
+
|
|
279
308
|
function getActivitySlotName(activityType: string): `activity-${string}` {
|
|
280
309
|
return `activity-${activityType}`;
|
|
281
310
|
}
|
|
@@ -310,11 +339,7 @@ function resolveCustomMessageRenderer(
|
|
|
310
339
|
|
|
311
340
|
return {
|
|
312
341
|
renderer: selected.render as ResolvedCustomMessageRenderer["renderer"],
|
|
313
|
-
props:
|
|
314
|
-
...getMeta(message),
|
|
315
|
-
message,
|
|
316
|
-
position,
|
|
317
|
-
},
|
|
342
|
+
props: getMessageSlotProps(message, position),
|
|
318
343
|
};
|
|
319
344
|
}
|
|
320
345
|
|
|
@@ -370,17 +395,10 @@ function resolveToolMessage(
|
|
|
370
395
|
v-for="message in deduplicatedMessages"
|
|
371
396
|
:key="rowRenderKeys.get(message.id) ?? message.id"
|
|
372
397
|
>
|
|
373
|
-
<
|
|
398
|
+
<MessageMetaSlot
|
|
374
399
|
v-if="componentSlots['message-before']"
|
|
375
|
-
|
|
376
|
-
:
|
|
377
|
-
position="before"
|
|
378
|
-
:run-id="getMeta(message).runId"
|
|
379
|
-
:message-index="getMeta(message).messageIndex"
|
|
380
|
-
:message-index-in-run="getMeta(message).messageIndexInRun"
|
|
381
|
-
:number-of-messages-in-run="getMeta(message).numberOfMessagesInRun"
|
|
382
|
-
:agent-id="getMeta(message).agentId"
|
|
383
|
-
:state-snapshot="getMeta(message).stateSnapshot"
|
|
400
|
+
:render="componentSlots['message-before']"
|
|
401
|
+
:slot-props="getMessageSlotProps(message, 'before')"
|
|
384
402
|
/>
|
|
385
403
|
<component
|
|
386
404
|
v-else-if="resolveCustomMessageRenderer(message, 'before')"
|
|
@@ -463,17 +481,10 @@ function resolveToolMessage(
|
|
|
463
481
|
</slot>
|
|
464
482
|
</slot>
|
|
465
483
|
|
|
466
|
-
<
|
|
484
|
+
<MessageMetaSlot
|
|
467
485
|
v-if="componentSlots['message-after']"
|
|
468
|
-
|
|
469
|
-
:
|
|
470
|
-
position="after"
|
|
471
|
-
:run-id="getMeta(message).runId"
|
|
472
|
-
:message-index="getMeta(message).messageIndex"
|
|
473
|
-
:message-index-in-run="getMeta(message).messageIndexInRun"
|
|
474
|
-
:number-of-messages-in-run="getMeta(message).numberOfMessagesInRun"
|
|
475
|
-
:agent-id="getMeta(message).agentId"
|
|
476
|
-
:state-snapshot="getMeta(message).stateSnapshot"
|
|
486
|
+
:render="componentSlots['message-after']"
|
|
487
|
+
:slot-props="getMessageSlotProps(message, 'after')"
|
|
477
488
|
/>
|
|
478
489
|
<component
|
|
479
490
|
v-else-if="resolveCustomMessageRenderer(message, 'after')"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { defineComponent, h, nextTick } from "vue";
|
|
2
|
+
import { defineComponent, h, nextTick, ref } from "vue";
|
|
3
3
|
import { flushPromises, mount } from "@vue/test-utils";
|
|
4
4
|
import CopilotKitProvider from "../../../providers/CopilotKitProvider.vue";
|
|
5
5
|
import CopilotChatConfigurationProvider from "../../../providers/CopilotChatConfigurationProvider.vue";
|
|
@@ -14,6 +14,134 @@ import CopilotChat from "../CopilotChat.vue";
|
|
|
14
14
|
// configuration transitions to a fresh, non-explicit thread via
|
|
15
15
|
// startNewThread()
|
|
16
16
|
describe("CopilotChat clear-on-fresh", () => {
|
|
17
|
+
it.each(["clearReplayCursor", "clearReconnectCursor"] as const)(
|
|
18
|
+
"clears state and the captured %s cursor on a fresh selection",
|
|
19
|
+
async (method) => {
|
|
20
|
+
const agent = new StateCapturingAgent();
|
|
21
|
+
const cursors = new Map<string, string>();
|
|
22
|
+
Object.defineProperty(agent, method, {
|
|
23
|
+
value: (threadId: string) => cursors.delete(threadId),
|
|
24
|
+
});
|
|
25
|
+
const threadId = ref("saved");
|
|
26
|
+
const explicit = ref(true);
|
|
27
|
+
const wrapper = mount(CopilotKitProvider, {
|
|
28
|
+
props: { agents__unsafe_dev_only: { default: agent } },
|
|
29
|
+
slots: {
|
|
30
|
+
default: () =>
|
|
31
|
+
h(
|
|
32
|
+
CopilotChatConfigurationProvider,
|
|
33
|
+
{ threadId: threadId.value, hasExplicitThreadId: explicit.value },
|
|
34
|
+
{ default: () => h(CopilotChat, { welcomeScreen: false }) },
|
|
35
|
+
),
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
await flushPromises();
|
|
39
|
+
agent.setState({ saved: true });
|
|
40
|
+
agent.pendingInterrupts = [{ id: "approval-A", reason: "confirmation" }];
|
|
41
|
+
cursors.set("saved", "last-event");
|
|
42
|
+
cursors.set("other", "keep-other");
|
|
43
|
+
threadId.value = "fresh";
|
|
44
|
+
explicit.value = false;
|
|
45
|
+
await flushPromises();
|
|
46
|
+
expect(agent.state).toEqual({});
|
|
47
|
+
expect(agent.pendingInterrupts).toEqual([]);
|
|
48
|
+
expect(cursors.get("saved")).toBeUndefined();
|
|
49
|
+
expect(cursors.get("other")).toBe("keep-other");
|
|
50
|
+
wrapper.unmount();
|
|
51
|
+
},
|
|
52
|
+
);
|
|
53
|
+
it.each(["fresh", "saved", "other", "via-fresh", "swap"])(
|
|
54
|
+
"protects %s from an abandoned connect that completes later",
|
|
55
|
+
async (destination) => {
|
|
56
|
+
let releaseDetach = () => {};
|
|
57
|
+
let releaseConnect = () => {};
|
|
58
|
+
let pendingDetach: Promise<void> | undefined;
|
|
59
|
+
const oldConnect = new Promise<void>((resolve) => {
|
|
60
|
+
releaseConnect = resolve;
|
|
61
|
+
});
|
|
62
|
+
const cursors = new Map<string, string>();
|
|
63
|
+
const requests: (string | null)[] = [];
|
|
64
|
+
class ReplayAgent extends StateCapturingAgent {
|
|
65
|
+
clearReplayCursor(threadId: string) {
|
|
66
|
+
cursors.delete(threadId);
|
|
67
|
+
}
|
|
68
|
+
async detachActiveRun() {
|
|
69
|
+
await pendingDetach;
|
|
70
|
+
await super.detachActiveRun();
|
|
71
|
+
}
|
|
72
|
+
async connectAgent(
|
|
73
|
+
...args: Parameters<StateCapturingAgent["connectAgent"]>
|
|
74
|
+
) {
|
|
75
|
+
requests.push(cursors.get(this.threadId) ?? null);
|
|
76
|
+
const first = requests.length === 1;
|
|
77
|
+
const result = await super.connectAgent(...args);
|
|
78
|
+
if (first) await oldConnect;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const agent = new ReplayAgent();
|
|
83
|
+
let currentAgent = agent;
|
|
84
|
+
const threadId = ref("saved");
|
|
85
|
+
const explicit = ref(true);
|
|
86
|
+
const wrapper = mount(CopilotKitProvider, {
|
|
87
|
+
props: { agents__unsafe_dev_only: { default: agent } },
|
|
88
|
+
slots: {
|
|
89
|
+
default: () =>
|
|
90
|
+
h(
|
|
91
|
+
CopilotChatConfigurationProvider,
|
|
92
|
+
{ threadId: threadId.value, hasExplicitThreadId: explicit.value },
|
|
93
|
+
{ default: () => h(CopilotChat, { welcomeScreen: false }) },
|
|
94
|
+
),
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
await flushPromises();
|
|
98
|
+
expect(requests).toEqual([null]);
|
|
99
|
+
pendingDetach = new Promise<void>((resolve) => {
|
|
100
|
+
releaseDetach = resolve;
|
|
101
|
+
});
|
|
102
|
+
cursors.set("saved", "old-event");
|
|
103
|
+
threadId.value = "fresh";
|
|
104
|
+
explicit.value = false;
|
|
105
|
+
await nextTick();
|
|
106
|
+
if (destination === "via-fresh") {
|
|
107
|
+
threadId.value = "fresh-again";
|
|
108
|
+
await nextTick();
|
|
109
|
+
}
|
|
110
|
+
if (destination === "swap") {
|
|
111
|
+
currentAgent = new ReplayAgent();
|
|
112
|
+
await wrapper.setProps({
|
|
113
|
+
agents__unsafe_dev_only: { default: currentAgent },
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const target =
|
|
117
|
+
destination === "via-fresh" || destination === "swap"
|
|
118
|
+
? "saved"
|
|
119
|
+
: destination;
|
|
120
|
+
if (target !== "fresh") {
|
|
121
|
+
threadId.value = target;
|
|
122
|
+
explicit.value = true;
|
|
123
|
+
await nextTick();
|
|
124
|
+
}
|
|
125
|
+
expect(requests).toEqual([null]);
|
|
126
|
+
cursors.set("saved", "late-event");
|
|
127
|
+
releaseDetach();
|
|
128
|
+
await flushPromises();
|
|
129
|
+
expect(requests).toEqual(target === "fresh" ? [null] : [null, null]);
|
|
130
|
+
currentAgent.setState({ current: true });
|
|
131
|
+
currentAgent.pendingInterrupts = [
|
|
132
|
+
{ id: "approval-new", reason: "confirmation" },
|
|
133
|
+
];
|
|
134
|
+
cursors.set(target, "new-event");
|
|
135
|
+
releaseConnect();
|
|
136
|
+
await flushPromises();
|
|
137
|
+
expect(currentAgent.state).toEqual({ current: true });
|
|
138
|
+
expect(currentAgent.pendingInterrupts).toEqual([
|
|
139
|
+
{ id: "approval-new", reason: "confirmation" },
|
|
140
|
+
]);
|
|
141
|
+
expect(cursors.get(target)).toBe("new-event");
|
|
142
|
+
wrapper.unmount();
|
|
143
|
+
},
|
|
144
|
+
);
|
|
17
145
|
it("does not clear messages on initial mount", async () => {
|
|
18
146
|
const agent = new StateCapturingAgent();
|
|
19
147
|
// Spy before mount: attaching afterwards can never observe a mount-time
|
|
@@ -133,7 +261,7 @@ describe("CopilotChat clear-on-fresh", () => {
|
|
|
133
261
|
await nextTick();
|
|
134
262
|
expect(currentThreadId).toBe("elsewhere");
|
|
135
263
|
|
|
136
|
-
newAgent!.setMessages([{ id: "m1", role: "user", content: "hi" }
|
|
264
|
+
newAgent!.setMessages([{ id: "m1", role: "user", content: "hi" }]);
|
|
137
265
|
expect(newAgent!.messages.length).toBe(1);
|
|
138
266
|
|
|
139
267
|
// Switch back non-explicitly. Any `setMessages([])` observed here must come
|
|
@@ -3,6 +3,7 @@ import { computed, defineComponent, onMounted } from "vue";
|
|
|
3
3
|
import type { PropType } from "vue";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import { EventType } from "@ag-ui/client";
|
|
6
|
+
import type { RunErrorEvent } from "@ag-ui/client";
|
|
6
7
|
import { z } from "zod";
|
|
7
8
|
import { useConfigureSuggestions } from "../../../hooks/use-configure-suggestions";
|
|
8
9
|
import {
|
|
@@ -189,7 +190,11 @@ describe("CopilotChat E2E - Chat Basics and Streaming Patterns", () => {
|
|
|
189
190
|
);
|
|
190
191
|
});
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
const errorEvent: RunErrorEvent = {
|
|
194
|
+
type: EventType.RUN_ERROR,
|
|
195
|
+
message: "Test run failed",
|
|
196
|
+
};
|
|
197
|
+
await agent.emit(errorEvent);
|
|
193
198
|
await agent.complete();
|
|
194
199
|
|
|
195
200
|
await waitFor(() => {
|