@assistant-ui/eve 0.0.12 → 0.0.14

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.
@@ -14,6 +14,7 @@ vi.mock("eve/react", async (importOriginal) => ({
14
14
 
15
15
  import type { EveMessageData } from "eve/react";
16
16
  import { useEveAgentRuntime } from "./useEveAgentRuntime";
17
+ import { eveExtras } from "./eveExtras";
17
18
 
18
19
  const stuckStreamingData: EveMessageData = {
19
20
  messages: [
@@ -34,6 +35,7 @@ const createAgent = (overrides: Record<string, unknown>) => ({
34
35
  session: undefined,
35
36
  status: "ready",
36
37
  send: vi.fn(),
38
+ respond: vi.fn(),
37
39
  stop: vi.fn(),
38
40
  reset: vi.fn(),
39
41
  ...overrides,
@@ -160,8 +162,7 @@ describe("useEveAgentRuntime status forwarding", () => {
160
162
  });
161
163
 
162
164
  await waitFor(() => {
163
- expect(agent.send).toHaveBeenCalledWith({
164
- message: "hello",
165
+ expect(agent.send).toHaveBeenCalledWith("hello", {
165
166
  clientContext: { page: "/pricing" },
166
167
  });
167
168
  });
@@ -183,7 +184,7 @@ describe("useEveAgentRuntime status forwarding", () => {
183
184
  });
184
185
 
185
186
  await waitFor(() => {
186
- expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
187
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
187
188
  });
188
189
  });
189
190
 
@@ -204,7 +205,7 @@ describe("useEveAgentRuntime status forwarding", () => {
204
205
  });
205
206
 
206
207
  await waitFor(() => {
207
- expect(agent.send).toHaveBeenCalledWith({ message: "hello" });
208
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
208
209
  });
209
210
  });
210
211
 
@@ -238,8 +239,7 @@ describe("useEveAgentRuntime status forwarding", () => {
238
239
  });
239
240
 
240
241
  await waitFor(() => {
241
- expect(agent.send).toHaveBeenCalledWith({
242
- message: "hello",
242
+ expect(agent.send).toHaveBeenCalledWith("hello", {
243
243
  clientContext: { page: "/reload" },
244
244
  });
245
245
  });
@@ -272,8 +272,7 @@ describe("useEveAgentRuntime status forwarding", () => {
272
272
  });
273
273
 
274
274
  await waitFor(() => {
275
- expect(agent.send).toHaveBeenCalledWith({
276
- message: "hello",
275
+ expect(agent.send).toHaveBeenCalledWith("hello", {
277
276
  clientContext: { page: "/staged" },
278
277
  });
279
278
  });
@@ -310,8 +309,7 @@ describe("useEveAgentRuntime status forwarding", () => {
310
309
  });
311
310
 
312
311
  await waitFor(() => {
313
- expect(agent.send).toHaveBeenCalledWith({
314
- message: "hello",
312
+ expect(agent.send).toHaveBeenCalledWith("hello", {
315
313
  clientContext: { page: "/staged" },
316
314
  });
317
315
  });
@@ -341,6 +339,51 @@ const settledData: EveMessageData = {
341
339
  ],
342
340
  };
343
341
 
342
+ const executingToolData: EveMessageData = {
343
+ messages: [
344
+ { id: "u1", role: "user", parts: [{ type: "text", text: "run it" }] },
345
+ {
346
+ id: "a1",
347
+ role: "assistant",
348
+ parts: [
349
+ {
350
+ type: "dynamic-tool",
351
+ state: "input-available",
352
+ toolCallId: "call_slow",
353
+ toolName: "slow_tool",
354
+ input: {},
355
+ },
356
+ ],
357
+ },
358
+ ],
359
+ };
360
+
361
+ const twoExecutingToolsData: EveMessageData = {
362
+ messages: [
363
+ { id: "u1", role: "user", parts: [{ type: "text", text: "run them" }] },
364
+ {
365
+ id: "a1",
366
+ role: "assistant",
367
+ parts: [
368
+ {
369
+ type: "dynamic-tool",
370
+ state: "input-available",
371
+ toolCallId: "call_slow_a",
372
+ toolName: "slow_tool",
373
+ input: {},
374
+ },
375
+ {
376
+ type: "dynamic-tool",
377
+ state: "input-available",
378
+ toolCallId: "call_slow_b",
379
+ toolName: "slow_tool",
380
+ input: {},
381
+ },
382
+ ],
383
+ },
384
+ ],
385
+ };
386
+
344
387
  const getText = (runtime: ReturnType<typeof useEveAgentRuntime>) =>
345
388
  runtime.thread
346
389
  .getState()
@@ -394,9 +437,7 @@ describe("useEveAgentRuntime staged messages", () => {
394
437
  content: [{ type: "text", text: "hello" }],
395
438
  });
396
439
  });
397
- expect(agent.send).toHaveBeenCalledWith({
398
- message: "hello",
399
- });
440
+ expect(agent.send).toHaveBeenCalledWith("hello", undefined);
400
441
 
401
442
  mockUseEveAgent.mockReturnValue(
402
443
  createAgent({
@@ -504,8 +545,8 @@ describe("useEveAgentRuntime staged messages", () => {
504
545
  });
505
546
 
506
547
  expect(send).toHaveBeenCalledTimes(3);
507
- expect(send).toHaveBeenNthCalledWith(2, { message: "first staged" });
508
- expect(send).toHaveBeenNthCalledWith(3, { message: "second staged" });
548
+ expect(send).toHaveBeenNthCalledWith(2, "first staged", undefined);
549
+ expect(send).toHaveBeenNthCalledWith(3, "second staged", undefined);
509
550
  await waitFor(() => {
510
551
  expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
511
552
  });
@@ -541,12 +582,10 @@ describe("useEveAgentRuntime staged messages", () => {
541
582
 
542
583
  // the earlier draft keeps the context it was staged with; only the
543
584
  // reloaded message takes the reload-time config
544
- expect(agent.send).toHaveBeenNthCalledWith(1, {
545
- message: "first staged",
585
+ expect(agent.send).toHaveBeenNthCalledWith(1, "first staged", {
546
586
  clientContext: { page: "/first" },
547
587
  });
548
- expect(agent.send).toHaveBeenNthCalledWith(2, {
549
- message: "second staged",
588
+ expect(agent.send).toHaveBeenNthCalledWith(2, "second staged", {
550
589
  clientContext: { page: "/reloaded" },
551
590
  });
552
591
  });
@@ -569,9 +608,7 @@ describe("useEveAgentRuntime staged messages", () => {
569
608
  });
570
609
 
571
610
  expect(agent.send).toHaveBeenCalledTimes(1);
572
- expect(agent.send).toHaveBeenCalledWith({
573
- message: "first staged",
574
- });
611
+ expect(agent.send).toHaveBeenCalledWith("first staged", undefined);
575
612
  await waitFor(() => {
576
613
  expect(getText(result.current)).toEqual([
577
614
  "earlier",
@@ -609,17 +646,13 @@ describe("useEveAgentRuntime staged messages", () => {
609
646
  });
610
647
 
611
648
  await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
612
- expect(send).toHaveBeenNthCalledWith(1, {
613
- message: "first staged",
614
- });
649
+ expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
615
650
 
616
651
  await act(async () => {
617
652
  resolveFirstSend();
618
653
  });
619
654
  await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
620
- expect(send).toHaveBeenNthCalledWith(2, {
621
- message: "second staged",
622
- });
655
+ expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
623
656
 
624
657
  await waitFor(() => {
625
658
  expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
@@ -681,7 +714,7 @@ describe("useEveAgentRuntime staged messages", () => {
681
714
  });
682
715
 
683
716
  expect(send).toHaveBeenCalledTimes(1);
684
- expect(send).toHaveBeenCalledWith({ message: "first staged" });
717
+ expect(send).toHaveBeenCalledWith("first staged", undefined);
685
718
  await waitFor(() => {
686
719
  expect(getText(result.current)).toEqual([
687
720
  "earlier",
@@ -756,6 +789,406 @@ describe("useEveAgentRuntime staged messages", () => {
756
789
  });
757
790
  });
758
791
 
792
+ describe("useEveAgentRuntime extras wiring", () => {
793
+ it("provides error, events, and session through the runtime extras", () => {
794
+ const error = new Error("boom");
795
+ const events = [{ type: "session.started" }];
796
+ const session = { sessionId: "s1" };
797
+ mockUseEveAgent.mockReturnValue(
798
+ createAgent({ status: "error", error, events, session }) as never,
799
+ );
800
+
801
+ const { result } = renderHook(() => useEveAgentRuntime());
802
+
803
+ expect(
804
+ eveExtras.tryGet(result.current.thread.getState().extras),
805
+ ).toMatchObject({ error, events, session });
806
+ });
807
+
808
+ it("discards staged inputs when reset is invoked", async () => {
809
+ const agent = createAgent({ data: settledData });
810
+ mockUseEveAgent.mockReturnValue(agent as never);
811
+ const { result } = renderHook(() => useEveAgentRuntime());
812
+
813
+ await stageMessage(result.current, "discarded draft");
814
+ const discardedId = result.current.thread.getState().messages[2]!.id;
815
+
816
+ act(() => {
817
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
818
+ });
819
+ await waitFor(() => {
820
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
821
+ });
822
+
823
+ // `onReload` is only wired while staged messages exist, so a fresh draft
824
+ // is needed to reach the staged-run lookup that reads `stagedInputsRef`.
825
+ await stageMessage(result.current, "fresh draft");
826
+
827
+ await expect(
828
+ Promise.resolve(
829
+ result.current.thread.startRun({
830
+ parentId: discardedId,
831
+ sourceId: null,
832
+ runConfig: {},
833
+ }),
834
+ ),
835
+ ).rejects.toThrow("Runtime does not support reloading messages.");
836
+ expect(agent.send).not.toHaveBeenCalled();
837
+ expect(agent.reset).toHaveBeenCalledTimes(1);
838
+ });
839
+
840
+ it("clears executing tool state when reset is invoked", async () => {
841
+ const agent = createAgent({ data: settledData });
842
+ mockUseEveAgent.mockReturnValue(agent as never);
843
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
844
+
845
+ act(() => {
846
+ result.current.registerModelContextProvider({
847
+ getModelContext: () => ({
848
+ tools: {
849
+ slow_tool: {
850
+ parameters: { type: "object", properties: {} },
851
+ execute: () => new Promise<never>(() => {}),
852
+ },
853
+ },
854
+ }),
855
+ });
856
+ });
857
+
858
+ // The tracker treats its first snapshot as historical, so the tool call
859
+ // has to arrive on a later one to actually execute.
860
+ mockUseEveAgent.mockReturnValue(
861
+ createAgent({ data: executingToolData, reset: agent.reset }) as never,
862
+ );
863
+ rerender();
864
+
865
+ await waitFor(() => {
866
+ expect(result.current.thread.getState().isRunning).toBe(true);
867
+ });
868
+
869
+ act(() => {
870
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
871
+ });
872
+
873
+ await waitFor(() => {
874
+ expect(result.current.thread.getState().isRunning).toBe(false);
875
+ });
876
+ expect(agent.reset).toHaveBeenCalledTimes(1);
877
+ });
878
+
879
+ it("drops a send queued behind an active turn when reset is invoked", async () => {
880
+ let releaseFirstSend: (() => void) | undefined;
881
+ const send = vi
882
+ .fn()
883
+ .mockImplementationOnce(
884
+ () =>
885
+ new Promise<void>((resolve) => {
886
+ releaseFirstSend = resolve;
887
+ }),
888
+ )
889
+ .mockResolvedValue(undefined);
890
+ const agent = createAgent({ data: settledData, send });
891
+ mockUseEveAgent.mockReturnValue(agent as never);
892
+ const { result } = renderHook(() => useEveAgentRuntime());
893
+
894
+ await stageMessage(result.current, "queued draft");
895
+ const queuedDraftId = result.current.thread.getState().messages[2]!.id;
896
+
897
+ act(() => {
898
+ result.current.thread.append({
899
+ role: "user",
900
+ content: [{ type: "text", text: "first" }],
901
+ });
902
+ });
903
+ await waitFor(() => {
904
+ expect(send).toHaveBeenCalledTimes(1);
905
+ });
906
+
907
+ const queuedReload = Promise.resolve(
908
+ result.current.thread.startRun({
909
+ parentId: queuedDraftId,
910
+ sourceId: null,
911
+ runConfig: {},
912
+ }),
913
+ );
914
+ // Let the reload reach `enqueueSend` and park behind the active turn.
915
+ await act(async () => {});
916
+
917
+ act(() => {
918
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
919
+ });
920
+
921
+ await act(async () => {
922
+ releaseFirstSend?.();
923
+ await queuedReload;
924
+ });
925
+
926
+ expect(send).toHaveBeenCalledTimes(1);
927
+ await waitFor(() => {
928
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
929
+ });
930
+ });
931
+
932
+ it("keeps a staged draft when a queued send is cancelled instead of reset", async () => {
933
+ let releaseFirstSend: (() => void) | undefined;
934
+ const send = vi
935
+ .fn()
936
+ .mockImplementationOnce(
937
+ () =>
938
+ new Promise<void>((resolve) => {
939
+ releaseFirstSend = resolve;
940
+ }),
941
+ )
942
+ .mockResolvedValue(undefined);
943
+ const agent = createAgent({ data: settledData, send });
944
+ mockUseEveAgent.mockReturnValue(agent as never);
945
+ const { result } = renderHook(() => useEveAgentRuntime());
946
+
947
+ await stageMessage(result.current, "queued draft");
948
+ const queuedDraftId = result.current.thread.getState().messages[2]!.id;
949
+
950
+ act(() => {
951
+ result.current.thread.append({
952
+ role: "user",
953
+ content: [{ type: "text", text: "first" }],
954
+ });
955
+ });
956
+ await waitFor(() => {
957
+ expect(send).toHaveBeenCalledTimes(1);
958
+ });
959
+
960
+ const queuedReload = Promise.resolve(
961
+ result.current.thread.startRun({
962
+ parentId: queuedDraftId,
963
+ sourceId: null,
964
+ runConfig: {},
965
+ }),
966
+ );
967
+ await act(async () => {});
968
+
969
+ act(() => {
970
+ result.current.thread.cancelRun();
971
+ });
972
+
973
+ await act(async () => {
974
+ releaseFirstSend?.();
975
+ await queuedReload;
976
+ });
977
+
978
+ expect(send).toHaveBeenCalledTimes(1);
979
+ expect(agent.stop).toHaveBeenCalledTimes(1);
980
+ await waitFor(() => {
981
+ expect(getText(result.current)).toEqual([
982
+ "earlier",
983
+ "earlier answer",
984
+ "queued draft",
985
+ ]);
986
+ });
987
+ });
988
+
989
+ it("aborts client tool executions when reset is invoked", async () => {
990
+ const agentReset = vi.fn(() => {
991
+ mockUseEveAgent.mockReturnValue(
992
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
993
+ );
994
+ });
995
+ const agent = createAgent({ data: settledData, reset: agentReset });
996
+ mockUseEveAgent.mockReturnValue(agent as never);
997
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
998
+
999
+ const abortReasons: string[] = [];
1000
+ act(() => {
1001
+ result.current.registerModelContextProvider({
1002
+ getModelContext: () => ({
1003
+ tools: {
1004
+ slow_tool: {
1005
+ parameters: { type: "object", properties: {} },
1006
+ execute: (
1007
+ _args: unknown,
1008
+ context: { toolCallId: string; abortSignal: AbortSignal },
1009
+ ) =>
1010
+ new Promise<never>(() => {
1011
+ context.abortSignal.addEventListener("abort", () => {
1012
+ abortReasons.push(context.toolCallId);
1013
+ });
1014
+ }),
1015
+ },
1016
+ },
1017
+ }),
1018
+ });
1019
+ });
1020
+
1021
+ mockUseEveAgent.mockReturnValue(
1022
+ createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
1023
+ );
1024
+ rerender();
1025
+
1026
+ await waitFor(() => {
1027
+ expect(result.current.thread.getState().isRunning).toBe(true);
1028
+ });
1029
+
1030
+ act(() => {
1031
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1032
+ });
1033
+ rerender();
1034
+
1035
+ await waitFor(() => {
1036
+ expect(abortReasons.sort()).toEqual(["call_slow_a", "call_slow_b"]);
1037
+ });
1038
+ expect(result.current.thread.getState().isRunning).toBe(false);
1039
+ });
1040
+
1041
+ it("rejects a pending human-input request when reset is invoked", async () => {
1042
+ const agentReset = vi.fn(() => {
1043
+ mockUseEveAgent.mockReturnValue(
1044
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
1045
+ );
1046
+ });
1047
+ const agent = createAgent({ data: settledData, reset: agentReset });
1048
+ mockUseEveAgent.mockReturnValue(agent as never);
1049
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1050
+
1051
+ const humanRejections: unknown[] = [];
1052
+ act(() => {
1053
+ result.current.registerModelContextProvider({
1054
+ getModelContext: () => ({
1055
+ tools: {
1056
+ slow_tool: {
1057
+ parameters: { type: "object", properties: {} },
1058
+ execute: async (
1059
+ _args: unknown,
1060
+ context: { human: (payload: unknown) => Promise<unknown> },
1061
+ ) => {
1062
+ try {
1063
+ return await context.human({ request: "approve" });
1064
+ } catch (error) {
1065
+ humanRejections.push(error);
1066
+ throw error;
1067
+ }
1068
+ },
1069
+ },
1070
+ },
1071
+ }),
1072
+ });
1073
+ });
1074
+
1075
+ mockUseEveAgent.mockReturnValue(
1076
+ createAgent({ data: twoExecutingToolsData, reset: agentReset }) as never,
1077
+ );
1078
+ rerender();
1079
+
1080
+ await waitFor(() => {
1081
+ expect(result.current.thread.getState().isRunning).toBe(true);
1082
+ });
1083
+
1084
+ act(() => {
1085
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1086
+ });
1087
+ rerender();
1088
+
1089
+ await waitFor(() => {
1090
+ expect(humanRejections).toHaveLength(2);
1091
+ });
1092
+ expect((humanRejections[0] as Error).message).toBe(
1093
+ "Tool execution aborted",
1094
+ );
1095
+ });
1096
+
1097
+ it("ignores tool statuses left over from a discarded session", async () => {
1098
+ const releases: Record<string, () => void> = {};
1099
+ const agentReset = vi.fn(() => {
1100
+ mockUseEveAgent.mockReturnValue(
1101
+ createAgent({ data: { messages: [] }, reset: agentReset }) as never,
1102
+ );
1103
+ });
1104
+ const agent = createAgent({ data: settledData, reset: agentReset });
1105
+ mockUseEveAgent.mockReturnValue(agent as never);
1106
+ const { result, rerender } = renderHook(() => useEveAgentRuntime());
1107
+
1108
+ act(() => {
1109
+ result.current.registerModelContextProvider({
1110
+ getModelContext: () => ({
1111
+ tools: {
1112
+ slow_tool: {
1113
+ parameters: { type: "object", properties: {} },
1114
+ execute: (_args: unknown, context: { toolCallId: string }) =>
1115
+ new Promise<never>((_resolve, reject) => {
1116
+ releases[context.toolCallId] = () =>
1117
+ reject(new Error("aborted"));
1118
+ }),
1119
+ },
1120
+ },
1121
+ }),
1122
+ });
1123
+ });
1124
+
1125
+ mockUseEveAgent.mockReturnValue(
1126
+ createAgent({
1127
+ data: twoExecutingToolsData,
1128
+ reset: agentReset,
1129
+ }) as never,
1130
+ );
1131
+ rerender();
1132
+
1133
+ await waitFor(() => {
1134
+ expect(result.current.thread.getState().isRunning).toBe(true);
1135
+ });
1136
+
1137
+ act(() => {
1138
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1139
+ });
1140
+ rerender();
1141
+
1142
+ await waitFor(() => {
1143
+ expect(result.current.thread.getState().isRunning).toBe(false);
1144
+ });
1145
+
1146
+ await act(async () => {
1147
+ releases["call_slow_a"]?.();
1148
+ await Promise.resolve();
1149
+ });
1150
+
1151
+ await waitFor(() => {
1152
+ expect(result.current.thread.getState().isRunning).toBe(false);
1153
+ });
1154
+ expect(agentReset).toHaveBeenCalledTimes(1);
1155
+ });
1156
+
1157
+ it("promotes every staged draft after reset when the discarded run failed", async () => {
1158
+ const send = vi.fn().mockResolvedValue(undefined);
1159
+ const agent = createAgent({ data: settledData, send });
1160
+ let capturedOptions: { onFinish?: (snapshot: unknown) => void } = {};
1161
+ mockUseEveAgent.mockImplementation((options) => {
1162
+ capturedOptions = options as typeof capturedOptions;
1163
+ return agent as never;
1164
+ });
1165
+ const { result } = renderHook(() => useEveAgentRuntime());
1166
+
1167
+ act(() => {
1168
+ capturedOptions.onFinish?.({ status: "error" });
1169
+ });
1170
+ act(() => {
1171
+ eveExtras.tryGet(result.current.thread.getState().extras)!.reset();
1172
+ });
1173
+
1174
+ await stageMessage(result.current, "first staged");
1175
+ await stageMessage(result.current, "second staged");
1176
+
1177
+ const secondStagedId = result.current.thread.getState().messages[3]!.id;
1178
+ await act(async () => {
1179
+ await result.current.thread.startRun({
1180
+ parentId: secondStagedId,
1181
+ sourceId: null,
1182
+ runConfig: {},
1183
+ });
1184
+ });
1185
+
1186
+ expect(send).toHaveBeenCalledTimes(2);
1187
+ expect(send).toHaveBeenNthCalledWith(1, "first staged", undefined);
1188
+ expect(send).toHaveBeenNthCalledWith(2, "second staged", undefined);
1189
+ });
1190
+ });
1191
+
759
1192
  const approvalData: EveMessageData = {
760
1193
  messages: [
761
1194
  { id: "u1", role: "user", parts: [{ type: "text", text: "send it" }] },
@@ -776,11 +1209,12 @@ const approvalData: EveMessageData = {
776
1209
  name: "send_email",
777
1210
  inputRequest: {
778
1211
  requestId: "req_1",
1212
+ kind: "tool-approval",
779
1213
  prompt: "Send the email?",
780
1214
  display: "confirmation",
781
1215
  options: [
782
1216
  { id: "approve", label: "Approve" },
783
- { id: "deny", label: "Deny" },
1217
+ { id: "cancel", label: "Cancel" },
784
1218
  ],
785
1219
  },
786
1220
  },
@@ -794,16 +1228,14 @@ const approvalData: EveMessageData = {
794
1228
  describe("useEveAgentRuntime concurrent sends", () => {
795
1229
  it("defers an approval clicked while a turn is in flight until the turn parks", async () => {
796
1230
  let resolveFirstSend!: () => void;
797
- const send = vi
798
- .fn()
799
- .mockImplementationOnce(
800
- () =>
801
- new Promise<void>((resolve) => {
802
- resolveFirstSend = resolve;
803
- }),
804
- )
805
- .mockResolvedValue(undefined);
806
- const agent = createAgent({ data: approvalData, send });
1231
+ const send = vi.fn().mockImplementationOnce(
1232
+ () =>
1233
+ new Promise<void>((resolve) => {
1234
+ resolveFirstSend = resolve;
1235
+ }),
1236
+ );
1237
+ const respond = vi.fn().mockResolvedValue(undefined);
1238
+ const agent = createAgent({ data: approvalData, send, respond });
807
1239
  mockUseEveAgent.mockReturnValue(agent as never);
808
1240
  const { result } = renderHook(() => useEveAgentRuntime());
809
1241
 
@@ -821,15 +1253,15 @@ describe("useEveAgentRuntime concurrent sends", () => {
821
1253
  .getMessagePartByToolCallId("call_1")
822
1254
  .respondToToolApproval({ optionId: "approve" });
823
1255
  });
824
- expect(send).toHaveBeenCalledTimes(1);
1256
+ expect(respond).not.toHaveBeenCalled();
825
1257
 
826
1258
  await act(async () => {
827
1259
  resolveFirstSend();
828
1260
  });
829
- await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
830
- expect(send).toHaveBeenNthCalledWith(2, {
831
- inputResponses: [{ requestId: "req_1", optionId: "approve" }],
832
- });
1261
+ await waitFor(() => expect(respond).toHaveBeenCalledTimes(1));
1262
+ expect(respond).toHaveBeenCalledWith([
1263
+ { requestId: "req_1", optionId: "approve" },
1264
+ ]);
833
1265
  });
834
1266
 
835
1267
  it("queues a send issued while a turn is in flight and preserves order", async () => {
@@ -867,8 +1299,8 @@ describe("useEveAgentRuntime concurrent sends", () => {
867
1299
  resolveFirstSend();
868
1300
  });
869
1301
  await waitFor(() => expect(send).toHaveBeenCalledTimes(2));
870
- expect(send).toHaveBeenNthCalledWith(1, { message: "first" });
871
- expect(send).toHaveBeenNthCalledWith(2, { message: "second" });
1302
+ expect(send).toHaveBeenNthCalledWith(1, "first", undefined);
1303
+ expect(send).toHaveBeenNthCalledWith(2, "second", undefined);
872
1304
  });
873
1305
 
874
1306
  it("drops a queued send when the run is cancelled before it dispatches", async () => {
@@ -911,6 +1343,141 @@ describe("useEveAgentRuntime concurrent sends", () => {
911
1343
  expect(send).toHaveBeenCalledTimes(1);
912
1344
  });
913
1345
 
1346
+ it("returns a cancelled queued send to the composer", async () => {
1347
+ let resolveFirstSend!: () => void;
1348
+ const send = vi
1349
+ .fn()
1350
+ .mockImplementationOnce(
1351
+ () =>
1352
+ new Promise<void>((resolve) => {
1353
+ resolveFirstSend = resolve;
1354
+ }),
1355
+ )
1356
+ .mockResolvedValue(undefined);
1357
+ const agent = createAgent({ data: settledData, send });
1358
+ mockUseEveAgent.mockReturnValue(agent as never);
1359
+ const { result } = renderHook(() => useEveAgentRuntime());
1360
+
1361
+ await act(async () => {
1362
+ result.current.thread.append({
1363
+ role: "user",
1364
+ content: [{ type: "text", text: "first" }],
1365
+ });
1366
+ });
1367
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1368
+
1369
+ await act(async () => {
1370
+ result.current.thread.composer.setText("queued");
1371
+ result.current.thread.composer.send();
1372
+ });
1373
+ expect(result.current.thread.composer.getState().text).toBe("");
1374
+
1375
+ act(() => {
1376
+ result.current.thread.cancelRun();
1377
+ });
1378
+ await act(async () => {
1379
+ resolveFirstSend();
1380
+ });
1381
+
1382
+ await waitFor(() => {
1383
+ expect(result.current.thread.composer.getState().text).toBe("queued");
1384
+ });
1385
+ expect(send).toHaveBeenCalledTimes(1);
1386
+ expect(getText(result.current)).toEqual(["earlier", "earlier answer"]);
1387
+ });
1388
+
1389
+ it("returns the queued send when cancel keeps the trailing message", async () => {
1390
+ let resolveFirstSend!: () => void;
1391
+ const send = vi
1392
+ .fn()
1393
+ .mockImplementationOnce(
1394
+ () =>
1395
+ new Promise<void>((resolve) => {
1396
+ resolveFirstSend = resolve;
1397
+ }),
1398
+ )
1399
+ .mockResolvedValue(undefined);
1400
+ // Before the turn streams, the dispatched message is the thread's trailing
1401
+ // user leaf. Eve owns that message and cannot remove it on cancel.
1402
+ const agent = createAgent({
1403
+ data: {
1404
+ messages: [
1405
+ ...settledData.messages,
1406
+ { id: "u2", role: "user", parts: [{ type: "text", text: "first" }] },
1407
+ ],
1408
+ } satisfies EveMessageData,
1409
+ status: "submitted",
1410
+ send,
1411
+ });
1412
+ mockUseEveAgent.mockReturnValue(agent as never);
1413
+ const { result } = renderHook(() => useEveAgentRuntime());
1414
+
1415
+ await act(async () => {
1416
+ result.current.thread.composer.setText("first");
1417
+ result.current.thread.composer.send();
1418
+ });
1419
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1420
+
1421
+ await act(async () => {
1422
+ result.current.thread.composer.setText("queued");
1423
+ result.current.thread.composer.send();
1424
+ });
1425
+ act(() => {
1426
+ result.current.thread.cancelRun();
1427
+ });
1428
+ expect(result.current.thread.composer.getState().text).toBe("");
1429
+
1430
+ await act(async () => {
1431
+ resolveFirstSend();
1432
+ });
1433
+
1434
+ await waitFor(() => {
1435
+ expect(result.current.thread.composer.getState().text).toBe("queued");
1436
+ });
1437
+ expect(send).toHaveBeenCalledTimes(1);
1438
+ });
1439
+
1440
+ it("keeps a cancelled tool approval discarded", async () => {
1441
+ let resolveFirstSend!: () => void;
1442
+ const send = vi.fn().mockImplementationOnce(
1443
+ () =>
1444
+ new Promise<void>((resolve) => {
1445
+ resolveFirstSend = resolve;
1446
+ }),
1447
+ );
1448
+ const respond = vi.fn().mockResolvedValue(undefined);
1449
+ const agent = createAgent({ data: approvalData, send, respond });
1450
+ mockUseEveAgent.mockReturnValue(agent as never);
1451
+ const { result } = renderHook(() => useEveAgentRuntime());
1452
+ const before = getText(result.current);
1453
+
1454
+ await act(async () => {
1455
+ result.current.thread.append({
1456
+ role: "user",
1457
+ content: [{ type: "text", text: "go" }],
1458
+ });
1459
+ });
1460
+ await waitFor(() => expect(send).toHaveBeenCalledTimes(1));
1461
+
1462
+ act(() => {
1463
+ result.current.thread
1464
+ .getMessageByIndex(1)
1465
+ .getMessagePartByToolCallId("call_1")
1466
+ .respondToToolApproval({ optionId: "approve" });
1467
+ });
1468
+ act(() => {
1469
+ result.current.thread.cancelRun();
1470
+ });
1471
+ await act(async () => {
1472
+ resolveFirstSend();
1473
+ });
1474
+
1475
+ expect(send).toHaveBeenCalledTimes(1);
1476
+ expect(respond).not.toHaveBeenCalled();
1477
+ expect(result.current.thread.composer.getState().text).toBe("");
1478
+ expect(getText(result.current)).toEqual(before);
1479
+ });
1480
+
914
1481
  it("drops a queued send when the hook unmounts before it dispatches", async () => {
915
1482
  let resolveFirstSend!: () => void;
916
1483
  const send = vi
@@ -1006,10 +1573,10 @@ describe("useEveAgentRuntime concurrent sends", () => {
1006
1573
  });
1007
1574
  await waitFor(() => expect(send).toHaveBeenCalledTimes(3));
1008
1575
 
1009
- expect(send.mock.calls.map(([payload]) => payload)).toEqual([
1010
- { message: "first staged" },
1011
- { message: "interleaved" },
1012
- { message: "second staged" },
1576
+ expect(send.mock.calls.map(([message]) => message)).toEqual([
1577
+ "first staged",
1578
+ "interleaved",
1579
+ "second staged",
1013
1580
  ]);
1014
1581
  });
1015
1582
  });