@assistant-ui/react-ag-ui 0.1.4 → 0.1.6

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.
@@ -4,11 +4,13 @@ import { act, cleanup, render, waitFor } from "@testing-library/react";
4
4
  import { HttpAgent, type RunAgentInput } from "@ag-ui/client";
5
5
  import {
6
6
  AssistantRuntimeProvider,
7
+ MessageByIndexProvider,
8
+ useActionBarResume,
7
9
  useCloudThreadListAdapter,
8
10
  useRemoteThreadListRuntime,
9
11
  } from "@assistant-ui/core/react";
10
12
  import type { AssistantRuntime } from "@assistant-ui/core";
11
- import { useAui, type AssistantClient } from "@assistant-ui/store";
13
+ import { useAui, useAuiState, type AssistantClient } from "@assistant-ui/store";
12
14
  import { AssistantCloud } from "assistant-cloud";
13
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
14
16
  import { useAgUiRuntime } from "./useAgUiRuntime";
@@ -107,6 +109,13 @@ const createServer = (
107
109
  if (row) row.content = body.content;
108
110
  return json({});
109
111
  }
112
+ if (method === "DELETE" && path === "/threads/remote-1/messages") {
113
+ for (const id of body.message_ids as string[]) {
114
+ const index = rows.findIndex((r) => r.id === id);
115
+ if (index !== -1) rows.splice(index, 1);
116
+ }
117
+ return json({});
118
+ }
110
119
  if (/^\/threads\/remote-1\/runs\/[^/]+\/heartbeat$/.test(path))
111
120
  return json({ heartbeat_interval_ms: heartbeatIntervalMs });
112
121
  if (/^\/threads\/remote-1\/runs\/[^/]+\/end$/.test(path)) return json({});
@@ -192,6 +201,20 @@ const renderApp = async (staleAfter?: number) => {
192
201
  aui = useAui();
193
202
  return null;
194
203
  };
