@parallelworks/ui 0.83.1 → 0.83.3
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/ai/ai.css +16 -0
- package/dist/ai/components/ChatInput.js +1 -1
- package/dist/ai/components/ChatMessage.js +6 -1
- package/dist/ai/components/ChatMessageList.js +26 -6
- package/dist/ai/components/agent/AgentMessageParts.d.ts +6 -0
- package/dist/ai/components/agent/AgentMessageParts.js +93 -3
- package/dist/ai/core/ChatProvider.d.ts +3 -0
- package/dist/ai/core/ChatProvider.js +14 -4
- package/dist/ai/core/parts.d.ts +3 -2
- package/dist/ai/core/parts.js +47 -24
- package/dist/ai/strings.d.ts +6 -0
- package/dist/ai/strings.js +3 -0
- package/dist/components/MarkdownImpl.js +3 -1
- package/dist/styles.css +1 -1
- package/package.json +1 -1
package/dist/ai/ai.css
CHANGED
|
@@ -347,6 +347,22 @@
|
|
|
347
347
|
0 10px 24px -10px rgb(0 0 0 / 0.18);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
/* The thread scrolls under the dock and the jump pill, so they must hide it
|
|
351
|
+
* even when a theme makes the panel color translucent: a second layer of the
|
|
352
|
+
* same color leaves the thread at most a hundredth visible. */
|
|
353
|
+
.chat-composer-dock,
|
|
354
|
+
.chat-jump {
|
|
355
|
+
background:
|
|
356
|
+
linear-gradient(var(--theme-panel-bg, #fff), var(--theme-panel-bg, #fff)),
|
|
357
|
+
var(--theme-panel-bg, #fff);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.chat-jump:hover {
|
|
361
|
+
background:
|
|
362
|
+
linear-gradient(var(--theme-muted-panel-bg, #f3f4f6), var(--theme-muted-panel-bg, #f3f4f6)),
|
|
363
|
+
var(--theme-muted-panel-bg, #f3f4f6);
|
|
364
|
+
}
|
|
365
|
+
|
|
350
366
|
/* The thread runs under the docked composer and fades out above it, so the
|
|
351
367
|
* last lines dissolve rather than cut off at the dock's edge. */
|
|
352
368
|
.chat-composer-dock::before {
|
|
@@ -204,7 +204,7 @@ function ChatInput({ onSend, disabled = false, placeholder, conversationId, onEd
|
|
|
204
204
|
testId: sendTestId
|
|
205
205
|
});
|
|
206
206
|
return /* @__PURE__ */ jsxs("div", {
|
|
207
|
-
className: "chat-composer-dock relative
|
|
207
|
+
className: "chat-composer-dock relative px-4 pt-2 pb-5",
|
|
208
208
|
...attachmentsAvailable ? dragHandlers : {},
|
|
209
209
|
children: [isDragging && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl" }), /* @__PURE__ */ jsxs("div", {
|
|
210
210
|
className: "mx-auto w-full max-w-[var(--chat-column,50rem)] space-y-2 sm:px-4",
|
|
@@ -9,6 +9,7 @@ import { CopyButton } from "../../components/CopyButton.js";
|
|
|
9
9
|
import { formatDuration } from "../../format.js";
|
|
10
10
|
import { RelativeTime } from "../../components/relativeTime.js";
|
|
11
11
|
import { useChatConfig } from "../core/config.js";
|
|
12
|
+
import { useChatModelName } from "../core/ChatProvider.js";
|
|
12
13
|
import Markdown from "../ui/Markdown.js";
|
|
13
14
|
import { chatProseClasses } from "../ui/prose.js";
|
|
14
15
|
import { Reasoning } from "./Reasoning.js";
|
|
@@ -93,11 +94,15 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
93
94
|
};
|
|
94
95
|
const timestamp = parseTimestamp(message.timestamp);
|
|
95
96
|
const totalTokens = message.tokensUsed?.total_tokens;
|
|
97
|
+
const modelName = useChatModelName(isUser ? void 0 : message.model);
|
|
96
98
|
const meta = (timestamp || !isUser && (message.model || totalTokens)) && /* @__PURE__ */ jsxs("span", {
|
|
97
99
|
className: cx("chat-meta flex items-center gap-2 ml-2 text-xs tabular-nums theme-muted-text transition-opacity duration-150 select-none", "opacity-0 group-focus-within:opacity-100 group-hover:opacity-100"),
|
|
98
100
|
children: [
|
|
99
101
|
/* @__PURE__ */ jsx(RelativeTime, { value: timestamp }),
|
|
100
|
-
!isUser && message.model && /* @__PURE__ */ jsx("span", {
|
|
102
|
+
!isUser && message.model && /* @__PURE__ */ jsx("span", {
|
|
103
|
+
title: modelName ? message.model : void 0,
|
|
104
|
+
children: modelName ?? message.model
|
|
105
|
+
}),
|
|
101
106
|
!isUser && totalTokens ? /* @__PURE__ */ jsx("span", {
|
|
102
107
|
title: message.tokensUsed?.prompt_tokens !== void 0 && message.tokensUsed?.completion_tokens !== void 0 ? t.tokensBreakdown(message.tokensUsed.prompt_tokens, message.tokensUsed.completion_tokens) : void 0,
|
|
103
108
|
children: t.tokens(totalTokens)
|
|
@@ -12,6 +12,8 @@ import ChatMessage_default from "./ChatMessage.js";
|
|
|
12
12
|
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
13
13
|
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
14
14
|
//#region src/ai/components/ChatMessageList.tsx
|
|
15
|
+
var READER_GROWTH_MS = 600;
|
|
16
|
+
var jumpClasses = "chat-composer chat-jump rounded-full px-3 py-1.5 text-xs font-medium theme-text transition-colors";
|
|
15
17
|
function ChatMessageList({ messages, allMessages, streamingMessage: streamingMessageProp, queuedMessages, onRemoveQueued, canRemoveQueued, queueDelivery = "afterReply", editingMessageId, showAuthor, currentUsername, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer, inputElement, isStreaming: isStreamingProp, flush, label, footer, workingIndicator = true }) {
|
|
16
18
|
const { adapter, isStreaming: providerStreaming } = useChat();
|
|
17
19
|
const { isThinking, streamingContent, streamingReasoning, streamingParts, thinkingStartTime } = useChatStream();
|
|
@@ -154,15 +156,25 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
154
156
|
useEffect(() => {
|
|
155
157
|
const content = contentRef.current;
|
|
156
158
|
if (!content || typeof ResizeObserver === "undefined") return;
|
|
159
|
+
let touchedAt = Number.NEGATIVE_INFINITY;
|
|
160
|
+
const touched = () => {
|
|
161
|
+
touchedAt = performance.now();
|
|
162
|
+
};
|
|
163
|
+
content.addEventListener("click", touched, true);
|
|
164
|
+
content.addEventListener("keydown", touched, true);
|
|
157
165
|
const observer = new ResizeObserver(() => {
|
|
158
166
|
const el = containerRef.current;
|
|
159
|
-
if (el && isNearBottomRef.current && !hasQueuedMessages) {
|
|
167
|
+
if (el && isNearBottomRef.current && !hasQueuedMessages && performance.now() - touchedAt > READER_GROWTH_MS) {
|
|
160
168
|
el.scrollTop = el.scrollHeight;
|
|
161
169
|
lastScrollTopRef.current = el.scrollTop;
|
|
162
170
|
}
|
|
163
171
|
});
|
|
164
172
|
observer.observe(content);
|
|
165
|
-
return () =>
|
|
173
|
+
return () => {
|
|
174
|
+
observer.disconnect();
|
|
175
|
+
content.removeEventListener("click", touched, true);
|
|
176
|
+
content.removeEventListener("keydown", touched, true);
|
|
177
|
+
};
|
|
166
178
|
}, [hasContent, hasQueuedMessages]);
|
|
167
179
|
if (messages.length === 0 && !streamingMessage && !hasQueuedMessages) return /* @__PURE__ */ jsxs("div", {
|
|
168
180
|
className: "chat-thread flex-1 overflow-y-auto flex flex-col bg-(--theme-panel-bg)",
|
|
@@ -304,18 +316,26 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
304
316
|
/* @__PURE__ */ jsx("div", { className: "h-8" })
|
|
305
317
|
]
|
|
306
318
|
}),
|
|
307
|
-
!pinned && /* @__PURE__ */ jsx("div", {
|
|
319
|
+
!pinned && !inputElement && /* @__PURE__ */ jsx("div", {
|
|
308
320
|
className: "sticky bottom-4 z-10 flex h-0 items-start justify-center overflow-visible",
|
|
309
321
|
children: /* @__PURE__ */ jsx("button", {
|
|
310
322
|
type: "button",
|
|
311
323
|
onClick: jumpToLatest,
|
|
312
|
-
className: "
|
|
324
|
+
className: cx(jumpClasses, "-translate-y-full"),
|
|
313
325
|
children: strings.chrome.jumpToLatest
|
|
314
326
|
})
|
|
315
327
|
}),
|
|
316
|
-
inputElement && /* @__PURE__ */
|
|
328
|
+
inputElement && /* @__PURE__ */ jsxs("div", {
|
|
317
329
|
className: "sticky bottom-0 z-10",
|
|
318
|
-
children:
|
|
330
|
+
children: [!pinned && /* @__PURE__ */ jsx("div", {
|
|
331
|
+
className: "pointer-events-none absolute inset-x-0 bottom-full flex justify-center pb-2",
|
|
332
|
+
children: /* @__PURE__ */ jsx("button", {
|
|
333
|
+
type: "button",
|
|
334
|
+
onClick: jumpToLatest,
|
|
335
|
+
className: cx(jumpClasses, "pointer-events-auto"),
|
|
336
|
+
children: strings.chrome.jumpToLatest
|
|
337
|
+
})
|
|
338
|
+
}), inputElement]
|
|
319
339
|
})
|
|
320
340
|
]
|
|
321
341
|
});
|
|
@@ -19,6 +19,12 @@ type RenderItem = {
|
|
|
19
19
|
} | {
|
|
20
20
|
kind: 'rollup';
|
|
21
21
|
members: RunMember[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: 'calls';
|
|
24
|
+
members: {
|
|
25
|
+
part: ToolCallPart;
|
|
26
|
+
index: number;
|
|
27
|
+
}[];
|
|
22
28
|
};
|
|
23
29
|
export declare function buildRenderItems(parts: MessagePart[], hostDrawn?: (toolName: string) => boolean): RenderItem[];
|
|
24
30
|
export default function AgentMessageParts({ message, isStreaming, onApprovalAnswer, }: {
|
|
@@ -282,12 +282,16 @@ function ToolCallBlock({ part, children }) {
|
|
|
282
282
|
children: fullToolArgs(header.arg, part.args)
|
|
283
283
|
}), /* @__PURE__ */ jsx(ResultBody, { part })]
|
|
284
284
|
}),
|
|
285
|
+
part.status === "error" && !expanded && /* @__PURE__ */ jsx("div", {
|
|
286
|
+
className: "ml-5 mt-0.5 truncate text-[13px] text-(--theme-error-text)",
|
|
287
|
+
children: t.failedWith(summary)
|
|
288
|
+
}),
|
|
285
289
|
children && /* @__PURE__ */ jsx("div", {
|
|
286
290
|
className: "mt-1.5 min-w-0 sm:ml-5",
|
|
287
291
|
children
|
|
288
292
|
}),
|
|
289
|
-
!children && !isDiff && !expanded && part.status !== "waiting" && /* @__PURE__ */ jsx("div", {
|
|
290
|
-
className:
|
|
293
|
+
!children && !isDiff && !expanded && part.status !== "waiting" && part.status !== "error" && /* @__PURE__ */ jsx("div", {
|
|
294
|
+
className: "ml-5 mt-0.5 truncate text-[13px] theme-muted-text",
|
|
291
295
|
children: summary
|
|
292
296
|
}),
|
|
293
297
|
isDiff && /* @__PURE__ */ jsx("div", {
|
|
@@ -867,7 +871,36 @@ function buildRenderItems(parts, hostDrawn = () => false) {
|
|
|
867
871
|
});
|
|
868
872
|
});
|
|
869
873
|
flush();
|
|
870
|
-
return items;
|
|
874
|
+
return groupRepeatedCalls(items);
|
|
875
|
+
}
|
|
876
|
+
/** A call that can share a row with the same tool's calls beside it: never one
|
|
877
|
+
* still running, so its progress stays in view, one waiting on an answer, or
|
|
878
|
+
* one whose decision line names who answered it. */
|
|
879
|
+
function foldableCall(item) {
|
|
880
|
+
return item.kind === "single" && item.part.kind === "tool_call" && (item.part.status === "ok" || item.part.status === "error") && !item.part.decision ? item.part : null;
|
|
881
|
+
}
|
|
882
|
+
function groupRepeatedCalls(items) {
|
|
883
|
+
const out = [];
|
|
884
|
+
let i = 0;
|
|
885
|
+
while (i < items.length) {
|
|
886
|
+
const item = items[i];
|
|
887
|
+
const first = foldableCall(item);
|
|
888
|
+
let j = i + 1;
|
|
889
|
+
while (first && j < items.length && foldableCall(items[j])?.name === first.name) j++;
|
|
890
|
+
if (j - i >= 2) out.push({
|
|
891
|
+
kind: "calls",
|
|
892
|
+
members: items.slice(i, j).map((m) => {
|
|
893
|
+
const single = m;
|
|
894
|
+
return {
|
|
895
|
+
part: single.part,
|
|
896
|
+
index: single.index
|
|
897
|
+
};
|
|
898
|
+
})
|
|
899
|
+
});
|
|
900
|
+
else out.push(item);
|
|
901
|
+
i = j;
|
|
902
|
+
}
|
|
903
|
+
return out;
|
|
871
904
|
}
|
|
872
905
|
function RollupGroup({ members, message }) {
|
|
873
906
|
const { strings } = useChatConfig();
|
|
@@ -894,6 +927,60 @@ function RollupGroup({ members, message }) {
|
|
|
894
927
|
})
|
|
895
928
|
});
|
|
896
929
|
}
|
|
930
|
+
/** Calls to one tool in a row, folded into one line that counts them and their
|
|
931
|
+
* failures. Folded, it still shows what a host drew for each call, such as a
|
|
932
|
+
* table, so results stay in view; calls with nothing drawn show nothing. */
|
|
933
|
+
function CallGroup({ members, message }) {
|
|
934
|
+
const { strings, slots } = useChatConfig();
|
|
935
|
+
const t = strings.agentTranscript;
|
|
936
|
+
const [open, setOpen] = useState(false);
|
|
937
|
+
const name = members[0]?.part.name ?? "";
|
|
938
|
+
const Tool = slots.toolRenderers?.[name];
|
|
939
|
+
const header = toolHeaderFor(name);
|
|
940
|
+
const failed = members.filter((m) => m.part.status === "error").length;
|
|
941
|
+
const status = failed === members.length ? "error" : "ok";
|
|
942
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
943
|
+
className: "py-1.5 text-sm",
|
|
944
|
+
"data-testid": "tool-call-group",
|
|
945
|
+
children: [/* @__PURE__ */ jsx(Disclosure, {
|
|
946
|
+
open,
|
|
947
|
+
onOpenChange: setOpen,
|
|
948
|
+
keepMounted: true,
|
|
949
|
+
summaryClassName: rowSummaryClasses,
|
|
950
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ToolStatusDot, { part: {
|
|
951
|
+
...members[0]?.part,
|
|
952
|
+
status
|
|
953
|
+
} }), /* @__PURE__ */ jsxs("span", {
|
|
954
|
+
className: "font-mono text-[13px] leading-5 min-w-0 truncate",
|
|
955
|
+
children: [t.callGroup(header.label, members.length), failed > 0 && /* @__PURE__ */ jsxs("span", {
|
|
956
|
+
className: "text-(--theme-error-text)",
|
|
957
|
+
children: [" · ", t.failedCount(failed)]
|
|
958
|
+
})]
|
|
959
|
+
})] }),
|
|
960
|
+
panelClassName: "ml-1.5 mt-0.5 rounded-md bg-(--theme-muted-panel-bg)/40 px-2",
|
|
961
|
+
children: members.map((m) => /* @__PURE__ */ jsx(PartDispatch, {
|
|
962
|
+
part: m.part,
|
|
963
|
+
message
|
|
964
|
+
}, m.index))
|
|
965
|
+
}), !open && Tool && members.map(({ part, index }) => {
|
|
966
|
+
const args = summarizeToolArgs(header.arg, part.args);
|
|
967
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
968
|
+
className: "mt-1.5 min-w-0 sm:ml-5 has-[>[data-drawing]:empty]:hidden",
|
|
969
|
+
children: [args && /* @__PURE__ */ jsx("div", {
|
|
970
|
+
className: "truncate font-mono text-xs theme-muted-text",
|
|
971
|
+
children: args
|
|
972
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
973
|
+
"data-drawing": true,
|
|
974
|
+
className: "mt-1",
|
|
975
|
+
children: /* @__PURE__ */ jsx(Tool, {
|
|
976
|
+
part,
|
|
977
|
+
message
|
|
978
|
+
})
|
|
979
|
+
})]
|
|
980
|
+
}, index);
|
|
981
|
+
})]
|
|
982
|
+
});
|
|
983
|
+
}
|
|
897
984
|
function AgentMessageParts({ message, isStreaming, onApprovalAnswer }) {
|
|
898
985
|
const parts = message.parts ?? [];
|
|
899
986
|
const { slots } = useChatConfig();
|
|
@@ -906,6 +993,9 @@ function AgentMessageParts({ message, isStreaming, onApprovalAnswer }) {
|
|
|
906
993
|
children: items.map((item) => item.kind === "rollup" ? /* @__PURE__ */ jsx(RollupGroup, {
|
|
907
994
|
members: item.members,
|
|
908
995
|
message
|
|
996
|
+
}, item.members[0]?.index ?? 0) : item.kind === "calls" ? /* @__PURE__ */ jsx(CallGroup, {
|
|
997
|
+
members: item.members,
|
|
998
|
+
message
|
|
909
999
|
}, item.members[0]?.index ?? 0) : item.kind === "reasoning" ? /* @__PURE__ */ jsx(ReasoningBlock, {
|
|
910
1000
|
part: mergeReasoning(item.parts),
|
|
911
1001
|
isStreaming: (isStreaming ?? false) && item.index + item.parts.length - 1 === lastIndex
|
|
@@ -69,6 +69,9 @@ export interface ChatStream {
|
|
|
69
69
|
streamingParts: MessagePart[];
|
|
70
70
|
thinkingStartTime: number | null;
|
|
71
71
|
}
|
|
72
|
+
/** A model's display name by its id, when the adapter listed one; undefined
|
|
73
|
+
* outside a ChatProvider or for a model it doesn't list. */
|
|
74
|
+
export declare function useChatModelName(id: string | null | undefined): string | undefined;
|
|
72
75
|
export declare function useChat(): ChatContextValue;
|
|
73
76
|
/** True while the adapter lists tools of its own, which the composer's +
|
|
74
77
|
* menu offers; false outside a ChatProvider. */
|
|
@@ -61,6 +61,13 @@ function historyMessages(messages, selectedProvider) {
|
|
|
61
61
|
var NO_TOOLS = [];
|
|
62
62
|
var ChatContext = createContext(void 0);
|
|
63
63
|
var ChatStreamContext = createContext(void 0);
|
|
64
|
+
var ChatModelsContext = createContext([]);
|
|
65
|
+
/** A model's display name by its id, when the adapter listed one; undefined
|
|
66
|
+
* outside a ChatProvider or for a model it doesn't list. */
|
|
67
|
+
function useChatModelName(id) {
|
|
68
|
+
const models = useContext(ChatModelsContext);
|
|
69
|
+
return id ? models.find((m) => m.id === id)?.name || void 0 : void 0;
|
|
70
|
+
}
|
|
64
71
|
function useChat() {
|
|
65
72
|
const context = useContext(ChatContext);
|
|
66
73
|
if (!context) throw new Error("useChat must be used within a ChatProvider");
|
|
@@ -984,13 +991,16 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
984
991
|
value: reportSidebarPresentation,
|
|
985
992
|
children: /* @__PURE__ */ jsx(ChatContext.Provider, {
|
|
986
993
|
value,
|
|
987
|
-
children: /* @__PURE__ */ jsx(
|
|
988
|
-
value:
|
|
989
|
-
children
|
|
994
|
+
children: /* @__PURE__ */ jsx(ChatModelsContext.Provider, {
|
|
995
|
+
value: state.models,
|
|
996
|
+
children: /* @__PURE__ */ jsx(ChatStreamContext.Provider, {
|
|
997
|
+
value: stream,
|
|
998
|
+
children
|
|
999
|
+
})
|
|
990
1000
|
})
|
|
991
1001
|
})
|
|
992
1002
|
})
|
|
993
1003
|
});
|
|
994
1004
|
}
|
|
995
1005
|
//#endregion
|
|
996
|
-
export { ChatProvider, useChat, useChatStream, useHasChatTools };
|
|
1006
|
+
export { ChatProvider, useChat, useChatModelName, useChatStream, useHasChatTools };
|
package/dist/ai/core/parts.d.ts
CHANGED
|
@@ -7,8 +7,9 @@ import type { MessagePart } from '../types.js';
|
|
|
7
7
|
* rather than invented — an end without a start has nothing to render. */
|
|
8
8
|
export declare function applyPartDelta(parts: MessagePart[], delta: PartDelta): MessagePart[];
|
|
9
9
|
/** The wire history for an assistant turn whose tool calls the gateway ran:
|
|
10
|
-
*
|
|
11
|
-
* calls comes back as the
|
|
10
|
+
* each round's calls, with the text that came before them, and each result,
|
|
11
|
+
* then the answer. A turn with no finished tool calls comes back as the
|
|
12
|
+
* answer alone. */
|
|
12
13
|
export declare function toolTurnMessages(parts: MessagePart[] | null | undefined, answer: CompletionMessage): CompletionMessage[];
|
|
13
14
|
/** A turn can end (completed or stopped) with tool calls still marked
|
|
14
15
|
* running — the transport died before their tool_end arrived. A stored
|
package/dist/ai/core/parts.js
CHANGED
|
@@ -99,31 +99,54 @@ function isStreamedText(part) {
|
|
|
99
99
|
return part?.kind === "text" || part?.kind === "reasoning";
|
|
100
100
|
}
|
|
101
101
|
/** The wire history for an assistant turn whose tool calls the gateway ran:
|
|
102
|
-
*
|
|
103
|
-
* calls comes back as the
|
|
102
|
+
* each round's calls, with the text that came before them, and each result,
|
|
103
|
+
* then the answer. A turn with no finished tool calls comes back as the
|
|
104
|
+
* answer alone. */
|
|
104
105
|
function toolTurnMessages(parts, answer) {
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
106
|
+
const rounds = [];
|
|
107
|
+
let text = "";
|
|
108
|
+
let calls = [];
|
|
109
|
+
let hasText = false;
|
|
110
|
+
for (const p of parts ?? []) if (p.kind === "text") {
|
|
111
|
+
hasText = true;
|
|
112
|
+
if (calls.length > 0) {
|
|
113
|
+
rounds.push({
|
|
114
|
+
text,
|
|
115
|
+
calls
|
|
116
|
+
});
|
|
117
|
+
text = "";
|
|
118
|
+
calls = [];
|
|
119
|
+
}
|
|
120
|
+
text += p.text;
|
|
121
|
+
} else if (p.kind === "tool_call" && p.status !== "running" && p.result !== void 0) calls.push(p);
|
|
122
|
+
if (calls.length > 0) {
|
|
123
|
+
rounds.push({
|
|
124
|
+
text,
|
|
125
|
+
calls
|
|
126
|
+
});
|
|
127
|
+
text = "";
|
|
128
|
+
}
|
|
129
|
+
if (rounds.length === 0) return answer.content || answer.tool_calls?.length ? [answer] : [];
|
|
130
|
+
const final = hasText ? {
|
|
131
|
+
...answer,
|
|
132
|
+
content: text || null
|
|
133
|
+
} : answer;
|
|
134
|
+
return [...rounds.flatMap((round) => [{
|
|
135
|
+
role: "assistant",
|
|
136
|
+
content: round.text || null,
|
|
137
|
+
tool_calls: round.calls.map((c) => ({
|
|
138
|
+
id: c.id,
|
|
139
|
+
type: "function",
|
|
140
|
+
function: {
|
|
141
|
+
name: c.name,
|
|
142
|
+
arguments: c.args
|
|
143
|
+
}
|
|
144
|
+
}))
|
|
145
|
+
}, ...round.calls.map((c) => ({
|
|
146
|
+
role: "tool",
|
|
147
|
+
tool_call_id: c.id,
|
|
148
|
+
content: c.result
|
|
149
|
+
}))]), ...final.content ? [final] : []];
|
|
127
150
|
}
|
|
128
151
|
/** A turn can end (completed or stopped) with tool calls still marked
|
|
129
152
|
* running — the transport died before their tool_end arrived. A stored
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -226,6 +226,12 @@ export interface ChatStrings {
|
|
|
226
226
|
running: string;
|
|
227
227
|
done: string;
|
|
228
228
|
failed: string;
|
|
229
|
+
/** A failed call's row, with the first line of its error. */
|
|
230
|
+
failedWith: (reason: string) => string;
|
|
231
|
+
/** Consecutive calls to one tool, folded into one row. */
|
|
232
|
+
callGroup: (tool: string, count: number) => string;
|
|
233
|
+
/** Beside a folded row: how many of its calls failed. */
|
|
234
|
+
failedCount: (count: number) => string;
|
|
229
235
|
interrupted: string;
|
|
230
236
|
noOutput: string;
|
|
231
237
|
diffTruncated: string;
|
package/dist/ai/strings.js
CHANGED
|
@@ -241,6 +241,9 @@ var defaultChatStrings = {
|
|
|
241
241
|
running: "Running…",
|
|
242
242
|
done: "Done",
|
|
243
243
|
failed: "Failed",
|
|
244
|
+
failedWith: (reason) => `Failed: ${reason}`,
|
|
245
|
+
callGroup: (tool, count) => `Ran ${tool} ${countOf(count, "time", "times")}`,
|
|
246
|
+
failedCount: (count) => `${count} failed`,
|
|
244
247
|
interrupted: "Interrupted",
|
|
245
248
|
noOutput: "(no output)",
|
|
246
249
|
diffTruncated: "Diff truncated",
|
|
@@ -10,7 +10,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
10
10
|
import { code } from "@streamdown/code";
|
|
11
11
|
import { math } from "@streamdown/math";
|
|
12
12
|
import "katex/dist/katex.min.css";
|
|
13
|
-
import { Streamdown, defaultComponents, defaultRehypePlugins, useIsCodeFenceIncomplete } from "streamdown";
|
|
13
|
+
import { Streamdown, defaultComponents, defaultRehypePlugins, defaultRemarkPlugins, useIsCodeFenceIncomplete } from "streamdown";
|
|
14
14
|
//#region src/components/MarkdownImpl.tsx
|
|
15
15
|
var basePlugins = {
|
|
16
16
|
code,
|
|
@@ -35,6 +35,7 @@ function loadMermaid() {
|
|
|
35
35
|
return mermaidLoad;
|
|
36
36
|
}
|
|
37
37
|
var rehypePlugins = [...Object.values(defaultRehypePlugins), rehypeCodeBreaks];
|
|
38
|
+
var remarkPlugins = Object.entries(defaultRemarkPlugins).map(([name, plugin]) => name === "gfm" ? [Array.isArray(plugin) ? plugin[0] : plugin, { singleTilde: false }] : plugin);
|
|
38
39
|
var httpsImagesRehypePlugins = [...Object.entries(defaultRehypePlugins).flatMap(([name, plugin]) => name === "harden" ? [rehypeHttpsImagesOnly, plugin] : [plugin]), rehypeCodeBreaks];
|
|
39
40
|
function codeControls(controls) {
|
|
40
41
|
if (controls === false) return {
|
|
@@ -146,6 +147,7 @@ function MarkdownImpl({ children, isStreaming, controls, components, httpsImages
|
|
|
146
147
|
} : {},
|
|
147
148
|
...controls ? { controls } : {},
|
|
148
149
|
plugins,
|
|
150
|
+
remarkPlugins,
|
|
149
151
|
rehypePlugins: httpsImagesOnly ? httpsImagesRehypePlugins : rehypePlugins,
|
|
150
152
|
shikiTheme: CODE_THEMES,
|
|
151
153
|
components: {
|