@parallelworks/ui 0.83.3 → 0.84.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/ai/adapter/openai/createOpenAIStream.js +1 -1
- package/dist/ai/adapter/openai/sse.d.ts +3 -2
- package/dist/ai/adapter/openai/sse.js +4 -2
- package/dist/ai/adapter/types.d.ts +2 -1
- package/dist/ai/agent/index.d.ts +1 -1
- package/dist/ai/agent/toolfmt.d.ts +1 -0
- package/dist/ai/agent/toolfmt.js +6 -2
- package/dist/ai/ai.css +14 -0
- package/dist/ai/components/ChatActivity.js +6 -4
- package/dist/ai/components/ChatMessage.js +42 -30
- package/dist/ai/components/ChatMessageList.js +17 -9
- package/dist/ai/components/agent/AgentMessageParts.d.ts +1 -1
- package/dist/ai/components/agent/AgentMessageParts.js +77 -23
- package/dist/ai/core/ChatProvider.d.ts +3 -1
- package/dist/ai/core/ChatProvider.js +27 -6
- package/dist/ai/core/chatReducer.d.ts +6 -1
- package/dist/ai/core/chatReducer.js +18 -1
- package/dist/ai/core/config.d.ts +2 -1
- package/dist/ai/core/parts.d.ts +7 -4
- package/dist/ai/core/parts.js +23 -2
- package/dist/ai/core/useSSEStream.d.ts +8 -0
- package/dist/ai/core/useSSEStream.js +80 -22
- package/dist/ai/index.js +2 -2
- package/dist/ai/types.d.ts +14 -0
- package/dist/components/Avatar.d.ts +4 -2
- package/dist/components/Avatar.js +2 -1
- package/dist/components/Kbd.d.ts +5 -2
- package/dist/components/Kbd.js +4 -1
- package/dist/components/LetterBadge.d.ts +7 -3
- package/dist/components/LetterBadge.js +2 -3
- package/dist/components/MarkdownImpl.d.ts +3 -0
- package/dist/components/MarkdownImpl.js +16 -2
- package/dist/components/RunStatusIcon.d.ts +5 -2
- package/dist/components/RunStatusIcon.js +2 -1
- package/dist/components/StatusBadge.d.ts +17 -11
- package/dist/components/StatusBadge.js +10 -5
- package/dist/components/Tag.d.ts +5 -3
- package/dist/components/Tag.js +7 -1
- package/dist/components/relativeTime.d.ts +12 -9
- package/dist/components/relativeTime.js +4 -2
- package/dist/index.d.ts +3 -3
- package/dist/styles.css +1 -1
- package/package.json +1 -1
|
@@ -35,7 +35,7 @@ function createOpenAICompatibleStream(config) {
|
|
|
35
35
|
if (!res.ok) throw new ChatAdapterError(await errorMessageFromResponse(res), { status: res.status });
|
|
36
36
|
const reader = res.body?.getReader();
|
|
37
37
|
if (!reader) throw new ChatAdapterError("No response body");
|
|
38
|
-
return processSSEStream(reader, handlers.onContent, void 0, void 0, handlers.onReasoning, handlers.onMessageId, useResponses ? parseResponsesSSELine : void 0, handlers.onPart);
|
|
38
|
+
return processSSEStream(reader, handlers.onContent, void 0, void 0, handlers.onReasoning, handlers.onMessageId, useResponses ? parseResponsesSSELine : void 0, handlers.onPart, handlers.onUsage);
|
|
39
39
|
};
|
|
40
40
|
}
|
|
41
41
|
//#endregion
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ToolCall, ToolCallDelta } from '../../types.js';
|
|
1
|
+
import type { TokenUsage, ToolCall, ToolCallDelta } from '../../types.js';
|
|
2
2
|
import type { PartDelta } from '../types.js';
|
|
3
3
|
import type { ToolApprovalChunk, ToolResultChunk } from './wire.js';
|
|
4
4
|
export type ParsedSSELine = {
|
|
@@ -13,6 +13,7 @@ export type ParsedSSELine = {
|
|
|
13
13
|
model?: string | undefined;
|
|
14
14
|
error?: string | undefined;
|
|
15
15
|
responsesOutput?: unknown[] | undefined;
|
|
16
|
+
usage?: TokenUsage | undefined;
|
|
16
17
|
} | null;
|
|
17
18
|
export type SSELineParser = (line: string) => ParsedSSELine;
|
|
18
19
|
export declare function parseSSELine(line: string): ParsedSSELine;
|
|
@@ -20,7 +21,7 @@ export declare function parseSSELine(line: string): ParsedSSELine;
|
|
|
20
21
|
* calls stream as parts; a call the gateway answered with a result is left
|
|
21
22
|
* off the returned `toolCalls`, which then hold only calls the caller must
|
|
22
23
|
* run. */
|
|
23
|
-
export declare function processSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>, onContent: (content: string) => void, onToolCall?: (toolCalls: ToolCall[]) => void, onError?: (error: string) => void, onReasoning?: (reasoning: string) => void, onMessageId?: (messageId: string) => void, parseLine?: SSELineParser, onPart?: (delta: PartDelta) => void): Promise<{
|
|
24
|
+
export declare function processSSEStream(reader: ReadableStreamDefaultReader<Uint8Array>, onContent: (content: string) => void, onToolCall?: (toolCalls: ToolCall[]) => void, onError?: (error: string) => void, onReasoning?: (reasoning: string) => void, onMessageId?: (messageId: string) => void, parseLine?: SSELineParser, onPart?: (delta: PartDelta) => void, onUsage?: (usage: TokenUsage) => void): Promise<{
|
|
24
25
|
content: string;
|
|
25
26
|
messageId: string;
|
|
26
27
|
toolCalls: ToolCall[];
|
|
@@ -17,7 +17,8 @@ function parseSSELine(line) {
|
|
|
17
17
|
toolResult: chunk.toolResult,
|
|
18
18
|
toolApproval: chunk.toolApproval,
|
|
19
19
|
finishReason: choice?.finish_reason,
|
|
20
|
-
model: chunk.model
|
|
20
|
+
model: chunk.model,
|
|
21
|
+
usage: chunk.usage
|
|
21
22
|
};
|
|
22
23
|
} catch {
|
|
23
24
|
return null;
|
|
@@ -27,7 +28,7 @@ function parseSSELine(line) {
|
|
|
27
28
|
* calls stream as parts; a call the gateway answered with a result is left
|
|
28
29
|
* off the returned `toolCalls`, which then hold only calls the caller must
|
|
29
30
|
* run. */
|
|
30
|
-
async function processSSEStream(reader, onContent, onToolCall, onError, onReasoning, onMessageId, parseLine = parseSSELine, onPart) {
|
|
31
|
+
async function processSSEStream(reader, onContent, onToolCall, onError, onReasoning, onMessageId, parseLine = parseSSELine, onPart, onUsage) {
|
|
31
32
|
const decoder = new TextDecoder();
|
|
32
33
|
let assistantContent = "";
|
|
33
34
|
let assistantReasoning = "";
|
|
@@ -77,6 +78,7 @@ async function processSSEStream(reader, onContent, onToolCall, onError, onReason
|
|
|
77
78
|
onReasoning?.(parsed.reasoningContent);
|
|
78
79
|
}
|
|
79
80
|
if (parsed.finishReason) finishReason = parsed.finishReason;
|
|
81
|
+
if (parsed.usage) onUsage?.(parsed.usage);
|
|
80
82
|
if (parsed.responsesOutput?.length) responsesOutput = parsed.responsesOutput;
|
|
81
83
|
if (parsed.toolCalls) for (const tc of parsed.toolCalls) {
|
|
82
84
|
if (tc.index === void 0) continue;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, ToolCall, ToolDecision, UnreachableSession } from '../types.js';
|
|
1
|
+
import type { Allocation, ApprovalPart, AttachmentRef, ChatModel, Conversation, ConversationSummary, ProviderInfo, ProviderIssue, ShareGroup, SharePermission, SharePermissionLevel, TodoItem, TokenUsage, ToolCall, ToolDecision, UnreachableSession } from '../types.js';
|
|
2
2
|
import type { CompletionMessage } from './openai/wire.js';
|
|
3
3
|
export declare class ChatAdapterError extends Error {
|
|
4
4
|
status?: number | undefined;
|
|
@@ -36,6 +36,7 @@ export interface StreamHandlers {
|
|
|
36
36
|
onReasoning?: (reasoning: string) => void;
|
|
37
37
|
onMessageId?: (messageId: string) => void;
|
|
38
38
|
onPart?: (delta: PartDelta) => void;
|
|
39
|
+
onUsage?: (usage: TokenUsage) => void;
|
|
39
40
|
}
|
|
40
41
|
export type PartDelta = {
|
|
41
42
|
type: 'text' | 'reasoning';
|
package/dist/ai/agent/index.d.ts
CHANGED
|
@@ -2,6 +2,6 @@ export { classifyPseudoUserMessage } from './classifier.js';
|
|
|
2
2
|
export { computeEditDiff, computeWriteRows, type DiffRow, type EditDiffModel, type EditToolArgs, editSummary, isEditDiffTool, lineDelta, MAX_EDIT_DIFF_LINES, MAX_WRITE_DIFF_LINES, parseEditArgs, type WriteModel, wordEmphasisRanges, } from './diff.js';
|
|
3
3
|
export { type DiffToken, languageForPath, tokenizeDiffLines, } from './diffHighlight.js';
|
|
4
4
|
export { clampPermissionMode, DEFAULT_PERMISSION_MODE, isNamedPermissionMode, NAMED_PERMISSION_MODES, type NamedPermissionMode, type PermissionMode, permissionModesUpTo, } from './permissions.js';
|
|
5
|
-
export { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, type ToolHeader, type ToolSummaryOptions, toolHeaderFor, toolSummary, } from './toolfmt.js';
|
|
5
|
+
export { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupClauses, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, type ToolHeader, type ToolSummaryOptions, toolHeaderFor, toolSummary, } from './toolfmt.js';
|
|
6
6
|
export { defaultToolStrings, type ToolStrings } from './toolStrings.js';
|
|
7
7
|
export { agentTypeLabel, askQuestionFromArgs, firstNonEmptyLine, hiddenTranscriptTool, INTERRUPTED_NOTICE, interruptionNotice, parseSubagentNotice, planFromArgs, type SubagentNotice, subagentDeliveryLine, taskCallFromArgs, thinkingHeading, } from './transcript.js';
|
|
@@ -19,5 +19,6 @@ export interface ToolSummaryOptions {
|
|
|
19
19
|
export declare function toolSummary(name: string, args: string, result: string, isError: boolean, { editSummary, strings: t }?: ToolSummaryOptions): string;
|
|
20
20
|
export declare function isGroupableTool(name: string): boolean;
|
|
21
21
|
export declare function rollupSentence(names: string[], t?: ToolStrings): string;
|
|
22
|
+
export declare function rollupClauses(names: string[], t?: ToolStrings): string[];
|
|
22
23
|
export declare const AGENT_COLOR_NAMES: readonly ['blue', 'green', 'yellow', 'purple', 'cyan', 'orange', 'pink', 'red'];
|
|
23
24
|
export declare function agentColorHex(name: string | undefined): string | null;
|
package/dist/ai/agent/toolfmt.js
CHANGED
|
@@ -325,6 +325,10 @@ function isGroupableTool(name) {
|
|
|
325
325
|
return !NON_GROUPABLE_TOOLS.has(name);
|
|
326
326
|
}
|
|
327
327
|
function rollupSentence(names, t = defaultToolStrings) {
|
|
328
|
+
const parts = rollupClauses(names, t);
|
|
329
|
+
return parts.length === 0 ? "" : t.rollup.sentence(parts);
|
|
330
|
+
}
|
|
331
|
+
function rollupClauses(names, t = defaultToolStrings) {
|
|
328
332
|
const counts = ROLLUP_CATEGORIES.map(() => 0);
|
|
329
333
|
for (const name of names) for (let i = 0; i < ROLLUP_CATEGORIES.length; i++) if (ROLLUP_CATEGORIES[i]?.[0](name)) {
|
|
330
334
|
counts[i] = (counts[i] ?? 0) + 1;
|
|
@@ -338,7 +342,7 @@ function rollupSentence(names, t = defaultToolStrings) {
|
|
|
338
342
|
if (cat) parts.push(t.rollup[cat[1]](n));
|
|
339
343
|
}
|
|
340
344
|
}
|
|
341
|
-
return parts
|
|
345
|
+
return parts;
|
|
342
346
|
}
|
|
343
347
|
var AGENT_COLOR_NAMES = [
|
|
344
348
|
"blue",
|
|
@@ -364,4 +368,4 @@ function agentColorHex(name) {
|
|
|
364
368
|
return name ? AGENT_COLOR_HEX[name] ?? null : null;
|
|
365
369
|
}
|
|
366
370
|
//#endregion
|
|
367
|
-
export { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, toolHeaderFor, toolSummary };
|
|
371
|
+
export { AGENT_COLOR_NAMES, agentColorHex, bashExitCode, clip, fullToolArgs, isGroupableTool, resultFailed, rollupClauses, rollupSentence, sanitizeLabel, sanitizePanelText, summarizeToolArgs, toolHeaderFor, toolSummary };
|
package/dist/ai/ai.css
CHANGED
|
@@ -444,6 +444,20 @@
|
|
|
444
444
|
color: var(--chat-accent);
|
|
445
445
|
}
|
|
446
446
|
|
|
447
|
+
/* A list's caret follows its last item's text rather than sitting on a line
|
|
448
|
+
of its own under the list. */
|
|
449
|
+
.markdown-streaming > :is(ul, ol):last-child:not([data-sd-caret-hidden])::after {
|
|
450
|
+
content: none;
|
|
451
|
+
}
|
|
452
|
+
.markdown-streaming > :is(ul, ol):last-child > li:last-child::after {
|
|
453
|
+
display: inline;
|
|
454
|
+
vertical-align: baseline;
|
|
455
|
+
content: var(--streamdown-caret);
|
|
456
|
+
}
|
|
457
|
+
.chat-thread .markdown-streaming > :is(ul, ol):last-child > li:last-child::after {
|
|
458
|
+
color: var(--chat-accent);
|
|
459
|
+
}
|
|
460
|
+
|
|
447
461
|
/* Selected text inside the chat takes the accent too. */
|
|
448
462
|
.chat-thread ::selection,
|
|
449
463
|
.chat-composer ::selection {
|
|
@@ -9,8 +9,8 @@ import { DateTime } from "luxon";
|
|
|
9
9
|
/** An agent session's working line for the provider's reply, naming its current step. */
|
|
10
10
|
function ChatActivity() {
|
|
11
11
|
const { isStreaming, tools } = useChat();
|
|
12
|
-
const { streamingParts } = useChatStream();
|
|
13
|
-
const { strings } = useChatConfig();
|
|
12
|
+
const { streamingParts, streamingUsage } = useChatStream();
|
|
13
|
+
const { strings, slots } = useChatConfig();
|
|
14
14
|
const [startedAt, setStartedAt] = useState(null);
|
|
15
15
|
useEffect(() => {
|
|
16
16
|
setStartedAt(isStreaming ? DateTime.now().toUTC().toISO() : null);
|
|
@@ -20,11 +20,13 @@ function ChatActivity() {
|
|
|
20
20
|
const running = streamingParts.findLast((p) => p.kind === "tool_call" && p.status === "running");
|
|
21
21
|
let word;
|
|
22
22
|
if (waiting) word = strings.activity.waitingForAnswer;
|
|
23
|
-
else if (running?.kind === "tool_call") word = tools.find((t) => t.id === running.name)?.label ?? toolHeaderFor(running.name).label;
|
|
23
|
+
else if (running?.kind === "tool_call") word = slots.toolPresentation?.(running)?.label ?? tools.find((t) => t.id === running.name)?.label ?? toolHeaderFor(running.name).label;
|
|
24
24
|
return /* @__PURE__ */ jsx(ActivityLine, {
|
|
25
25
|
testId: "chat-activity",
|
|
26
26
|
startedAt,
|
|
27
|
-
word
|
|
27
|
+
word,
|
|
28
|
+
inputTokens: streamingUsage?.prompt_tokens,
|
|
29
|
+
outputTokens: streamingUsage?.completion_tokens
|
|
28
30
|
});
|
|
29
31
|
}
|
|
30
32
|
//#endregion
|
|
@@ -46,7 +46,8 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
46
46
|
const hasReasoning = !!message.reasoning;
|
|
47
47
|
const toolCallCount = message.parts?.filter((p) => p.kind === "tool_call").length ?? 0;
|
|
48
48
|
const showReasoningButton = !isUser && !isStreaming && hasReasoning;
|
|
49
|
-
const
|
|
49
|
+
const narrated = !!message.parts?.some((p) => p.kind === "text");
|
|
50
|
+
const showTurnDurationLabel = !isUser && !isStreaming && !hasReasoning && (!!message.reasoningDuration || toolCallCount > 0 && (!narrated || !!askerName));
|
|
50
51
|
const { messageMeta: t, thinking: tThinking } = useChatConfig().strings;
|
|
51
52
|
const { common } = useStrings();
|
|
52
53
|
const [editContent, setEditContent] = useState(message.content || "");
|
|
@@ -114,35 +115,42 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
114
115
|
const said = replyText(message);
|
|
115
116
|
return /* @__PURE__ */ jsxs("div", {
|
|
116
117
|
className: cx("group py-3 w-full", !flush && "sm:px-4"),
|
|
117
|
-
children: [
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
118
|
+
children: [
|
|
119
|
+
/* @__PURE__ */ jsx(AgentMessageParts, {
|
|
120
|
+
message: partsMessage,
|
|
121
|
+
isStreaming,
|
|
122
|
+
onApprovalAnswer: approvalAnswer
|
|
123
|
+
}),
|
|
124
|
+
isStreaming && !isUser && /* @__PURE__ */ jsx("div", {
|
|
125
|
+
"aria-hidden": true,
|
|
126
|
+
className: "mt-2 h-7"
|
|
127
|
+
}),
|
|
128
|
+
!isUser && !isStreaming && (said || onRegenerate || meta) && /* @__PURE__ */ jsxs("div", {
|
|
129
|
+
className: "chat-actions -ml-1.5 mt-2 flex items-center gap-0.5",
|
|
130
|
+
children: [
|
|
131
|
+
said && /* @__PURE__ */ jsx(CopyButton, {
|
|
132
|
+
text: said,
|
|
133
|
+
label: t.copyLabel,
|
|
134
|
+
quiet: true,
|
|
135
|
+
size: "md",
|
|
136
|
+
className: actionClasses
|
|
137
|
+
}),
|
|
138
|
+
onRegenerate && /* @__PURE__ */ jsx(IconButton, {
|
|
139
|
+
label: t.regenerate,
|
|
140
|
+
onClick: () => onRegenerate(message.id),
|
|
141
|
+
size: "md",
|
|
142
|
+
className: actionClasses,
|
|
143
|
+
icon: /* @__PURE__ */ jsx(RetryIcon, {})
|
|
144
|
+
}),
|
|
145
|
+
onNavigateBranch && /* @__PURE__ */ jsx(BranchNavigator, {
|
|
146
|
+
messages: allMessages,
|
|
147
|
+
currentMessageId: message.id,
|
|
148
|
+
onNavigate: onNavigateBranch
|
|
149
|
+
}),
|
|
150
|
+
meta
|
|
151
|
+
]
|
|
152
|
+
})
|
|
153
|
+
]
|
|
146
154
|
});
|
|
147
155
|
}
|
|
148
156
|
if (isTool) return null;
|
|
@@ -266,6 +274,10 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
266
274
|
onOpen: onOpenAttachment,
|
|
267
275
|
className: isRightAligned ? "justify-end" : void 0
|
|
268
276
|
}),
|
|
277
|
+
isStreaming && !isUser && /* @__PURE__ */ jsx("div", {
|
|
278
|
+
"aria-hidden": true,
|
|
279
|
+
className: "mt-2 h-7"
|
|
280
|
+
}),
|
|
269
281
|
!isStreaming && !isEditing && /* @__PURE__ */ jsxs("div", {
|
|
270
282
|
className: cx("chat-actions flex items-center gap-0.5 transition-opacity duration-150", isRightAligned ? "absolute right-0 top-full -mr-1.5 mt-0.5" : "-ml-1.5 mt-2", isRightAligned && "pointer-events-none opacity-0 group-focus-within:pointer-events-auto group-focus-within:opacity-100 group-hover:pointer-events-auto group-hover:opacity-100"),
|
|
271
283
|
children: [
|
|
@@ -19,7 +19,8 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
19
19
|
const { isThinking, streamingContent, streamingReasoning, streamingParts, thinkingStartTime } = useChatStream();
|
|
20
20
|
const isStreaming = isStreamingProp ?? providerStreaming;
|
|
21
21
|
const conversationId = messages[0]?.conversationId;
|
|
22
|
-
const
|
|
22
|
+
const textInParts = streamingParts.some((part) => part.kind === "text");
|
|
23
|
+
const providerStreamingMessage = useMemo(() => providerStreaming && streamingContent && !textInParts ? {
|
|
23
24
|
id: "streaming",
|
|
24
25
|
role: "assistant",
|
|
25
26
|
content: streamingContent,
|
|
@@ -28,6 +29,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
28
29
|
} : null, [
|
|
29
30
|
providerStreaming,
|
|
30
31
|
streamingContent,
|
|
32
|
+
textInParts,
|
|
31
33
|
conversationId
|
|
32
34
|
]);
|
|
33
35
|
const streamingMessage = streamingMessageProp === void 0 ? providerStreamingMessage : streamingMessageProp;
|
|
@@ -99,7 +101,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
99
101
|
}, []);
|
|
100
102
|
const hasContent = messages.length > 0 || !!streamingMessage;
|
|
101
103
|
const awaitingApproval = streamingParts.some((part) => part.kind === "approval" && !part.resolved);
|
|
102
|
-
const showWorkingIndicator = workingIndicator && isStreaming && !streamingReasoning && !streamingMessage && !awaitingApproval;
|
|
104
|
+
const showWorkingIndicator = workingIndicator && isStreaming && !streamingReasoning && !streamingMessage && !textInParts && !awaitingApproval;
|
|
103
105
|
useEffect(() => {
|
|
104
106
|
const container = containerRef.current;
|
|
105
107
|
if (!container) return;
|
|
@@ -145,8 +147,11 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
145
147
|
isStreaming,
|
|
146
148
|
hasQueuedMessages
|
|
147
149
|
]);
|
|
150
|
+
const wasStreamingRef = useRef(isStreaming);
|
|
148
151
|
useEffect(() => {
|
|
149
|
-
|
|
152
|
+
const streamFinished = wasStreamingRef.current && !isStreaming;
|
|
153
|
+
wasStreamingRef.current = isStreaming;
|
|
154
|
+
if (!containerRef.current || !isNearBottomRef.current || isStreaming || streamFinished || !hadMessagesRef.current) return;
|
|
150
155
|
const el = containerRef.current;
|
|
151
156
|
el.scrollTo({
|
|
152
157
|
top: el.scrollHeight,
|
|
@@ -224,7 +229,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
224
229
|
isStreaming && streamingReasoning && /* @__PURE__ */ jsx("div", {
|
|
225
230
|
className: "animate-chat-enter",
|
|
226
231
|
children: /* @__PURE__ */ jsx("div", {
|
|
227
|
-
className: cx("pt-3", !flush && "px-4"),
|
|
232
|
+
className: cx("pt-3", !flush && "sm:px-4"),
|
|
228
233
|
children: /* @__PURE__ */ jsx(Reasoning, {
|
|
229
234
|
label: /* @__PURE__ */ jsx(ThinkingLabel, {
|
|
230
235
|
startedAt: isThinking ? thinkingStartTime : void 0,
|
|
@@ -253,7 +258,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
253
258
|
})
|
|
254
259
|
}, "thinking"),
|
|
255
260
|
isStreaming && streamingParts.length > 0 && /* @__PURE__ */ jsx("div", {
|
|
256
|
-
className: cx("animate-chat-enter py-1", !flush && "px-4"),
|
|
261
|
+
className: cx("animate-chat-enter py-1", !flush && "sm:px-4"),
|
|
257
262
|
children: /* @__PURE__ */ jsx(AgentMessageParts, {
|
|
258
263
|
message: streamingPartsMessage,
|
|
259
264
|
isStreaming
|
|
@@ -262,7 +267,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
262
267
|
showWorkingIndicator && /* @__PURE__ */ jsx("div", {
|
|
263
268
|
className: "animate-chat-enter",
|
|
264
269
|
children: /* @__PURE__ */ jsx("div", {
|
|
265
|
-
className: cx("py-3", !flush && "px-4"),
|
|
270
|
+
className: cx("py-3", !flush && "sm:px-4"),
|
|
266
271
|
children: /* @__PURE__ */ jsx("div", {
|
|
267
272
|
className: "animate-shimmer text-shimmer text-sm",
|
|
268
273
|
children: tThinking.ellipsis
|
|
@@ -294,7 +299,7 @@ function ChatMessageList({ messages, allMessages, streamingMessage: streamingMes
|
|
|
294
299
|
onOpenAttachment
|
|
295
300
|
}), /* @__PURE__ */ jsxs("div", {
|
|
296
301
|
"data-testid": "queued-status",
|
|
297
|
-
className: cx("-mt-4 mb-2 flex items-center justify-end gap-2 text-xs theme-muted-text", !flush && "px-4"),
|
|
302
|
+
className: cx("-mt-4 mb-2 flex items-center justify-end gap-2 text-xs theme-muted-text", !flush && "sm:px-4"),
|
|
298
303
|
children: [
|
|
299
304
|
removable ? /* @__PURE__ */ jsx(ClockIcon, { className: "h-3 w-3" }) : /* @__PURE__ */ jsx(CheckIcon, { className: "h-3 w-3" }),
|
|
300
305
|
/* @__PURE__ */ jsx("span", { children: status }),
|
|
@@ -355,15 +360,18 @@ function ThinkingLabel({ startedAt, finalDuration }) {
|
|
|
355
360
|
* for every streamed token, so a long conversation is not walked per token. */
|
|
356
361
|
var MessageRows = memo(function MessageRows({ messages, allMessages, streaming, flush, editingMessageId, showAuthor, currentUsername, attachmentDownloadUrl, onOpenAttachment, onNavigateBranch, onEditMessage, onSaveEdit, onCancelEdit, onRegenerateMessage, onRetryMessage, onOpenReasoning, onApprovalAnswer }) {
|
|
357
362
|
const shownRef = useRef(/* @__PURE__ */ new Set());
|
|
363
|
+
const wasStreamingRef = useRef(streaming);
|
|
358
364
|
const shown = shownRef.current;
|
|
359
365
|
const appended = shown.size > 0 && messages.some((m) => shown.has(m.id));
|
|
366
|
+
const streamFinished = wasStreamingRef.current && !streaming;
|
|
360
367
|
useLayoutEffect(() => {
|
|
361
368
|
shownRef.current = new Set(messages.map((m) => m.id));
|
|
362
|
-
|
|
369
|
+
wasStreamingRef.current = streaming;
|
|
370
|
+
}, [messages, streaming]);
|
|
363
371
|
const lastIndex = messages.findLastIndex((m) => !m.pseudo);
|
|
364
372
|
return messages.map((message, i) => {
|
|
365
373
|
if (message.pseudo) return null;
|
|
366
|
-
const isNew = appended && !shown.has(message.id);
|
|
374
|
+
const isNew = appended && !shown.has(message.id) && !(streamFinished && message.role === "assistant");
|
|
367
375
|
return /* @__PURE__ */ jsx("div", {
|
|
368
376
|
className: isNew ? "animate-chat-enter" : void 0,
|
|
369
377
|
children: /* @__PURE__ */ jsx(ChatMessage_default, {
|
|
@@ -26,7 +26,7 @@ type RenderItem = {
|
|
|
26
26
|
index: number;
|
|
27
27
|
}[];
|
|
28
28
|
};
|
|
29
|
-
export declare function buildRenderItems(parts: MessagePart[], hostDrawn?: (toolName: string) => boolean): RenderItem[];
|
|
29
|
+
export declare function buildRenderItems(parts: MessagePart[], hostDrawn?: (toolName: string) => boolean, joinsRollup?: (part: ToolCallPart) => boolean): RenderItem[];
|
|
30
30
|
export default function AgentMessageParts({ message, isStreaming, onApprovalAnswer, }: {
|
|
31
31
|
message: PartRendererProps['message'];
|
|
32
32
|
isStreaming?: boolean;
|
|
@@ -9,7 +9,7 @@ import { Disclosure } from "../../../components/Disclosure.js";
|
|
|
9
9
|
import { formatDuration } from "../../../format.js";
|
|
10
10
|
import { RelativeTime } from "../../../components/relativeTime.js";
|
|
11
11
|
import { LogViewer } from "../../../logviewer/LogViewer.js";
|
|
12
|
-
import { agentColorHex, bashExitCode, fullToolArgs, isGroupableTool,
|
|
12
|
+
import { agentColorHex, bashExitCode, fullToolArgs, isGroupableTool, rollupClauses, summarizeToolArgs, toolHeaderFor, toolSummary } from "../../agent/toolfmt.js";
|
|
13
13
|
import { computeEditDiff, computeWriteRows, editSummary, isEditDiffTool, parseEditArgs } from "../../agent/diff.js";
|
|
14
14
|
import { languageForPath, tokenizeDiffLines } from "../../agent/diffHighlight.js";
|
|
15
15
|
import { thinkingHeading } from "../../agent/transcript.js";
|
|
@@ -232,13 +232,22 @@ function ToolStatusDot({ part }) {
|
|
|
232
232
|
}
|
|
233
233
|
/** A part's toggle row: its status mark and name, across the transcript's width. */
|
|
234
234
|
var rowSummaryClasses = "min-h-6 w-full";
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
235
|
+
function RowSummary({ text }) {
|
|
236
|
+
return text ? /* @__PURE__ */ jsx("div", {
|
|
237
|
+
className: "ml-5 mt-0.5 truncate text-[13px] theme-muted-text",
|
|
238
|
+
children: text
|
|
239
|
+
}) : null;
|
|
240
|
+
}
|
|
241
|
+
/** A call's row: its status and signature, or the host's words for it, opening
|
|
242
|
+
* to its full arguments and result. `children` is a host's drawing of the
|
|
243
|
+
* call, shown under the row in place of the one-line summary, or inside it
|
|
244
|
+
* when the host names the call. */
|
|
238
245
|
function ToolCallBlock({ part, children }) {
|
|
239
|
-
const { strings } = useChatConfig();
|
|
246
|
+
const { strings, slots } = useChatConfig();
|
|
240
247
|
const t = strings.agentTranscript;
|
|
241
248
|
const [expanded, setExpanded] = useState(false);
|
|
249
|
+
const presentation = slots.toolPresentation?.(part);
|
|
250
|
+
const drawingInPanel = !!children && !!presentation && !presentation.open && part.status !== "waiting";
|
|
242
251
|
const header = toolHeaderFor(part.name);
|
|
243
252
|
const argSummary = summarizeToolArgs(header.arg, part.args);
|
|
244
253
|
const isDiff = isEditDiffTool(part.name);
|
|
@@ -262,7 +271,10 @@ function ToolCallBlock({ part, children }) {
|
|
|
262
271
|
onOpenChange: setExpanded,
|
|
263
272
|
disabled: isDiff,
|
|
264
273
|
summaryClassName: rowSummaryClasses,
|
|
265
|
-
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ToolStatusDot, { part }), /* @__PURE__ */
|
|
274
|
+
summary: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(ToolStatusDot, { part }), presentation ? /* @__PURE__ */ jsx("span", {
|
|
275
|
+
className: "text-[13px] leading-5 min-w-0 truncate theme-text",
|
|
276
|
+
children: presentation.label
|
|
277
|
+
}) : /* @__PURE__ */ jsxs("span", {
|
|
266
278
|
className: "font-mono text-[13px] leading-5 min-w-0 truncate",
|
|
267
279
|
children: [/* @__PURE__ */ jsx("span", {
|
|
268
280
|
className: "font-semibold",
|
|
@@ -277,23 +289,26 @@ function ToolCallBlock({ part, children }) {
|
|
|
277
289
|
})]
|
|
278
290
|
})] }),
|
|
279
291
|
panelClassName: "ml-5 mt-1 space-y-1.5",
|
|
280
|
-
children: [fullToolArgs(header.arg, part.args) && /* @__PURE__ */
|
|
292
|
+
children: [fullToolArgs(header.arg, part.args) && /* @__PURE__ */ jsxs("pre", {
|
|
281
293
|
className: "overflow-x-auto rounded-md bg-(--theme-muted-panel-bg) p-2 font-mono text-xs whitespace-pre-wrap",
|
|
282
|
-
children:
|
|
283
|
-
|
|
294
|
+
children: [presentation && /* @__PURE__ */ jsxs("span", {
|
|
295
|
+
className: "font-semibold",
|
|
296
|
+
children: [header.label, " "]
|
|
297
|
+
}), fullToolArgs(header.arg, part.args)]
|
|
298
|
+
}), drawingInPanel ? /* @__PURE__ */ jsx("div", {
|
|
299
|
+
className: "min-w-0",
|
|
300
|
+
children
|
|
301
|
+
}) : /* @__PURE__ */ jsx(ResultBody, { part })]
|
|
284
302
|
}),
|
|
285
303
|
part.status === "error" && !expanded && /* @__PURE__ */ jsx("div", {
|
|
286
304
|
className: "ml-5 mt-0.5 truncate text-[13px] text-(--theme-error-text)",
|
|
287
305
|
children: t.failedWith(summary)
|
|
288
306
|
}),
|
|
289
|
-
children && /* @__PURE__ */ jsx("div", {
|
|
307
|
+
children && !drawingInPanel && /* @__PURE__ */ jsx("div", {
|
|
290
308
|
className: "mt-1.5 min-w-0 sm:ml-5",
|
|
291
309
|
children
|
|
292
310
|
}),
|
|
293
|
-
!
|
|
294
|
-
className: "ml-5 mt-0.5 truncate text-[13px] theme-muted-text",
|
|
295
|
-
children: summary
|
|
296
|
-
}),
|
|
311
|
+
!isDiff && !expanded && part.status !== "waiting" && part.status !== "error" && (!children || drawingInPanel) && /* @__PURE__ */ jsx(RowSummary, { text: part.status === "running" ? summary : presentation?.summary ?? (drawingInPanel ? void 0 : summary) }),
|
|
297
312
|
isDiff && /* @__PURE__ */ jsx("div", {
|
|
298
313
|
className: "ml-5",
|
|
299
314
|
children: /* @__PURE__ */ jsx(EditDiffBlock, { part })
|
|
@@ -810,7 +825,7 @@ function mergeReasoning(parts) {
|
|
|
810
825
|
...durationMs > 0 ? { durationMs } : {}
|
|
811
826
|
};
|
|
812
827
|
}
|
|
813
|
-
function buildRenderItems(parts, hostDrawn = () => false) {
|
|
828
|
+
function buildRenderItems(parts, hostDrawn = () => false, joinsRollup = () => false) {
|
|
814
829
|
const items = [];
|
|
815
830
|
let run = [];
|
|
816
831
|
const emitThoughts = (members) => {
|
|
@@ -852,7 +867,7 @@ function buildRenderItems(parts, hostDrawn = () => false) {
|
|
|
852
867
|
run = [];
|
|
853
868
|
};
|
|
854
869
|
const decided = new Set(parts.flatMap((p) => p.kind === "tool_call" && p.decision ? [p.id] : []));
|
|
855
|
-
const drawnByHost = new Set(parts.flatMap((p) => p.kind === "tool_call" && hostDrawn(p.name) ? [p.id] : []));
|
|
870
|
+
const drawnByHost = new Set(parts.flatMap((p) => p.kind === "tool_call" && hostDrawn(p.name) && !joinsRollup(p) ? [p.id] : []));
|
|
856
871
|
parts.forEach((part, index) => {
|
|
857
872
|
if (part.kind === "approval" && part.resolved && decided.has(part.id)) return;
|
|
858
873
|
if (part.kind === "approval" && drawnByHost.has(part.id)) return;
|
|
@@ -903,8 +918,36 @@ function groupRepeatedCalls(items) {
|
|
|
903
918
|
return out;
|
|
904
919
|
}
|
|
905
920
|
function RollupGroup({ members, message }) {
|
|
906
|
-
const { strings } = useChatConfig();
|
|
907
|
-
const sentence =
|
|
921
|
+
const { strings, slots } = useChatConfig();
|
|
922
|
+
const { sentence, allNamed } = useMemo(() => {
|
|
923
|
+
const builtin = [];
|
|
924
|
+
const named = /* @__PURE__ */ new Map();
|
|
925
|
+
for (const m of members) {
|
|
926
|
+
if (m.part.kind !== "tool_call") continue;
|
|
927
|
+
const rollup = slots.toolPresentation?.(m.part)?.rollup;
|
|
928
|
+
if (!rollup) {
|
|
929
|
+
builtin.push(m.part.name);
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
const entry = named.get(m.part.name) ?? {
|
|
933
|
+
count: 0,
|
|
934
|
+
rollup
|
|
935
|
+
};
|
|
936
|
+
named.set(m.part.name, {
|
|
937
|
+
...entry,
|
|
938
|
+
count: entry.count + 1
|
|
939
|
+
});
|
|
940
|
+
}
|
|
941
|
+
const clauses = [...[...named.values()].map((n) => n.rollup(n.count)), ...rollupClauses(builtin, strings.tools)];
|
|
942
|
+
return {
|
|
943
|
+
sentence: clauses.length ? strings.tools.rollup.sentence(clauses) : "",
|
|
944
|
+
allNamed: named.size > 0 && builtin.length === 0
|
|
945
|
+
};
|
|
946
|
+
}, [
|
|
947
|
+
members,
|
|
948
|
+
slots.toolPresentation,
|
|
949
|
+
strings.tools
|
|
950
|
+
]);
|
|
908
951
|
return /* @__PURE__ */ jsx("div", {
|
|
909
952
|
className: "py-1.5 text-sm",
|
|
910
953
|
"data-testid": "tool-rollup",
|
|
@@ -916,7 +959,7 @@ function RollupGroup({ members, message }) {
|
|
|
916
959
|
"aria-hidden": "true",
|
|
917
960
|
children: "●"
|
|
918
961
|
}), /* @__PURE__ */ jsx("span", {
|
|
919
|
-
className: "
|
|
962
|
+
className: cx("text-[13px] min-w-0 truncate", allNamed ? "theme-text" : "font-mono theme-muted-text"),
|
|
920
963
|
children: sentence
|
|
921
964
|
})] }),
|
|
922
965
|
panelClassName: "ml-1.5 mt-0.5 rounded-md bg-(--theme-muted-panel-bg)/40 px-2",
|
|
@@ -937,6 +980,9 @@ function CallGroup({ members, message }) {
|
|
|
937
980
|
const name = members[0]?.part.name ?? "";
|
|
938
981
|
const Tool = slots.toolRenderers?.[name];
|
|
939
982
|
const header = toolHeaderFor(name);
|
|
983
|
+
const first = members[0]?.part;
|
|
984
|
+
const presentation = first ? slots.toolPresentation?.(first) : void 0;
|
|
985
|
+
const drawFolded = Tool && !(presentation && !presentation.open);
|
|
940
986
|
const failed = members.filter((m) => m.part.status === "error").length;
|
|
941
987
|
const status = failed === members.length ? "error" : "ok";
|
|
942
988
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -951,8 +997,8 @@ function CallGroup({ members, message }) {
|
|
|
951
997
|
...members[0]?.part,
|
|
952
998
|
status
|
|
953
999
|
} }), /* @__PURE__ */ jsxs("span", {
|
|
954
|
-
className: "
|
|
955
|
-
children: [t.callGroup(header.label, members.length), failed > 0 && /* @__PURE__ */ jsxs("span", {
|
|
1000
|
+
className: cx("text-[13px] leading-5 min-w-0 truncate", presentation?.group ? "theme-text" : "font-mono"),
|
|
1001
|
+
children: [presentation?.group?.(members.length) ?? t.callGroup(header.label, members.length), failed > 0 && /* @__PURE__ */ jsxs("span", {
|
|
956
1002
|
className: "text-(--theme-error-text)",
|
|
957
1003
|
children: [" · ", t.failedCount(failed)]
|
|
958
1004
|
})]
|
|
@@ -962,7 +1008,7 @@ function CallGroup({ members, message }) {
|
|
|
962
1008
|
part: m.part,
|
|
963
1009
|
message
|
|
964
1010
|
}, m.index))
|
|
965
|
-
}), !open &&
|
|
1011
|
+
}), !open && drawFolded && members.map(({ part, index }) => {
|
|
966
1012
|
const args = summarizeToolArgs(header.arg, part.args);
|
|
967
1013
|
return /* @__PURE__ */ jsxs("div", {
|
|
968
1014
|
className: "mt-1.5 min-w-0 sm:ml-5 has-[>[data-drawing]:empty]:hidden",
|
|
@@ -985,7 +1031,15 @@ function AgentMessageParts({ message, isStreaming, onApprovalAnswer }) {
|
|
|
985
1031
|
const parts = message.parts ?? [];
|
|
986
1032
|
const { slots } = useChatConfig();
|
|
987
1033
|
const toolRenderers = slots.toolRenderers;
|
|
988
|
-
const
|
|
1034
|
+
const toolPresentation = slots.toolPresentation;
|
|
1035
|
+
const items = useMemo(() => buildRenderItems(parts, (name) => !!toolRenderers?.[name], (part) => {
|
|
1036
|
+
const p = toolPresentation?.(part);
|
|
1037
|
+
return !!p?.rollup && !p.open;
|
|
1038
|
+
}), [
|
|
1039
|
+
parts,
|
|
1040
|
+
toolRenderers,
|
|
1041
|
+
toolPresentation
|
|
1042
|
+
]);
|
|
989
1043
|
const lastIndex = parts.length - 1;
|
|
990
1044
|
return /* @__PURE__ */ jsx("div", {
|
|
991
1045
|
className: "w-full",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { ChatAdapter, ChatToolOption } from '../adapter/types.js';
|
|
3
|
-
import type { ApprovalAnswerValue, AttachmentMeta, ChatModel, ChatNavigation, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, ProviderIssue, UnreachableSession } from '../types.js';
|
|
3
|
+
import type { ApprovalAnswerValue, AttachmentMeta, ChatModel, ChatNavigation, ChatUser, Conversation, ConversationSummary, ChatMessage as Message, MessagePart, ProviderInfo, ProviderIssue, TokenUsage, UnreachableSession } from '../types.js';
|
|
4
4
|
import { type ChatUIConfig } from './config.js';
|
|
5
5
|
import { type SidebarPresentation, type SidebarState } from './sidebarState.js';
|
|
6
6
|
interface ChatContextValue {
|
|
@@ -67,6 +67,8 @@ export interface ChatStream {
|
|
|
67
67
|
streamingContent: string;
|
|
68
68
|
streamingReasoning: string;
|
|
69
69
|
streamingParts: MessagePart[];
|
|
70
|
+
/** Tokens the streaming reply has used so far, when the stream reports them. */
|
|
71
|
+
streamingUsage: TokenUsage | null;
|
|
70
72
|
thinkingStartTime: number | null;
|
|
71
73
|
}
|
|
72
74
|
/** A model's display name by its id, when the adapter listed one; undefined
|