@gram-ai/elements 1.37.0 → 1.38.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/components/Markdown.d.ts +7 -0
- package/dist/components/MessageContent.d.ts +4 -0
- package/dist/components/ui/tool-ui.d.ts +52 -3
- package/dist/contexts/ThreadMetaContext.d.ts +20 -0
- package/dist/elements.cjs +1 -1
- package/dist/elements.css +1 -1
- package/dist/elements.js +26 -22
- package/dist/hooks/useMCPTools.d.ts +1 -1
- package/dist/{index-Em1Ot0b6.js → index--UMkUr53.js} +27562 -21884
- package/dist/index--UMkUr53.js.map +1 -0
- package/dist/index-Cz9y5YHw.cjs +222 -0
- package/dist/index-Cz9y5YHw.cjs.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/lib/messageConverter.d.ts +2 -0
- package/dist/{profiler-BnInDjd4.js → profiler-BHXyuGiY.js} +2 -2
- package/dist/{profiler-BnInDjd4.js.map → profiler-BHXyuGiY.js.map} +1 -1
- package/dist/{profiler-DIwReaSQ.cjs → profiler-jAEvoPXB.cjs} +2 -2
- package/dist/{profiler-DIwReaSQ.cjs.map → profiler-jAEvoPXB.cjs.map} +1 -1
- package/dist/{startRecording-P_J6QFPD.js → startRecording-D8IbKhJo.js} +2 -2
- package/dist/{startRecording-P_J6QFPD.js.map → startRecording-D8IbKhJo.js.map} +1 -1
- package/dist/{startRecording-Cg4fxzWw.cjs → startRecording-Dw4aGDrV.cjs} +2 -2
- package/dist/{startRecording-Cg4fxzWw.cjs.map → startRecording-Dw4aGDrV.cjs.map} +1 -1
- package/package.json +11 -13
- package/src/components/Markdown.tsx +210 -0
- package/src/components/MessageContent.tsx +9 -0
- package/src/components/assistant-ui/thinking-indicator.tsx +42 -0
- package/src/components/assistant-ui/thread-list.tsx +50 -5
- package/src/components/ui/tool-ui.tsx +360 -7
- package/src/contexts/ElementsProvider.tsx +2 -1
- package/src/contexts/ThreadMetaContext.ts +27 -0
- package/src/hooks/useGramThreadListAdapter.tsx +101 -20
- package/src/hooks/useMCPTools.ts +1 -1
- package/src/index.ts +18 -0
- package/src/lib/messageConverter.ts +5 -0
- package/src/lib/tools.test.ts +24 -12
- package/dist/index-Dpk3C8VH.cjs +0 -194
- package/dist/index-Dpk3C8VH.cjs.map +0 -1
- package/dist/index-Em1Ot0b6.js.map +0 -1
|
@@ -15,6 +15,10 @@ import {
|
|
|
15
15
|
convertGramMessagesToUIMessages,
|
|
16
16
|
} from "@/lib/messageConverter";
|
|
17
17
|
import { sleep } from "@/lib/utils";
|
|
18
|
+
import {
|
|
19
|
+
ThreadMetaContext,
|
|
20
|
+
type ThreadMeta,
|
|
21
|
+
} from "@/contexts/ThreadMetaContext";
|
|
18
22
|
import {
|
|
19
23
|
useCallback,
|
|
20
24
|
useEffect,
|
|
@@ -108,6 +112,18 @@ interface ListChatsResponse {
|
|
|
108
112
|
chats: GramChatOverview[];
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Reads a chat's creation timestamp from the `chat.list` payload. The wire
|
|
117
|
+
* format is snake_case (`created_at`) — the hand-written GramChatOverview type
|
|
118
|
+
* declares camelCase, which never matched the JSON — so check both and return
|
|
119
|
+
* an ISO string, or undefined when absent.
|
|
120
|
+
*/
|
|
121
|
+
function readChatCreatedAt(chat: GramChatOverview): string | undefined {
|
|
122
|
+
const raw = chat as unknown as Record<string, unknown>;
|
|
123
|
+
const value = raw["created_at"] ?? raw["createdAt"];
|
|
124
|
+
return typeof value === "string" ? value : undefined;
|
|
125
|
+
}
|
|
126
|
+
|
|
111
127
|
/**
|
|
112
128
|
* Resolves request headers from the live adapter options: the async
|
|
113
129
|
* `getHeaders` (which waits for auth to settle) when provided, otherwise the
|
|
@@ -119,6 +135,54 @@ async function resolveAdapterHeaders(
|
|
|
119
135
|
return options.getHeaders ? options.getHeaders() : options.headers;
|
|
120
136
|
}
|
|
121
137
|
|
|
138
|
+
// chat.load paginates by seq keyset and returns only the newest page by default.
|
|
139
|
+
// assistant-ui's history adapter is one-shot (it imports the whole thread at
|
|
140
|
+
// once), so page backwards to the start and return the chat with every message
|
|
141
|
+
// in ascending order. Without this a long thread would render only its newest
|
|
142
|
+
// page with no way to reach the rest.
|
|
143
|
+
const FULL_LOAD_PAGE_SIZE = 200;
|
|
144
|
+
|
|
145
|
+
async function loadFullChat(
|
|
146
|
+
apiUrl: string,
|
|
147
|
+
chatId: string,
|
|
148
|
+
headers: Record<string, string>,
|
|
149
|
+
): Promise<GramChat | null> {
|
|
150
|
+
let beforeSeq: number | undefined;
|
|
151
|
+
let all: GramChatMessage[] = [];
|
|
152
|
+
let base: GramChat | null = null;
|
|
153
|
+
|
|
154
|
+
for (;;) {
|
|
155
|
+
const cursor = beforeSeq !== undefined ? `&before_seq=${beforeSeq}` : "";
|
|
156
|
+
const response = await fetch(
|
|
157
|
+
`${apiUrl}/rpc/chat.load?id=${encodeURIComponent(chatId)}&limit=${FULL_LOAD_PAGE_SIZE}${cursor}`,
|
|
158
|
+
{ headers },
|
|
159
|
+
);
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
// First page failed → nothing to show. A later page failing means we'd
|
|
162
|
+
// silently truncate the thread, so log loudly rather than passing partial
|
|
163
|
+
// history off as complete.
|
|
164
|
+
if (!base) {
|
|
165
|
+
console.error("Failed to load chat:", response.status);
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
console.error(
|
|
169
|
+
`Failed to load full chat history (status ${response.status}); ` +
|
|
170
|
+
`returning ${all.length} of ${base.numMessages ?? "?"} messages — transcript is truncated.`,
|
|
171
|
+
);
|
|
172
|
+
return { ...base, messages: all };
|
|
173
|
+
}
|
|
174
|
+
const page = (await response.json()) as GramChat;
|
|
175
|
+
if (!base) base = page;
|
|
176
|
+
// Each page is ascending; older pages prepend.
|
|
177
|
+
all = [...page.messages, ...all];
|
|
178
|
+
const oldest = page.messages[0];
|
|
179
|
+
if (!page.has_more_before || !oldest || oldest.seq === undefined) break;
|
|
180
|
+
beforeSeq = oldest.seq;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return base ? { ...base, messages: all } : null;
|
|
184
|
+
}
|
|
185
|
+
|
|
122
186
|
/**
|
|
123
187
|
* Thread history adapter that loads messages from Gram API.
|
|
124
188
|
* Note: We use `as ThreadHistoryAdapter` cast because the withFormat generic
|
|
@@ -172,17 +236,15 @@ class GramThreadHistoryAdapter {
|
|
|
172
236
|
}
|
|
173
237
|
|
|
174
238
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
239
|
+
const chat = await loadFullChat(
|
|
240
|
+
this.apiUrl,
|
|
241
|
+
remoteId,
|
|
242
|
+
await this.getHeaders(),
|
|
178
243
|
);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
console.error("Failed to load chat:", response.status);
|
|
244
|
+
if (!chat) {
|
|
245
|
+
console.error("Failed to load chat");
|
|
182
246
|
return { messages: [], headId: null };
|
|
183
247
|
}
|
|
184
|
-
|
|
185
|
-
const chat = (await response.json()) as GramChat;
|
|
186
248
|
return convertGramMessagesToExported(this.applyTransform(chat.messages));
|
|
187
249
|
} catch (error) {
|
|
188
250
|
console.error("Error loading chat:", error);
|
|
@@ -206,17 +268,15 @@ class GramThreadHistoryAdapter {
|
|
|
206
268
|
return { messages: [], headId: null };
|
|
207
269
|
}
|
|
208
270
|
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
271
|
+
const chat = await loadFullChat(
|
|
272
|
+
this.apiUrl,
|
|
273
|
+
remoteId,
|
|
274
|
+
await this.getHeaders(),
|
|
212
275
|
);
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
console.error("Failed to load chat (withFormat):", response.status);
|
|
276
|
+
if (!chat) {
|
|
277
|
+
console.error("Failed to load chat (withFormat)");
|
|
216
278
|
return { messages: [], headId: null };
|
|
217
279
|
}
|
|
218
|
-
|
|
219
|
-
const chat = (await response.json()) as GramChat;
|
|
220
280
|
return convertGramMessagesToUIMessages(
|
|
221
281
|
this.applyTransform(chat.messages),
|
|
222
282
|
);
|
|
@@ -293,6 +353,14 @@ export function useGramThreadListAdapter(
|
|
|
293
353
|
optionsRef.current = options;
|
|
294
354
|
}, [options]);
|
|
295
355
|
|
|
356
|
+
// Side channel for per-chat metadata (creation date) that assistant-ui's
|
|
357
|
+
// RemoteThreadMetadata can't carry. `list()` writes a fresh object into the
|
|
358
|
+
// ref and bumps the counter so the provider passes a new context value and
|
|
359
|
+
// ThreadListItem re-renders with the dates. A ref (not state) holds the data
|
|
360
|
+
// so the stable `list()` closure can write it without a re-created identity.
|
|
361
|
+
const metaRef = useRef<Record<string, ThreadMeta>>({});
|
|
362
|
+
const [, bumpMeta] = useState(0);
|
|
363
|
+
|
|
296
364
|
// Create stable Provider component using useCallback
|
|
297
365
|
const unstable_Provider = useCallback(function GramHistoryProvider({
|
|
298
366
|
children,
|
|
@@ -300,9 +368,11 @@ export function useGramThreadListAdapter(
|
|
|
300
368
|
const history = useGramThreadHistoryAdapter(optionsRef);
|
|
301
369
|
const adapters = useMemo(() => ({ history }), [history]);
|
|
302
370
|
return (
|
|
303
|
-
<
|
|
304
|
-
{
|
|
305
|
-
|
|
371
|
+
<ThreadMetaContext.Provider value={metaRef.current}>
|
|
372
|
+
<RuntimeAdapterProvider adapters={adapters}>
|
|
373
|
+
{children}
|
|
374
|
+
</RuntimeAdapterProvider>
|
|
375
|
+
</ThreadMetaContext.Provider>
|
|
306
376
|
);
|
|
307
377
|
}, []);
|
|
308
378
|
|
|
@@ -329,6 +399,15 @@ export function useGramThreadListAdapter(
|
|
|
329
399
|
}
|
|
330
400
|
|
|
331
401
|
const data = (await response.json()) as ListChatsResponse;
|
|
402
|
+
// Stash creation dates in the side channel before returning the
|
|
403
|
+
// assistant-ui metadata (which can't carry them) — keyed by chat id,
|
|
404
|
+
// the same value used for remoteId/externalId below.
|
|
405
|
+
const nextMeta: Record<string, ThreadMeta> = { ...metaRef.current };
|
|
406
|
+
for (const chat of data.chats) {
|
|
407
|
+
nextMeta[chat.id] = { createdAt: readChatCreatedAt(chat) };
|
|
408
|
+
}
|
|
409
|
+
metaRef.current = nextMeta;
|
|
410
|
+
bumpMeta((n) => n + 1);
|
|
332
411
|
return {
|
|
333
412
|
threads: data.chats.map((chat) => ({
|
|
334
413
|
remoteId: chat.id,
|
|
@@ -474,8 +553,10 @@ export function useGramThreadListAdapter(
|
|
|
474
553
|
}
|
|
475
554
|
|
|
476
555
|
try {
|
|
556
|
+
// Only chat metadata (id/title) is needed here, so request the
|
|
557
|
+
// smallest page instead of a full message page.
|
|
477
558
|
const response = await fetch(
|
|
478
|
-
`${optionsRef.current.apiUrl}/rpc/chat.load?id=${encodeURIComponent(threadId)}`,
|
|
559
|
+
`${optionsRef.current.apiUrl}/rpc/chat.load?id=${encodeURIComponent(threadId)}&limit=1`,
|
|
479
560
|
{
|
|
480
561
|
headers: await resolveAdapterHeaders(optionsRef.current),
|
|
481
562
|
},
|
package/src/hooks/useMCPTools.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { assert } from "@/lib/utils";
|
|
2
2
|
import type { MCPServerEntry } from "@/types";
|
|
3
3
|
import { ToolsFilter } from "@/types";
|
|
4
|
-
import {
|
|
4
|
+
import { createMCPClient } from "@ai-sdk/mcp";
|
|
5
5
|
import { useQuery, type UseQueryResult } from "@tanstack/react-query";
|
|
6
6
|
import { useMemo, useRef } from "react";
|
|
7
7
|
import { trackError } from "@/lib/errorTracking";
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,24 @@ export type { ShareButtonProps } from "@/components/ShareButton";
|
|
|
20
20
|
export { ToolFallback } from "@/components/assistant-ui/tool-fallback";
|
|
21
21
|
export { MessageContent } from "@/components/MessageContent";
|
|
22
22
|
export type { MessageContentProps } from "@/components/MessageContent";
|
|
23
|
+
export { Markdown } from "@/components/Markdown";
|
|
24
|
+
export type { MarkdownProps } from "@/components/Markdown";
|
|
25
|
+
|
|
26
|
+
// Static presentation primitives — render with no ElementsProvider/runtime, so
|
|
27
|
+
// the dashboard's chat detail panel can reuse the elements tool UI directly.
|
|
28
|
+
export {
|
|
29
|
+
ToolUI,
|
|
30
|
+
ToolUISection,
|
|
31
|
+
SyntaxHighlightedCode,
|
|
32
|
+
} from "@/components/ui/tool-ui";
|
|
33
|
+
export type {
|
|
34
|
+
ToolUIProps,
|
|
35
|
+
ToolUISectionProps,
|
|
36
|
+
ToolStatus,
|
|
37
|
+
ContentItem,
|
|
38
|
+
SectionHighlight,
|
|
39
|
+
SectionMatch,
|
|
40
|
+
} from "@/components/ui/tool-ui";
|
|
23
41
|
|
|
24
42
|
// Replay
|
|
25
43
|
export { Replay } from "@/components/Replay";
|
|
@@ -71,6 +71,8 @@ export type GramChatContent = string | GramChatContentPart[];
|
|
|
71
71
|
*/
|
|
72
72
|
export interface GramChatMessage {
|
|
73
73
|
id: string;
|
|
74
|
+
// Monotonic sequence number; used as the keyset cursor when paging chat.load.
|
|
75
|
+
seq?: number;
|
|
74
76
|
model: string;
|
|
75
77
|
created_at: Date | string;
|
|
76
78
|
role: "system" | "developer" | "user" | "assistant" | "tool";
|
|
@@ -92,6 +94,9 @@ export interface GramChat {
|
|
|
92
94
|
messages: GramChatMessage[];
|
|
93
95
|
createdAt: Date | string;
|
|
94
96
|
updatedAt: Date | string;
|
|
97
|
+
// chat.load paginates by seq keyset; true when older messages remain before
|
|
98
|
+
// the first message in `messages` (snake_case to match the wire payload).
|
|
99
|
+
has_more_before?: boolean;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
/**
|
package/src/lib/tools.test.ts
CHANGED
|
@@ -11,12 +11,12 @@ import {
|
|
|
11
11
|
type UIMessage,
|
|
12
12
|
type UIMessagePart,
|
|
13
13
|
} from "ai";
|
|
14
|
-
import {
|
|
14
|
+
import { MockLanguageModelV3 } from "ai/test";
|
|
15
15
|
|
|
16
16
|
type MockStream = Extract<
|
|
17
17
|
NonNullable<
|
|
18
18
|
NonNullable<
|
|
19
|
-
ConstructorParameters<typeof
|
|
19
|
+
ConstructorParameters<typeof MockLanguageModelV3>[0]
|
|
20
20
|
>["doStream"]
|
|
21
21
|
>,
|
|
22
22
|
(...a: never[]) => PromiseLike<{ stream: ReadableStream<unknown> }>
|
|
@@ -70,8 +70,16 @@ function toolCallChunks(opts: {
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
type: "finish",
|
|
73
|
-
finishReason: "tool-calls",
|
|
74
|
-
usage: {
|
|
73
|
+
finishReason: { unified: "tool-calls", raw: undefined },
|
|
74
|
+
usage: {
|
|
75
|
+
inputTokens: {
|
|
76
|
+
total: 1,
|
|
77
|
+
noCache: 1,
|
|
78
|
+
cacheRead: undefined,
|
|
79
|
+
cacheWrite: undefined,
|
|
80
|
+
},
|
|
81
|
+
outputTokens: { total: 1, text: 1, reasoning: undefined },
|
|
82
|
+
},
|
|
75
83
|
},
|
|
76
84
|
];
|
|
77
85
|
}
|
|
@@ -118,7 +126,7 @@ async function streamToolCallOnly(toolCallId: string): Promise<UIMessage[]> {
|
|
|
118
126
|
},
|
|
119
127
|
} as unknown as ToolSet;
|
|
120
128
|
|
|
121
|
-
const model = new
|
|
129
|
+
const model = new MockLanguageModelV3({
|
|
122
130
|
doStream: async () => ({
|
|
123
131
|
stream: makeStream([
|
|
124
132
|
...toolCallChunks({
|
|
@@ -174,10 +182,13 @@ describe("frontend tool Skip flow (sendAutomaticallyWhen fix)", () => {
|
|
|
174
182
|
false,
|
|
175
183
|
);
|
|
176
184
|
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
185
|
+
// The resulting model-message sequence has an assistant `tool-call` with no
|
|
186
|
+
// matching tool result. (In ai v5 `convertToModelMessages` fabricated a
|
|
187
|
+
// bogus empty `role: "tool"` message here; ai v6 instead drops the
|
|
188
|
+
// unresolved call entirely and the next turn is the follow-up user message.)
|
|
189
|
+
// Either way the sequence is invalid — a tool-call with no result — which is
|
|
190
|
+
// exactly the state the `sendAutomaticallyWhen` fix prevents.
|
|
191
|
+
const modelMsgs = await convertToModelMessages(follow);
|
|
181
192
|
const assistantIdx = modelMsgs.findIndex(
|
|
182
193
|
(m) =>
|
|
183
194
|
m.role === "assistant" &&
|
|
@@ -187,8 +198,9 @@ describe("frontend tool Skip flow (sendAutomaticallyWhen fix)", () => {
|
|
|
187
198
|
),
|
|
188
199
|
);
|
|
189
200
|
expect(assistantIdx).toBeGreaterThanOrEqual(0);
|
|
190
|
-
|
|
191
|
-
expect(modelMsgs
|
|
201
|
+
// No valid tool result was produced for the unresolved tool-call.
|
|
202
|
+
expect(modelMsgs.some((m) => m.role === "tool")).toBe(false);
|
|
203
|
+
expect(modelMsgs[assistantIdx + 1]?.role).not.toBe("tool");
|
|
192
204
|
});
|
|
193
205
|
|
|
194
206
|
it("once the tool-result is patched onto the message, sendAutomaticallyWhen fires and the sequence is valid", async () => {
|
|
@@ -237,7 +249,7 @@ describe("frontend tool Skip flow (sendAutomaticallyWhen fix)", () => {
|
|
|
237
249
|
|
|
238
250
|
// And the sequence handed to the model is well-formed (assistant
|
|
239
251
|
// tool-call is followed by a real role:"tool" message with a result).
|
|
240
|
-
const modelMsgs = convertToModelMessages(patched);
|
|
252
|
+
const modelMsgs = await convertToModelMessages(patched);
|
|
241
253
|
const assistantIdx = modelMsgs.findIndex(
|
|
242
254
|
(m) =>
|
|
243
255
|
m.role === "assistant" &&
|