@assistant-ui/react-langchain 0.0.32 → 0.0.34

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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/dist/attachSubagentTranscripts.d.ts +13 -14
  3. package/dist/attachSubagentTranscripts.d.ts.map +1 -1
  4. package/dist/convertMessages.d.ts +6 -9
  5. package/dist/convertMessages.d.ts.map +1 -1
  6. package/dist/convertMessages.js +57 -15
  7. package/dist/convertMessages.js.map +1 -1
  8. package/dist/converter.d.ts +128 -128
  9. package/dist/converter.d.ts.map +1 -1
  10. package/dist/converter.js +2 -1
  11. package/dist/converter.js.map +1 -1
  12. package/dist/findForkCheckpointInHistory.d.ts +15 -16
  13. package/dist/findForkCheckpointInHistory.d.ts.map +1 -1
  14. package/dist/hooks.d.ts +5 -7
  15. package/dist/hooks.d.ts.map +1 -1
  16. package/dist/index.d.ts +7 -8
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/resolveForkCheckpoint.d.ts +4 -5
  19. package/dist/resolveForkCheckpoint.d.ts.map +1 -1
  20. package/dist/runtimeExtras.d.ts +1 -3
  21. package/dist/runtimeExtras.d.ts.map +1 -1
  22. package/dist/sdkIdentity.d.ts +1 -3
  23. package/dist/sdkIdentity.d.ts.map +1 -1
  24. package/dist/sdkIdentity.js +1 -1
  25. package/dist/streamingTiming.d.ts +2 -4
  26. package/dist/streamingTiming.d.ts.map +1 -1
  27. package/dist/subagentMessagesProjection.d.ts +17 -0
  28. package/dist/subagentMessagesProjection.d.ts.map +1 -0
  29. package/dist/subagentMessagesProjection.js +40 -0
  30. package/dist/subagentMessagesProjection.js.map +1 -0
  31. package/dist/types.d.ts +100 -103
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/uiMessages.d.ts +18 -6
  34. package/dist/uiMessages.d.ts.map +1 -1
  35. package/dist/uiMessages.js +33 -1
  36. package/dist/uiMessages.js.map +1 -1
  37. package/dist/useStreamRuntime.d.ts +7 -10
  38. package/dist/useStreamRuntime.d.ts.map +1 -1
  39. package/dist/useStreamRuntime.js +100 -26
  40. package/dist/useStreamRuntime.js.map +1 -1
  41. package/dist/useSubagentTranscripts.d.ts +3 -5
  42. package/dist/useSubagentTranscripts.d.ts.map +1 -1
  43. package/dist/useSubagentTranscripts.js +5 -3
  44. package/dist/useSubagentTranscripts.js.map +1 -1
  45. package/package.json +16 -9
  46. package/src/convertMessages.test.ts +153 -0
  47. package/src/convertMessages.ts +54 -8
  48. package/src/converter.ts +6 -0
  49. package/src/subagentMessagesProjection.test.ts +175 -0
  50. package/src/subagentMessagesProjection.ts +50 -0
  51. package/src/tests/settleOutsideAct.ts +15 -0
  52. package/src/uiMessages.test.ts +106 -0
  53. package/src/uiMessages.ts +43 -0
  54. package/src/useStreamRuntime.test.tsx +147 -7
  55. package/src/useStreamRuntime.ts +197 -45
  56. package/src/useStreamRuntime.voice.test.tsx +713 -0
  57. package/src/useSubagentTranscripts.fast-refresh.test.tsx +152 -0
  58. package/src/useSubagentTranscripts.test.tsx +2 -0
  59. package/src/useSubagentTranscripts.ts +5 -7