204
+ const resume: { current: ReturnType<typeof useActionBarResume> | undefined } =
205
+ { current: undefined };
206
+ const CaptureResume = () => {
207
+ resume.current = useActionBarResume();
208
+ return null;
209
+ };
210
+ const ReplyResume = () => {
211
+ const hasReply = useAuiState((s) => s.thread.messages.length >= 2);
212
+ return hasReply ? (
213
+ <MessageByIndexProvider index={1}>
214
+ <CaptureResume />
215
+ </MessageByIndexProvider>
216
+ ) : null;
217
+ };
195
218
  const App = () => {
196
219
  const adapter = useCloudThreadListAdapter({
197
220
  cloud,
@@ -206,6 +229,7 @@ const renderApp = async (staleAfter?: number) => {
206
229
  return (
207
230
  <AssistantRuntimeProvider runtime={runtime}>
208
231
  <Capture />
232
+ <ReplyResume />
209
233
  </AssistantRuntimeProvider>
210
234
  );
211
235
  };
@@ -213,19 +237,25 @@ const renderApp = async (staleAfter?: number) => {
213
237
  await act(async () => {
214
238
  await aui.threads.getLoadThreadsPromise();
215
239
  });
216
- return { agent, getAui: () => aui, getRuntime: () => runtime };
240
+ return {
241
+ agent,
242
+ getAui: () => aui,
243
+ getRuntime: () => runtime,
244
+ getResume: () => resume.current,
245
+ };
217
246
  };
218
247
 
219
248
  const assistantRow = (
220
249
  last_heartbeat_ms_ago: number,
221
250
  status: "running" | "ended" = "running",
251
+ content: readonly unknown[] = [{ type: "text", text: "half an answer" }],
222
252
  ): Row => ({
223
253
  id: "a1",
224
254
  parent_id: "u1",
225
255
  format: "aui/v0",
226
256
  content: {
227
257
  role: "assistant",
228
- content: [{ type: "text", text: "half an answer" }],
258
+ content: [...content],
229
259
  status: { type: "incomplete", reason: "cancelled" },
230
260
  },
231
261
  external_id: "a1",
@@ -304,7 +334,50 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
304
334
  expect(reply.id).toBe("agent-msg-1");
305
335
  });
306
336
 
307
- it("shows a reloaded run quiet for longer than staleAfter as interrupted; Resume posts a sibling with its own lease and runs the agent again", async () => {
337
+ it("shows a reloaded empty run quiet for longer than staleAfter as stale; Resume deletes it and runs the agent again from the user message with no sibling", async () => {
338
+ useServer([userRow, assistantRow(45_000, "running", [])]);
339
+ const { agent, getAui, getRuntime, getResume } = await renderApp();
340
+ await act(async () => {
341
+ await getRuntime().threads.switchToThread("remote-1");
342
+ });
343
+ await waitFor(() =>
344
+ expect(getAui().thread.message({ index: 1 }).getState().runState).toBe(
345
+ "stale",
346
+ ),
347
+ );
348
+ await waitFor(() => expect(getResume()?.disabled).toBe(false));
349
+
350
+ await act(async () => {
351
+ getResume()!.resume();
352
+ });
353
+ await waitFor(() => expect(agent.runs).toHaveLength(1));
354
+ await waitFor(() =>
355
+ expect(createsWith(server.calls, true)).toHaveLength(1),
356
+ );
357
+ expect(createsWith(server.calls, true)[0]!.body).toMatchObject({
358
+ parent_id: "u1",
359
+ open_run: true,
360
+ });
361
+ await act(async () => {
362
+ agent.runs[0]!.finish();
363
+ });
364
+ await waitFor(() =>
365
+ expect(
366
+ server.calls.some((c) => c.path.endsWith("/runs/lease_1/end")),
367
+ ).toBe(true),
368
+ );
369
+ const deletes = server.calls.filter((c) => c.method === "DELETE");
370
+ expect(deletes.map((c) => c.body)).toEqual([{ message_ids: ["a1"] }]);
371
+ expect(server.rows.map((r) => r.id)).toEqual(["u1", "msg_1"]);
372
+ const messages = getAui().thread.getState().messages;
373
+ expect(messages.map((m) => m.role)).toEqual(["user", "assistant"]);
374
+ expect(messages[1]!.id).not.toBe("a1");
375
+ expect(getAui().thread.message({ index: 1 }).getState().branchCount).toBe(
376
+ 1,
377
+ );
378
+ });
379
+
380
+ it("shows a reloaded run quiet for longer than staleAfter as interrupted; Regenerate posts a sibling with its own lease and runs the agent again", async () => {
308
381
  useServer([userRow, assistantRow(45_000)]);
309
382
  const { agent, getAui, getRuntime } = await renderApp();
310
383
  await act(async () => {
@@ -314,7 +387,12 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
314
387
  expect(getAui().thread.getState().messages).toHaveLength(2),
315
388
  );
316
389
  const stale = getAui().thread.message({ index: 1 });
317
- await waitFor(() => expect(stale.getState().runState).toBe("stale"));
390
+ await waitFor(() =>
391
+ expect(stale.getState().status).toMatchObject({
392
+ error: { code: "run_stale" },
393
+ }),
394
+ );
395
+ expect(stale.getState().runState).toBeUndefined();
318
396
 
319
397
  await act(async () => {
320
398
  stale.reload();
@@ -363,8 +441,8 @@ describe("useAgUiRuntime with Assistant Cloud run leases", () => {
363
441
  });
364
442
  await waitFor(() =>
365
443
  expect(
366
- short.getAui().thread.message({ index: 1 }).getState().runState,
367
- ).toBe("stale"),
444
+ short.getAui().thread.message({ index: 1 }).getState().status,
445
+ ).toMatchObject({ error: { code: "run_stale" } }),
368
446
  );
369
447
  });
370
448
 
@@ -345,6 +345,7 @@ export function useAgUiRuntime(
345
345
  queueController?.clear();
346
346
  return core.reload(parentId, config);
347
347
  },
348
+ onDelete: (messageId: string) => core.deleteMessage(messageId),
348
349
  onCancel: async () => {
349
350
  queueController?.notifyCancelled();
350
351
  core.cancel();