@assistant-ui/react 0.15.17 → 0.15.19
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/README.md +8 -2
- package/dist/context/providers/ThreadViewportProvider.js +2 -2
- package/dist/context/providers/ThreadViewportProvider.js.map +1 -1
- package/dist/hooks/useToolCallElapsed.js.map +1 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +5 -3
- package/dist/mcp-apps/McpAppRenderer.d.ts +30 -8
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.js +91 -47
- package/dist/mcp-apps/McpAppRenderer.js.map +1 -1
- package/dist/mcp-apps/app-frame.d.ts.map +1 -1
- package/dist/mcp-apps/app-frame.js +57 -33
- package/dist/mcp-apps/app-frame.js.map +1 -1
- package/dist/mcp-apps/bridge.js +9 -4
- package/dist/mcp-apps/bridge.js.map +1 -1
- package/dist/mcp-apps/index.d.ts +2 -2
- package/dist/primitives/assistantModal/AssistantModalRoot.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalRoot.js +47 -79
- package/dist/primitives/assistantModal/AssistantModalRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +61 -138
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +3 -8
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts +1 -2
- package/dist/primitives/composer/ComposerInputPluginContext.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInputPluginContext.js +1 -6
- package/dist/primitives/composer/ComposerInputPluginContext.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js +46 -15
- package/dist/primitives/composer/trigger/TriggerPopoverAction.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js +40 -10
- package/dist/primitives/composer/trigger/TriggerPopoverDirective.js.map +1 -1
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts +6 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.d.ts.map +1 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js +9 -0
- package/dist/primitives/composer/trigger/matchesTriggerItemQuery.js.map +1 -0
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js +53 -61
- package/dist/primitives/composer/trigger/triggerKeyboardResource.js.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.js +129 -141
- package/dist/primitives/composer/trigger/triggerNavigationResource.js.map +1 -1
- package/dist/primitives/message/MessageParts.js +2 -14
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.d.ts +3 -3
- package/dist/primitives/message/MessagePartsGrouped.js +2 -2
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts +2 -0
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js +13 -36
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/reasoning/useScrollLock.js +3 -1
- package/dist/primitives/reasoning/useScrollLock.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.d.ts.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +3 -3
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/sandbox-host/SandboxHost.d.ts.map +1 -1
- package/dist/sandbox-host/SandboxHost.js +36 -9
- package/dist/sandbox-host/SandboxHost.js.map +1 -1
- package/dist/unstable/useLiveCompletionAdapter.js +24 -2
- package/dist/unstable/useLiveCompletionAdapter.js.map +1 -1
- package/dist/unstable/useMentionAdapter.d.ts +9 -3
- package/dist/unstable/useMentionAdapter.d.ts.map +1 -1
- package/dist/unstable/useMentionAdapter.js +50 -43
- package/dist/unstable/useMentionAdapter.js.map +1 -1
- package/dist/unstable/useMessageStallDetection.d.ts +4 -5
- package/dist/unstable/useMessageStallDetection.d.ts.map +1 -1
- package/dist/unstable/useMessageStallDetection.js +16 -15
- package/dist/unstable/useMessageStallDetection.js.map +1 -1
- package/dist/unstable/useModelContextSnapshot.d.ts +34 -0
- package/dist/unstable/useModelContextSnapshot.d.ts.map +1 -0
- package/dist/unstable/useModelContextSnapshot.js +61 -0
- package/dist/unstable/useModelContextSnapshot.js.map +1 -0
- package/dist/unstable/useSlashCommandAdapter.d.ts.map +1 -1
- package/dist/unstable/useSlashCommandAdapter.js +2 -8
- package/dist/unstable/useSlashCommandAdapter.js.map +1 -1
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts +14 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.d.ts.map +1 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js +85 -0
- package/dist/unstable/webmcp/WebMcpRegistrationResource.js.map +1 -0
- package/dist/unstable/webmcp/convertTools.d.ts +21 -0
- package/dist/unstable/webmcp/convertTools.d.ts.map +1 -0
- package/dist/unstable/webmcp/convertTools.js +161 -0
- package/dist/unstable/webmcp/convertTools.js.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts +21 -0
- package/dist/unstable/webmcp/useWebMcpProvider.d.ts.map +1 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js +106 -0
- package/dist/unstable/webmcp/useWebMcpProvider.js.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts +39 -0
- package/dist/unstable/webmcp/webmcp-host.d.ts.map +1 -0
- package/dist/unstable/webmcp/webmcp-host.js +29 -0
- package/dist/unstable/webmcp/webmcp-host.js.map +1 -0
- package/dist/utils/Primitive.d.ts +10 -2
- package/dist/utils/Primitive.d.ts.map +1 -1
- package/dist/utils/Primitive.js +25 -6
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/getSelectionMessageId.d.ts.map +1 -1
- package/dist/utils/getSelectionMessageId.js +17 -3
- package/dist/utils/getSelectionMessageId.js.map +1 -1
- package/package.json +13 -13
- package/src/context/providers/ThreadViewportProvider.tsx +2 -2
- package/src/hooks/useToolCallElapsed.ts +3 -0
- package/src/index.ts +14 -1
- package/src/mcp-apps/McpAppRenderer.test.tsx +253 -5
- package/src/mcp-apps/McpAppRenderer.tsx +147 -52
- package/src/mcp-apps/app-frame.test.tsx +437 -1
- package/src/mcp-apps/app-frame.tsx +76 -21
- package/src/mcp-apps/bridge.test.ts +98 -0
- package/src/mcp-apps/bridge.ts +14 -3
- package/src/mcp-apps/index.ts +5 -1
- package/src/primitives/assistantModal/AssistantModalRoot.test.tsx +123 -0
- package/src/primitives/assistantModal/AssistantModalRoot.tsx +11 -31
- package/src/primitives/assistantModal/AssistantModalTrigger.test.tsx +1 -16
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +43 -0
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +2 -10
- package/src/primitives/composer/ComposerInput.test.tsx +22 -0
- package/src/primitives/composer/ComposerInput.tsx +5 -9
- package/src/primitives/composer/ComposerInputPluginContext.tsx +0 -10
- package/src/primitives/composer/trigger/TriggerPopoverAction.tsx +4 -2
- package/src/primitives/composer/trigger/TriggerPopoverBehavior.test.tsx +109 -0
- package/src/primitives/composer/trigger/TriggerPopoverDirective.tsx +4 -2
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.test.ts +52 -0
- package/src/primitives/composer/trigger/matchesTriggerItemQuery.ts +13 -0
- package/src/primitives/composer/trigger/triggerKeyboardResource.ts +13 -7
- package/src/primitives/composer/trigger/triggerNavigationResource.ts +8 -13
- package/src/primitives/message/MessageParts.tsx +1 -21
- package/src/primitives/message/MessagePartsGrouped.test.tsx +93 -0
- package/src/primitives/message/MessagePartsGrouped.tsx +6 -6
- package/src/primitives/messagePart/MessagePartText.test.tsx +69 -0
- package/src/primitives/messagePart/MessagePartText.tsx +22 -9
- package/src/primitives/reasoning/useScrollLock.test.tsx +129 -0
- package/src/primitives/reasoning/useScrollLock.ts +15 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +38 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +7 -6
- package/src/sandbox-host/SandboxHost.test.tsx +160 -2
- package/src/sandbox-host/SandboxHost.tsx +50 -10
- package/src/tests/MessageParts.rendererOptions.test.tsx +117 -0
- package/src/tests/toolCallTiming.test.tsx +55 -1
- package/src/unstable/useComposerInputHistory.test.tsx +2 -1
- package/src/unstable/useLiveCompletionAdapter.test.tsx +202 -1
- package/src/unstable/useLiveCompletionAdapter.ts +38 -8
- package/src/unstable/useMentionAdapter.test.tsx +383 -0
- package/src/unstable/useMentionAdapter.ts +94 -57
- package/src/unstable/useMessageStallDetection.ts +26 -19
- package/src/unstable/useModelContextSnapshot.test.tsx +132 -0
- package/src/unstable/useModelContextSnapshot.ts +67 -0
- package/src/unstable/useSlashCommandAdapter.ts +2 -9
- package/src/unstable/webmcp/WebMcpRegistrationResource.ts +92 -0
- package/src/unstable/webmcp/__tests__/webmcp.fake.tsx +168 -0
- package/src/unstable/webmcp/convertTools.test.ts +553 -0
- package/src/unstable/webmcp/convertTools.ts +260 -0
- package/src/unstable/webmcp/useWebMcpProvider.test.tsx +466 -0
- package/src/unstable/webmcp/useWebMcpProvider.ts +148 -0
- package/src/unstable/webmcp/webmcp-host.test.ts +142 -0
- package/src/unstable/webmcp/webmcp-host.ts +83 -0
- package/src/utils/Primitive.test.tsx +46 -1
- package/src/utils/Primitive.tsx +37 -7
- package/src/utils/getSelectionMessageId.test.ts +176 -1
- package/src/utils/getSelectionMessageId.ts +21 -3
- package/dist/utils/invokeCallbackSafely.d.ts +0 -5
- package/dist/utils/invokeCallbackSafely.d.ts.map +0 -1
- package/dist/utils/invokeCallbackSafely.js +0 -15
- package/dist/utils/invokeCallbackSafely.js.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.d.ts +0 -19
- package/dist/utils/useToolArgsFieldStatus.d.ts.map +0 -1
- package/dist/utils/useToolArgsFieldStatus.js +0 -23
- package/dist/utils/useToolArgsFieldStatus.js.map +0 -1
- package/src/utils/invokeCallbackSafely.ts +0 -11
- package/src/utils/useToolArgsFieldStatus.ts +0 -18
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
|
|
3
|
+
import { act, renderHook } from "@testing-library/react";
|
|
4
|
+
import {
|
|
5
|
+
createTapRoot,
|
|
6
|
+
flushTapSync,
|
|
7
|
+
resource,
|
|
8
|
+
useResource,
|
|
9
|
+
} from "@assistant-ui/tap";
|
|
10
|
+
import type { Unstable_TriggerAdapter } from "@assistant-ui/core";
|
|
11
|
+
import { useState } from "react";
|
|
12
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
|
+
import { TriggerNavigationResource } from "../primitives/composer/trigger/triggerNavigationResource";
|
|
14
|
+
import {
|
|
15
|
+
unstable_useMentionAdapter,
|
|
16
|
+
type Unstable_UseMentionAdapterOptions,
|
|
17
|
+
} from "./useMentionAdapter";
|
|
18
|
+
|
|
19
|
+
const runtime = vi.hoisted(() => {
|
|
20
|
+
const state = {
|
|
21
|
+
tools: {} as Record<string, { description?: string }>,
|
|
22
|
+
};
|
|
23
|
+
const listeners = new Map<string, Set<() => void>>();
|
|
24
|
+
const events: string[] = [];
|
|
25
|
+
return {
|
|
26
|
+
state,
|
|
27
|
+
listeners,
|
|
28
|
+
events,
|
|
29
|
+
client: {
|
|
30
|
+
thread: {
|
|
31
|
+
getModelContext: () => ({ tools: state.tools }),
|
|
32
|
+
},
|
|
33
|
+
on: (
|
|
34
|
+
selector: string | { scope: string; event: string },
|
|
35
|
+
listener: () => void,
|
|
36
|
+
) => {
|
|
37
|
+
const event = typeof selector === "string" ? selector : selector.event;
|
|
38
|
+
events.push(event);
|
|
39
|
+
const eventListeners = listeners.get(event) ?? new Set();
|
|
40
|
+
eventListeners.add(listener);
|
|
41
|
+
listeners.set(event, eventListeners);
|
|
42
|
+
return () => eventListeners.delete(listener);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
emit: (event: string) =>
|
|
46
|
+
listeners.get(event)?.forEach((listener) => listener()),
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => ({
|
|
51
|
+
...(await importOriginal<typeof import("@assistant-ui/store")>()),
|
|
52
|
+
useAui: () => runtime.client,
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
const TestNavigationResource = resource(function useTestNavigationResource(
|
|
56
|
+
initialAdapter: Unstable_TriggerAdapter,
|
|
57
|
+
) {
|
|
58
|
+
const [adapter, setAdapter] = useState(initialAdapter);
|
|
59
|
+
const navigation = useResource(
|
|
60
|
+
TriggerNavigationResource({ adapter, query: "", open: true }),
|
|
61
|
+
);
|
|
62
|
+
return { navigation, setAdapter };
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
describe("unstable_useMentionAdapter", () => {
|
|
66
|
+
beforeEach(() => {
|
|
67
|
+
runtime.state.tools = {};
|
|
68
|
+
runtime.listeners.clear();
|
|
69
|
+
runtime.events.length = 0;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("ignores flat items when categories is explicitly empty", () => {
|
|
73
|
+
const { result } = renderHook(() =>
|
|
74
|
+
unstable_useMentionAdapter({
|
|
75
|
+
items: [{ id: "alice", type: "person", label: "Alice" }],
|
|
76
|
+
categories: [],
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result.current.adapter.search?.("")).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not expose flat items when the last category is removed", () => {
|
|
84
|
+
const items = [{ id: "alice", type: "person", label: "Alice" }];
|
|
85
|
+
const { result, rerender } = renderHook(
|
|
86
|
+
(options: Unstable_UseMentionAdapterOptions) =>
|
|
87
|
+
unstable_useMentionAdapter(options),
|
|
88
|
+
{
|
|
89
|
+
initialProps: {
|
|
90
|
+
items,
|
|
91
|
+
categories: [
|
|
92
|
+
{
|
|
93
|
+
id: "people",
|
|
94
|
+
label: "People",
|
|
95
|
+
items: [{ id: "bob", type: "person", label: "Bob" }],
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
expect(result.current.adapter.search?.("")).toEqual([
|
|
103
|
+
{ id: "bob", type: "person", label: "Bob" },
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
rerender({ items, categories: [] });
|
|
107
|
+
expect(result.current.adapter.search?.("")).toEqual([]);
|
|
108
|
+
|
|
109
|
+
rerender({ items });
|
|
110
|
+
expect(result.current.adapter.search?.("")).toEqual([
|
|
111
|
+
{ id: "alice", type: "person", label: "Alice" },
|
|
112
|
+
]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("keeps categorized model-context tools current", () => {
|
|
116
|
+
runtime.state.tools = {
|
|
117
|
+
searchDocs: { description: "Search documentation" },
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const { result } = renderHook(() =>
|
|
121
|
+
unstable_useMentionAdapter({
|
|
122
|
+
categories: [{ id: "people", label: "People", items: [] }],
|
|
123
|
+
includeModelContextTools: true,
|
|
124
|
+
}),
|
|
125
|
+
);
|
|
126
|
+
const initialAdapter = result.current.adapter;
|
|
127
|
+
const navigationRoot = createTapRoot(function NavigationRoot() {
|
|
128
|
+
return useResource(TestNavigationResource(initialAdapter));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
expect(navigationRoot.getValue().navigation.categories).toEqual([
|
|
132
|
+
{ id: "people", label: "People" },
|
|
133
|
+
{ id: "tools", label: "Tools" },
|
|
134
|
+
]);
|
|
135
|
+
expect(runtime.events).toContain("thread.modelContextUpdate");
|
|
136
|
+
|
|
137
|
+
act(() =>
|
|
138
|
+
flushTapSync(() =>
|
|
139
|
+
navigationRoot.getValue().navigation.selectCategory("tools"),
|
|
140
|
+
),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(navigationRoot.getValue().navigation.items).toEqual([
|
|
144
|
+
{
|
|
145
|
+
id: "searchDocs",
|
|
146
|
+
type: "tool",
|
|
147
|
+
label: "searchDocs",
|
|
148
|
+
description: "Search documentation",
|
|
149
|
+
},
|
|
150
|
+
]);
|
|
151
|
+
|
|
152
|
+
act(() => {
|
|
153
|
+
runtime.state.tools = {
|
|
154
|
+
createIssue: { description: "Create an issue" },
|
|
155
|
+
};
|
|
156
|
+
runtime.emit("thread.modelContextUpdate");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result.current.adapter).not.toBe(initialAdapter);
|
|
160
|
+
act(() =>
|
|
161
|
+
flushTapSync(() =>
|
|
162
|
+
navigationRoot.getValue().setAdapter(result.current.adapter),
|
|
163
|
+
),
|
|
164
|
+
);
|
|
165
|
+
expect(navigationRoot.getValue().navigation.items).toEqual([
|
|
166
|
+
{
|
|
167
|
+
id: "createIssue",
|
|
168
|
+
type: "tool",
|
|
169
|
+
label: "createIssue",
|
|
170
|
+
description: "Create an issue",
|
|
171
|
+
},
|
|
172
|
+
]);
|
|
173
|
+
act(() => {
|
|
174
|
+
runtime.state.tools = {};
|
|
175
|
+
runtime.emit("thread.modelContextUpdate");
|
|
176
|
+
});
|
|
177
|
+
act(() =>
|
|
178
|
+
flushTapSync(() =>
|
|
179
|
+
navigationRoot.getValue().setAdapter(result.current.adapter),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
expect(navigationRoot.getValue().navigation.categories).toEqual([
|
|
184
|
+
{ id: "people", label: "People" },
|
|
185
|
+
]);
|
|
186
|
+
expect(navigationRoot.getValue().navigation.items).toEqual([]);
|
|
187
|
+
navigationRoot.unmount();
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("keeps flat model-context tools current while navigation is open", () => {
|
|
191
|
+
runtime.state.tools = {
|
|
192
|
+
searchDocs: { description: "Search documentation" },
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const { result } = renderHook(() => unstable_useMentionAdapter());
|
|
196
|
+
const initialAdapter = result.current.adapter;
|
|
197
|
+
const navigationRoot = createTapRoot(function NavigationRoot() {
|
|
198
|
+
return useResource(TestNavigationResource(initialAdapter));
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
expect(runtime.events).toContain("thread.modelContextUpdate");
|
|
202
|
+
|
|
203
|
+
expect(navigationRoot.getValue().navigation.items).toEqual([
|
|
204
|
+
{
|
|
205
|
+
id: "searchDocs",
|
|
206
|
+
type: "tool",
|
|
207
|
+
label: "searchDocs",
|
|
208
|
+
description: "Search documentation",
|
|
209
|
+
},
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
act(() => {
|
|
213
|
+
runtime.state.tools = {
|
|
214
|
+
createIssue: { description: "Create an issue" },
|
|
215
|
+
};
|
|
216
|
+
runtime.emit("thread.modelContextUpdate");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
expect(result.current.adapter).not.toBe(initialAdapter);
|
|
220
|
+
act(() =>
|
|
221
|
+
flushTapSync(() =>
|
|
222
|
+
navigationRoot.getValue().setAdapter(result.current.adapter),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
expect(navigationRoot.getValue().navigation.items).toEqual([
|
|
226
|
+
{
|
|
227
|
+
id: "createIssue",
|
|
228
|
+
type: "tool",
|
|
229
|
+
label: "createIssue",
|
|
230
|
+
description: "Create an issue",
|
|
231
|
+
},
|
|
232
|
+
]);
|
|
233
|
+
navigationRoot.unmount();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("refreshes tool mentions when the selected thread changes", () => {
|
|
237
|
+
runtime.state.tools = {
|
|
238
|
+
searchDocs: { description: "Search documentation" },
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
const { result } = renderHook(() => unstable_useMentionAdapter());
|
|
242
|
+
const initialAdapter = result.current.adapter;
|
|
243
|
+
|
|
244
|
+
expect(runtime.events).toContain("threads.selectionChanged");
|
|
245
|
+
|
|
246
|
+
act(() => {
|
|
247
|
+
runtime.state.tools = {
|
|
248
|
+
createIssue: { description: "Create an issue" },
|
|
249
|
+
};
|
|
250
|
+
runtime.emit("threads.selectionChanged");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
expect(result.current.adapter).not.toBe(initialAdapter);
|
|
254
|
+
expect(result.current.adapter.search?.("")).toEqual([
|
|
255
|
+
{
|
|
256
|
+
id: "createIssue",
|
|
257
|
+
type: "tool",
|
|
258
|
+
label: "createIssue",
|
|
259
|
+
description: "Create an issue",
|
|
260
|
+
},
|
|
261
|
+
]);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("keeps adapter identity when tool mentions are unchanged", () => {
|
|
265
|
+
runtime.state.tools = {
|
|
266
|
+
searchDocs: { description: "Search documentation" },
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const { result } = renderHook(() => unstable_useMentionAdapter());
|
|
270
|
+
const initialAdapter = result.current.adapter;
|
|
271
|
+
|
|
272
|
+
act(() => {
|
|
273
|
+
runtime.state.tools = {
|
|
274
|
+
searchDocs: { description: "Search documentation" },
|
|
275
|
+
};
|
|
276
|
+
runtime.emit("thread.modelContextUpdate");
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.current.adapter).toBe(initialAdapter);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("observes a description mutated in place on a stable tool object", () => {
|
|
283
|
+
const tool = { description: "Search documentation" };
|
|
284
|
+
runtime.state.tools = { searchDocs: tool };
|
|
285
|
+
|
|
286
|
+
const { result } = renderHook(() => unstable_useMentionAdapter());
|
|
287
|
+
const initialAdapter = result.current.adapter;
|
|
288
|
+
|
|
289
|
+
act(() => {
|
|
290
|
+
tool.description = "Search the docs site";
|
|
291
|
+
runtime.emit("thread.modelContextUpdate");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
expect(result.current.adapter).not.toBe(initialAdapter);
|
|
295
|
+
expect(result.current.adapter.search?.("")).toEqual([
|
|
296
|
+
{
|
|
297
|
+
id: "searchDocs",
|
|
298
|
+
type: "tool",
|
|
299
|
+
label: "searchDocs",
|
|
300
|
+
description: "Search the docs site",
|
|
301
|
+
},
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("uses an explicit tool category without custom categories", () => {
|
|
306
|
+
runtime.state.tools = {
|
|
307
|
+
searchDocs: { description: "Search documentation" },
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const { result } = renderHook(() =>
|
|
311
|
+
unstable_useMentionAdapter({
|
|
312
|
+
includeModelContextTools: {
|
|
313
|
+
category: { id: "actions", label: "Actions" },
|
|
314
|
+
},
|
|
315
|
+
}),
|
|
316
|
+
);
|
|
317
|
+
|
|
318
|
+
expect(result.current.adapter.categories()).toEqual([
|
|
319
|
+
{ id: "actions", label: "Actions" },
|
|
320
|
+
]);
|
|
321
|
+
expect(result.current.adapter.categoryItems("actions")).toEqual([
|
|
322
|
+
{
|
|
323
|
+
id: "searchDocs",
|
|
324
|
+
type: "tool",
|
|
325
|
+
label: "searchDocs",
|
|
326
|
+
description: "Search documentation",
|
|
327
|
+
},
|
|
328
|
+
]);
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it("keeps an async explicit item list flat when a tool category is configured", () => {
|
|
332
|
+
runtime.state.tools = {
|
|
333
|
+
searchDocs: { description: "Search documentation" },
|
|
334
|
+
createIssue: { description: "Create an issue" },
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
const { result, rerender } = renderHook(
|
|
338
|
+
({
|
|
339
|
+
items,
|
|
340
|
+
}: {
|
|
341
|
+
items: readonly {
|
|
342
|
+
id: string;
|
|
343
|
+
type: string;
|
|
344
|
+
label: string;
|
|
345
|
+
}[];
|
|
346
|
+
}) =>
|
|
347
|
+
unstable_useMentionAdapter({
|
|
348
|
+
items,
|
|
349
|
+
includeModelContextTools: {
|
|
350
|
+
category: { id: "actions", label: "Actions" },
|
|
351
|
+
},
|
|
352
|
+
}),
|
|
353
|
+
{ initialProps: { items: [] } },
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
expect(result.current.adapter.categories()).toEqual([]);
|
|
357
|
+
|
|
358
|
+
rerender({
|
|
359
|
+
items: [
|
|
360
|
+
{
|
|
361
|
+
id: "searchDocs",
|
|
362
|
+
type: "person",
|
|
363
|
+
label: "Documentation owner",
|
|
364
|
+
},
|
|
365
|
+
],
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(result.current.adapter.categories()).toEqual([]);
|
|
369
|
+
expect(result.current.adapter.search?.("")).toEqual([
|
|
370
|
+
{
|
|
371
|
+
id: "searchDocs",
|
|
372
|
+
type: "person",
|
|
373
|
+
label: "Documentation owner",
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
id: "createIssue",
|
|
377
|
+
type: "tool",
|
|
378
|
+
label: "createIssue",
|
|
379
|
+
description: "Create an issue",
|
|
380
|
+
},
|
|
381
|
+
]);
|
|
382
|
+
});
|
|
383
|
+
});
|
|
@@ -10,6 +10,12 @@ import type {
|
|
|
10
10
|
} from "@assistant-ui/core";
|
|
11
11
|
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
12
12
|
import type { ReadonlyJSONObject } from "assistant-stream/utils";
|
|
13
|
+
import { matchesTriggerItemQuery } from "../primitives/composer/trigger/matchesTriggerItemQuery";
|
|
14
|
+
import {
|
|
15
|
+
shallowEqualRecords,
|
|
16
|
+
useModelContextSnapshot,
|
|
17
|
+
type ModelContextSnapshotSource,
|
|
18
|
+
} from "./useModelContextSnapshot";
|
|
13
19
|
|
|
14
20
|
/** Icon component shape consumed by `ComposerTriggerPopover`'s `iconMap`. */
|
|
15
21
|
export type Unstable_IconComponent = FC<{ className?: string }>;
|
|
@@ -31,7 +37,10 @@ export type Unstable_MentionCategory = {
|
|
|
31
37
|
};
|
|
32
38
|
|
|
33
39
|
export type Unstable_ModelContextToolsOptions = {
|
|
34
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Wrap tools in a dedicated category. Selects drill-down mode on its own
|
|
42
|
+
* when `categories` is unset and `items` is omitted.
|
|
43
|
+
*/
|
|
35
44
|
readonly category?: { readonly id: string; readonly label: string };
|
|
36
45
|
/** Format tool name for display. */
|
|
37
46
|
readonly formatLabel?: (toolName: string) => string;
|
|
@@ -40,7 +49,10 @@ export type Unstable_ModelContextToolsOptions = {
|
|
|
40
49
|
};
|
|
41
50
|
|
|
42
51
|
export type Unstable_UseMentionAdapterOptions = {
|
|
43
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* Flat mention list. Ignored when `categories` is set, and keeps the
|
|
54
|
+
* adapter flat when a tool `category` is configured.
|
|
55
|
+
*/
|
|
44
56
|
readonly items?: readonly Unstable_Mention[];
|
|
45
57
|
/** Categorized mentions for drill-down navigation. */
|
|
46
58
|
readonly categories?: readonly Unstable_MentionCategory[];
|
|
@@ -49,7 +61,7 @@ export type Unstable_UseMentionAdapterOptions = {
|
|
|
49
61
|
* - `false`: exclude.
|
|
50
62
|
* - `true`: include (default when no `items`/`categories`; as a category
|
|
51
63
|
* if `categories` is set, flat otherwise).
|
|
52
|
-
* - object: explicit config.
|
|
64
|
+
* - object: explicit config; `category` also selects drill-down mode.
|
|
53
65
|
*
|
|
54
66
|
* Omitted → defaults to `true` iff neither `items` nor `categories`.
|
|
55
67
|
*/
|
|
@@ -71,6 +83,42 @@ export type Unstable_MentionDirective = {
|
|
|
71
83
|
readonly onInserted?: ((item: Unstable_TriggerItem) => void) | undefined;
|
|
72
84
|
};
|
|
73
85
|
|
|
86
|
+
const EMPTY_TOOL_MENTIONS: Readonly<Record<string, string | undefined>> =
|
|
87
|
+
Object.freeze({});
|
|
88
|
+
|
|
89
|
+
// The description is copied out rather than reached through the tool, so a
|
|
90
|
+
// provider that returns a stable tool object and edits it in place is observed.
|
|
91
|
+
const toolMentionSource: ModelContextSnapshotSource<
|
|
92
|
+
Readonly<Record<string, string | undefined>>
|
|
93
|
+
> = {
|
|
94
|
+
empty: EMPTY_TOOL_MENTIONS,
|
|
95
|
+
read: (aui) => {
|
|
96
|
+
const tools = aui.thread.getModelContext().tools;
|
|
97
|
+
if (!tools) return EMPTY_TOOL_MENTIONS;
|
|
98
|
+
const mentions = Object.create(null) as Record<string, string | undefined>;
|
|
99
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
100
|
+
mentions[name] = tool.description;
|
|
101
|
+
}
|
|
102
|
+
return mentions;
|
|
103
|
+
},
|
|
104
|
+
subscribe: (aui, onChange) => {
|
|
105
|
+
const unsubscribeContext = aui.on("thread.modelContextUpdate", onChange);
|
|
106
|
+
// Rebinding the thread event subject to a new thread does not replay it,
|
|
107
|
+
// so a switch between threads carrying different providers is its own
|
|
108
|
+
// refresh trigger. Subscribed globally because a composer can render
|
|
109
|
+
// without a thread list scope.
|
|
110
|
+
const unsubscribeSelection = aui.on(
|
|
111
|
+
{ scope: "*", event: "threads.selectionChanged" },
|
|
112
|
+
onChange,
|
|
113
|
+
);
|
|
114
|
+
return () => {
|
|
115
|
+
unsubscribeContext();
|
|
116
|
+
unsubscribeSelection();
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
isEqual: shallowEqualRecords,
|
|
120
|
+
};
|
|
121
|
+
|
|
74
122
|
/**
|
|
75
123
|
* @deprecated Under active development and might change without notice.
|
|
76
124
|
*
|
|
@@ -103,50 +151,48 @@ export function unstable_useMentionAdapter(
|
|
|
103
151
|
const wantsTools = includeTools !== false;
|
|
104
152
|
const formatter = options?.formatter;
|
|
105
153
|
const onInserted = options?.onInserted;
|
|
154
|
+
const isCategorized =
|
|
155
|
+
categories !== undefined ||
|
|
156
|
+
(toolsConfig?.category !== undefined && items === undefined);
|
|
157
|
+
const toolMentions = useModelContextSnapshot(
|
|
158
|
+
aui,
|
|
159
|
+
wantsTools,
|
|
160
|
+
toolMentionSource,
|
|
161
|
+
);
|
|
106
162
|
|
|
107
163
|
const adapter = useMemo<Unstable_TriggerAdapter>(() => {
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
icon: defaultIcon,
|
|
122
|
-
}),
|
|
123
|
-
);
|
|
124
|
-
};
|
|
164
|
+
const formatLabel = toolsConfig?.formatLabel;
|
|
165
|
+
const defaultIcon = toolsConfig?.icon;
|
|
166
|
+
const toolItems = wantsTools
|
|
167
|
+
? Object.entries(toolMentions).map(([name, description]) =>
|
|
168
|
+
toTriggerItem({
|
|
169
|
+
id: name,
|
|
170
|
+
type: "tool",
|
|
171
|
+
label: formatLabel ? formatLabel(name) : name,
|
|
172
|
+
description,
|
|
173
|
+
icon: defaultIcon,
|
|
174
|
+
}),
|
|
175
|
+
)
|
|
176
|
+
: [];
|
|
125
177
|
|
|
126
178
|
// Categorized: drill-down mode
|
|
127
|
-
if (
|
|
128
|
-
const groups = categories.map((cat) => ({
|
|
179
|
+
if (isCategorized) {
|
|
180
|
+
const groups = (categories ?? []).map((cat) => ({
|
|
129
181
|
id: cat.id,
|
|
130
182
|
label: cat.label,
|
|
131
183
|
items: cat.items.map(toTriggerItem),
|
|
132
184
|
}));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
label: toolsConfig?.category?.label ?? "Tools",
|
|
145
|
-
items: toolItems,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
const allGroups = toolCategory ? [...groups, toolCategory] : groups;
|
|
185
|
+
const allGroups =
|
|
186
|
+
toolItems.length > 0
|
|
187
|
+
? [
|
|
188
|
+
...groups,
|
|
189
|
+
{
|
|
190
|
+
id: toolsConfig?.category?.id ?? "tools",
|
|
191
|
+
label: toolsConfig?.category?.label ?? "Tools",
|
|
192
|
+
items: toolItems,
|
|
193
|
+
},
|
|
194
|
+
]
|
|
195
|
+
: groups;
|
|
150
196
|
|
|
151
197
|
return {
|
|
152
198
|
categories: () => allGroups.map(({ id, label }) => ({ id, label })),
|
|
@@ -155,30 +201,29 @@ export function unstable_useMentionAdapter(
|
|
|
155
201
|
const lower = query.toLowerCase();
|
|
156
202
|
return allGroups
|
|
157
203
|
.flatMap((g) => g.items)
|
|
158
|
-
.filter((item) =>
|
|
204
|
+
.filter((item) => matchesTriggerItemQuery(item, lower));
|
|
159
205
|
},
|
|
160
206
|
};
|
|
161
207
|
}
|
|
162
208
|
|
|
163
209
|
// Flat: items + (optionally) tools, all in one search pool
|
|
164
210
|
const flatItems = (items ?? []).map(toTriggerItem);
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
};
|
|
211
|
+
// Dedupe by id — explicit items win.
|
|
212
|
+
const seen = new Set(flatItems.map((i) => i.id));
|
|
213
|
+
const flatPool = [
|
|
214
|
+
...flatItems,
|
|
215
|
+
...toolItems.filter((t) => !seen.has(t.id)),
|
|
216
|
+
];
|
|
172
217
|
|
|
173
218
|
return {
|
|
174
219
|
categories: (): readonly Unstable_TriggerCategory[] => [],
|
|
175
220
|
categoryItems: () => [],
|
|
176
221
|
search: (query) => {
|
|
177
222
|
const lower = query.toLowerCase();
|
|
178
|
-
return
|
|
223
|
+
return flatPool.filter((item) => matchesTriggerItemQuery(item, lower));
|
|
179
224
|
},
|
|
180
225
|
};
|
|
181
|
-
}, [
|
|
226
|
+
}, [items, categories, wantsTools, toolsConfig, isCategorized, toolMentions]);
|
|
182
227
|
|
|
183
228
|
const directive = useMemo<Unstable_MentionDirective>(
|
|
184
229
|
() => ({
|
|
@@ -207,11 +252,3 @@ function toTriggerItem(m: Unstable_Mention): Unstable_TriggerItem {
|
|
|
207
252
|
...(metadata !== undefined ? { metadata } : {}),
|
|
208
253
|
};
|
|
209
254
|
}
|
|
210
|
-
|
|
211
|
-
function matchesQuery(item: Unstable_TriggerItem, lower: string): boolean {
|
|
212
|
-
if (!lower) return true;
|
|
213
|
-
if (item.id.toLowerCase().includes(lower)) return true;
|
|
214
|
-
if (item.label.toLowerCase().includes(lower)) return true;
|
|
215
|
-
if (item.description?.toLowerCase().includes(lower)) return true;
|
|
216
|
-
return false;
|
|
217
|
-
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
|
+
import { useShallowSelector } from "@assistant-ui/store/internal";
|
|
5
6
|
|
|
6
7
|
export type Unstable_MessageStallDetectionOptions = {
|
|
7
8
|
/**
|
|
@@ -23,11 +24,10 @@ export type Unstable_MessageStallDetection = {
|
|
|
23
24
|
* @deprecated Under active development and might change without notice.
|
|
24
25
|
*
|
|
25
26
|
* Detects mid-run output stalls on the current message: while the message is
|
|
26
|
-
* running, watches
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* tokens have already streamed.
|
|
27
|
+
* running, watches its text, reasoning, and tool-argument values plus tool-result
|
|
28
|
+
* availability and reports a stall once they stop changing for `thresholdMs`.
|
|
29
|
+
* Useful for re-surfacing a "still working" indicator during tool think-time or
|
|
30
|
+
* provider stalls, after the first tokens have already streamed.
|
|
31
31
|
*
|
|
32
32
|
* Must be used inside a message scope.
|
|
33
33
|
*/
|
|
@@ -36,20 +36,27 @@ export function unstable_useMessageStallDetection(
|
|
|
36
36
|
): Unstable_MessageStallDetection {
|
|
37
37
|
const thresholdMs = options?.thresholdMs ?? 2000;
|
|
38
38
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
const activity = useAuiState(
|
|
40
|
+
useShallowSelector((s) => {
|
|
41
|
+
const running = s.message.status?.type === "running";
|
|
42
|
+
if (!running) return [false];
|
|
43
|
+
|
|
44
|
+
const values: unknown[] = [true, s.message.content.length];
|
|
45
|
+
|
|
46
|
+
for (const part of s.message.content) {
|
|
47
|
+
if (part.type === "text" || part.type === "reasoning") {
|
|
48
|
+
values.push(part.type, part.text);
|
|
49
|
+
} else if (part.type === "tool-call") {
|
|
50
|
+
values.push(part.type, part.argsText, part.result !== undefined);
|
|
51
|
+
} else {
|
|
52
|
+
values.push(part.type);
|
|
53
|
+
}
|
|
47
54
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
return values;
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
51
58
|
|
|
52
|
-
const running =
|
|
59
|
+
const running = activity[0] === true;
|
|
53
60
|
const lastActivityRef = useRef(Date.now());
|
|
54
61
|
const [stalled, setStalled] = useState(false);
|
|
55
62
|
const [, setTick] = useState(0);
|
|
@@ -58,7 +65,7 @@ export function unstable_useMessageStallDetection(
|
|
|
58
65
|
if (!running) return undefined;
|
|
59
66
|
lastActivityRef.current = Date.now();
|
|
60
67
|
return undefined;
|
|
61
|
-
}, [running,
|
|
68
|
+
}, [running, activity]);
|
|
62
69
|
|
|
63
70
|
useEffect(() => {
|
|
64
71
|
if (!running) {
|
|
@@ -75,7 +82,7 @@ export function unstable_useMessageStallDetection(
|
|
|
75
82
|
setStalled(false);
|
|
76
83
|
const id = setTimeout(() => setStalled(true), thresholdMs - sinceActivity);
|
|
77
84
|
return () => clearTimeout(id);
|
|
78
|
-
}, [running,
|
|
85
|
+
}, [running, activity, thresholdMs]);
|
|
79
86
|
|
|
80
87
|
useEffect(() => {
|
|
81
88
|
if (!stalled) return undefined;
|