@@ -0,0 +1,152 @@
1
+ // @vitest-environment jsdom
2
+
3
+ import { act, useMemo } from "react";
4
+ import { afterAll, afterEach, expect, it, vi } from "vitest";
5
+ import type { UIMessage } from "./types";
6
+
7
+ type Family = { current: unknown };
8
+ type RendererInternals = {
9
+ setRefreshHandler: (resolve: (type: unknown) => Family | undefined) => void;
10
+ scheduleRefresh: (
11
+ root: unknown,
12
+ update: { staleFamilies: Set<Family>; updatedFamilies: Set<Family> },
13
+ ) => void;
14
+ };
15
+
16
+ const { streamController } = vi.hoisted(() => ({
17
+ streamController: Symbol("STREAM_CONTROLLER"),
18
+ }));
19
+
20
+ vi.mock("@langchain/react", async (importOriginal) => ({
21
+ ...(await importOriginal<typeof import("@langchain/react")>()),
22
+ STREAM_CONTROLLER: streamController,
23
+ }));
24
+
25
+ let renderer: RendererInternals | undefined;
26
+ const fiberRoots = new Set<unknown>();
27
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
28
+ vi.stubGlobal("__REACT_DEVTOOLS_GLOBAL_HOOK__", {
29
+ supportsFiber: true,
30
+ inject: (internals: RendererInternals) => {
31
+ renderer = internals;
32
+ return 1;
33
+ },
34
+ onScheduleFiberRoot: () => {},
35
+ onCommitFiberRoot: (_id: number, root: unknown) => fiberRoots.add(root),
36
+ onCommitFiberUnmount: () => {},
37
+ });
38
+ const { cleanup, render, waitFor } = await import("@testing-library/react");
39
+ const { useSubagentTranscripts } = await import("./useSubagentTranscripts");
40
+
41
+ afterEach(() => {
42
+ cleanup();
43
+ renderer?.setRefreshHandler(() => undefined);
44
+ fiberRoots.clear();
45
+ });
46
+ afterAll(() => vi.unstubAllGlobals());
47
+
48
+ it("keeps a live subagent projection and namespace request through Fast Refresh, then disposes on unmount", async () => {
49
+ const listeners = new Set<() => void>();
50
+ let messages = [
51
+ { id: "message-one", _getType: () => "ai", content: "partial" },
52
+ ];
53
+ const store = {
54
+ getSnapshot: () => messages,
55
+ subscribe: (listener: () => void) => {
56
+ listeners.add(listener);
57
+ return () => listeners.delete(listener);
58
+ },
59
+ };
60
+ const release = vi.fn();
61
+ const acquire = vi.fn(() => ({ store, release }));
62
+ let settleFirst!: () => void;
63
+ const pending = new Promise<void>((resolve) => {
64
+ settleFirst = resolve;
65
+ });
66
+ const resolveSubagentNamespace = vi
67
+ .fn<() => Promise<void>>()
68
+ .mockReturnValueOnce(pending)
69
+ .mockResolvedValue(undefined);
70
+ const snapshot = (status: "running" | "complete") => ({
71
+ id: "task-one",
72
+ namespace: ["tools:task-one"],
73
+ status,
74
+ parentId: null,
75
+ depth: 1,
76
+ startedAt: new Date(1_000),
77
+ completedAt: status === "complete" ? new Date(4_000) : null,
78
+ });
79
+ const stream = {
80
+ subagents: new Map([["task-one", snapshot("running")]]),
81
+ [streamController]: {
82
+ registry: { acquire },
83
+ resolveSubagentNamespace,
84
+ },
85
+ };
86
+ const uiMaps = new Set<Map<string, UIMessage[]>>();
87
+ let transcript: ReturnType<typeof useSubagentTranscripts> | undefined;
88
+ let rendered: string | undefined;
89
+ const host = (name: string) => () => {
90
+ const uiMessagesByParent = useMemo(
91
+ () => new Map<string, UIMessage[]>(),
92
+ [],
93
+ );
94
+ uiMaps.add(uiMessagesByParent);
95
+ rendered = name;
96
+ transcript = useSubagentTranscripts(stream as never, uiMessagesByParent);
97
+ return null;
98
+ };
99
+ const Before = host("before");
100
+ const After = host("after");
101
+ const view = render(<Before />);
102
+
103
+ await waitFor(() => expect(transcript?.has("task-one")).toBe(true));
104
+ expect(resolveSubagentNamespace).toHaveBeenCalledOnce();
105
+ expect(listeners.size).toBe(1);
106
+ await act(async () => {
107
+ messages = [
108
+ { id: "message-one", _getType: () => "ai", content: "partial answer" },
109
+ ];
110
+ for (const listener of listeners) listener();
111
+ });
112
+
113
+ const family: Family = { current: After };
114
+ renderer!.setRefreshHandler((type) =>
115
+ type === Before || type === After ? family : undefined,
116
+ );
117
+ await act(async () => {
118
+ for (const root of fiberRoots) {
119
+ renderer!.scheduleRefresh(root, {
120
+ staleFamilies: new Set(),
121
+ updatedFamilies: new Set([family]),
122
+ });
123
+ }
124
+ });
125
+ await act(async () => {});
126
+
127
+ expect(rendered).toBe("after");
128
+ expect(uiMaps.size).toBeGreaterThan(1);
129
+ expect(release).not.toHaveBeenCalled();
130
+ expect(acquire).toHaveBeenCalledOnce();
131
+ expect(listeners.size).toBe(1);
132
+ expect(resolveSubagentNamespace).toHaveBeenCalledOnce();
133
+
134
+ stream.subagents = new Map([["task-one", snapshot("complete")]]);
135
+ view.rerender(<After />);
136
+ await waitFor(() =>
137
+ expect(
138
+ transcript?.get("task-one")?.messages[0]?.metadata?.timing,
139
+ ).toMatchObject({
140
+ totalChunks: 2,
141
+ }),
142
+ );
143
+ await act(async () => settleFirst());
144
+ await waitFor(() =>
145
+ expect(resolveSubagentNamespace).toHaveBeenCalledTimes(2),
146
+ );
147
+
148
+ view.unmount();
149
+ await act(async () => {});
150
+ expect(release).toHaveBeenCalledOnce();
151
+ expect(listeners.size).toBe(0);
152
+ });
@@ -185,6 +185,7 @@ describe("useSubagentTranscripts", () => {
185
185
  expect(stream.acquire).toHaveBeenCalledTimes(2);
186
186
  expect(stream.resolveSubagentNamespace).not.toHaveBeenCalled();
187
187
  hook.unmount();
188
+ await act(async () => {});
188
189
  expect(stream.releases.get("tools:one")).toHaveBeenCalledOnce();
189
190
  expect(stream.releases.get("tools:two")).toHaveBeenCalledOnce();
190
191
  });
@@ -773,6 +774,7 @@ describe("useSubagentTranscripts", () => {
773
774
  expect(channelSpecs[0]?.namespace).toEqual(["tools:parent", "tools:child"]);
774
775
 
775
776
  hook.unmount();
777
+ await act(async () => {});
776
778
  expect(
777
779
  stream.uiReleases.get("tools:parent/tools:child"),
778
780
  ).toHaveBeenCalledOnce();
@@ -14,13 +14,10 @@ import {
14
14
  type ExternalMessageConversionCache,
15
15
  type useExternalMessageConverter,
16
16
  } from "@assistant-ui/core/react";
17
+ import { useReplaySafeEffect } from "@assistant-ui/store/internal";
17
18
  import { STREAM_CONTROLLER, type AnyStream } from "@langchain/react";
18
19
  import type { BaseMessage } from "@langchain/core/messages";
19
- import {
20
- channelProjection,
21
- messagesProjection,
22
- type Event,
23
- } from "@langchain/langgraph-sdk/stream";
20
+ import { channelProjection, type Event } from "@langchain/langgraph-sdk/stream";
24
21
  import type { SubagentDiscoverySnapshot } from "@langchain/react";
25
22
  import {
26
23
  attachSubagentTranscripts,
@@ -31,6 +28,7 @@ import {
31
28
  import { convertLangChainBaseMessage } from "./convertMessages";
32
29
  import { groupUIMessagesByParent } from "./converter";
33
30
  import { langChainStreamingTimingAccessors } from "./streamingTiming";
31
+ import { subagentMessagesProjection } from "./subagentMessagesProjection";
34
32
  import type { LangChainBaseMessage, UIMessage } from "./types";
35
33
  import {
36
34
  createUIFoldMemo,
@@ -301,7 +299,7 @@ const createSubagentTranscriptSource = (): SubagentTranscriptSource => {
301
299
  }
302
300
  if (source.resources.has(snapshot.id)) continue;
303
301
  const acquired = controller.registry.acquire(
304
- messagesProjection(snapshot.namespace),
302
+ subagentMessagesProjection(snapshot.namespace),
305
303
  );
306
304
  const acquiredUI =
307
305
  snapshot.namespace.length > ROOT_UI_CHANNEL_DEPTH
@@ -516,7 +514,7 @@ export const useSubagentTranscripts = (
516
514
  source.reconcile(controller, stream.subagents, uiMessagesByParent);
517
515
  }, [controller, source, stream.subagents, uiMessagesByParent]);
518
516
 
519
- useEffect(() => () => source.dispose(), [source]);
517
+ useReplaySafeEffect(() => () => source.dispose(), [source]);
520
518
 
521
519
  return useSyncExternalStore(
522
520
  source.subscribe,