@parallelworks/ui 0.75.1 → 0.77.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/README.md +13 -0
- package/dist/ai/adapter/openai/createOpenAIStream.js +1 -0
- package/dist/ai/adapter/openai/sse.d.ts +2 -1
- package/dist/ai/adapter/openai/sse.js +20 -0
- package/dist/ai/adapter/openai/wire.d.ts +5 -0
- package/dist/ai/adapter/types.d.ts +12 -0
- package/dist/ai/components/ChatActivity.d.ts +2 -0
- package/dist/ai/components/ChatActivity.js +31 -0
- package/dist/ai/components/ChatMessage.js +30 -5
- package/dist/ai/components/ChatThread.js +4 -3
- package/dist/ai/components/ComposerAddMenu.js +17 -6
- package/dist/ai/components/SessionView.js +4 -1
- package/dist/ai/components/ShareDialog.d.ts +3 -0
- package/dist/ai/components/ShareDialog.js +49 -13
- package/dist/ai/components/agent/AgentMessageParts.d.ts +1 -1
- package/dist/ai/components/agent/AgentMessageParts.js +66 -7
- package/dist/ai/core/ChatProvider.d.ts +3 -1
- package/dist/ai/core/ChatProvider.js +224 -131
- package/dist/ai/core/config.d.ts +10 -1
- package/dist/ai/core/parts.js +5 -1
- package/dist/ai/index.d.ts +1 -1
- package/dist/ai/strings.d.ts +16 -0
- package/dist/ai/strings.js +13 -5
- package/dist/ai/types.d.ts +6 -1
- package/dist/components/relativeTime.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -924,6 +924,19 @@ OpenAI-compatible transport sends them as the `X-Platform-Tools` header
|
|
|
924
924
|
(`TOOLS_HEADER`), comma-separated and empty when every tool is off. A request
|
|
925
925
|
without a tool choice sends no header.
|
|
926
926
|
|
|
927
|
+
A tool that changes something sets `asksFirst`, which the menu shows, and its
|
|
928
|
+
calls wait for the reader. The gateway streams the call, then a `toolApproval`
|
|
929
|
+
chunk (`toolCallId`, optional `expiresAt`) in place of a result, and ends the
|
|
930
|
+
reply there. Chat shows a confirm under the call with Allow, Deny and Deny
|
|
931
|
+
with feedback, which expires at `expiresAt`. The answer goes back through
|
|
932
|
+
`useChat().answerToolApproval` as a request with no new user turn:
|
|
933
|
+
`StreamRequest.resume` (`messageId`, `toolCallId`, `allowed`, optional
|
|
934
|
+
`feedback`), sent as `resume` in the body. The gateway's `toolResult` for the
|
|
935
|
+
call lands on the reply that paused, and the model's answer streams as a new
|
|
936
|
+
reply under it. In a shared conversation only the person whose message
|
|
937
|
+
started the turn may answer; others see whom it waits on, and a turn someone
|
|
938
|
+
else started says whose access its tools ran with.
|
|
939
|
+
|
|
927
940
|
Tailwind v4 consumers add the chat rules (markdown rendering, streaming
|
|
928
941
|
animations) and streamdown's classes next to the theme contract:
|
|
929
942
|
|
|
@@ -24,6 +24,7 @@ function createOpenAICompatibleStream(config) {
|
|
|
24
24
|
messages: req.messages,
|
|
25
25
|
stream: true
|
|
26
26
|
};
|
|
27
|
+
if (req.resume && !useResponses) body["resume"] = req.resume;
|
|
27
28
|
const res = await (config.fetch ?? globalThis.fetch.bind(globalThis))(url, {
|
|
28
29
|
method: "POST",
|
|
29
30
|
headers: requestHeaders(config.buildHeaders?.(req), req.tools),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ToolCall, ToolCallDelta } from '../../types.js';
|
|
2
2
|
import type { PartDelta } from '../types.js';
|
|
3
|
-
import type { ToolResultChunk } from './wire.js';
|
|
3
|
+
import type { ToolApprovalChunk, ToolResultChunk } from './wire.js';
|
|
4
4
|
export type ParsedSSELine = {
|
|
5
5
|
id?: string | undefined;
|
|
6
6
|
messageId?: string | undefined;
|
|
@@ -8,6 +8,7 @@ export type ParsedSSELine = {
|
|
|
8
8
|
reasoningContent?: string | undefined;
|
|
9
9
|
toolCalls?: ToolCallDelta[] | undefined;
|
|
10
10
|
toolResult?: ToolResultChunk | undefined;
|
|
11
|
+
toolApproval?: ToolApprovalChunk | undefined;
|
|
11
12
|
finishReason?: string | null | undefined;
|
|
12
13
|
model?: string | undefined;
|
|
13
14
|
error?: string | undefined;
|
|
@@ -15,6 +15,7 @@ function parseSSELine(line) {
|
|
|
15
15
|
reasoningContent: choice?.delta?.reasoning_content,
|
|
16
16
|
toolCalls: choice?.delta?.tool_calls ?? void 0,
|
|
17
17
|
toolResult: chunk.toolResult,
|
|
18
|
+
toolApproval: chunk.toolApproval,
|
|
18
19
|
finishReason: choice?.finish_reason,
|
|
19
20
|
model: chunk.model
|
|
20
21
|
};
|
|
@@ -108,6 +109,25 @@ async function processSSEStream(reader, onContent, onToolCall, onError, onReason
|
|
|
108
109
|
...parsed.toolResult.isError ? { isError: true } : {}
|
|
109
110
|
});
|
|
110
111
|
}
|
|
112
|
+
if (parsed.toolApproval) {
|
|
113
|
+
const { toolCallId, expiresAt } = parsed.toolApproval;
|
|
114
|
+
answeredToolCalls.add(toolCallId);
|
|
115
|
+
onPart?.({
|
|
116
|
+
type: "tool_wait",
|
|
117
|
+
id: toolCallId
|
|
118
|
+
});
|
|
119
|
+
onPart?.({
|
|
120
|
+
type: "approval_request",
|
|
121
|
+
approval: {
|
|
122
|
+
kind: "approval",
|
|
123
|
+
id: toolCallId,
|
|
124
|
+
approvalKind: "confirm",
|
|
125
|
+
subject: "call",
|
|
126
|
+
canAllowDirectory: false,
|
|
127
|
+
...expiresAt ? { expiresAt } : {}
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
}
|
|
@@ -31,9 +31,14 @@ export interface CompletionChunk {
|
|
|
31
31
|
created?: number;
|
|
32
32
|
messageId?: string;
|
|
33
33
|
toolResult?: ToolResultChunk;
|
|
34
|
+
toolApproval?: ToolApprovalChunk;
|
|
34
35
|
choices?: CompletionChunkChoice[] | null;
|
|
35
36
|
usage?: TokenUsage;
|
|
36
37
|
}
|
|
38
|
+
export interface ToolApprovalChunk {
|
|
39
|
+
toolCallId: string;
|
|
40
|
+
expiresAt?: string;
|
|
41
|
+
}
|
|
37
42
|
export interface ToolResultChunk {
|
|
38
43
|
toolCallId: string;
|
|
39
44
|
content: string;
|
|
@@ -21,6 +21,14 @@ export interface StreamRequest {
|
|
|
21
21
|
allocation?: string | null;
|
|
22
22
|
useResponses?: boolean;
|
|
23
23
|
tools?: string[];
|
|
24
|
+
resume?: ToolApprovalAnswer;
|
|
25
|
+
}
|
|
26
|
+
export interface ToolApprovalAnswer {
|
|
27
|
+
messageId: string;
|
|
28
|
+
toolCallId: string;
|
|
29
|
+
allowed: boolean;
|
|
30
|
+
feedback?: string;
|
|
31
|
+
tool?: string;
|
|
24
32
|
}
|
|
25
33
|
export interface StreamHandlers {
|
|
26
34
|
onContent: (content: string) => void;
|
|
@@ -43,6 +51,9 @@ export type PartDelta = {
|
|
|
43
51
|
type: 'tool_update';
|
|
44
52
|
id: string;
|
|
45
53
|
args: string;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'tool_wait';
|
|
56
|
+
id: string;
|
|
46
57
|
} | {
|
|
47
58
|
type: 'tool_end';
|
|
48
59
|
id: string;
|
|
@@ -133,6 +144,7 @@ export interface ChatToolOption {
|
|
|
133
144
|
id: string;
|
|
134
145
|
label: string;
|
|
135
146
|
description?: string;
|
|
147
|
+
asksFirst?: boolean;
|
|
136
148
|
}
|
|
137
149
|
export interface ChatToolsAdapter {
|
|
138
150
|
list(): Promise<ChatToolOption[]>;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { toolHeaderFor } from "../agent/toolfmt.js";
|
|
2
|
+
import { useChatConfig } from "../core/config.js";
|
|
3
|
+
import { useChat, useChatStream } from "../core/ChatProvider.js";
|
|
4
|
+
import ActivityLine from "./agent/ActivityLine.js";
|
|
5
|
+
import { jsx } from "react/jsx-runtime";
|
|
6
|
+
import { useEffect, useState } from "react";
|
|
7
|
+
import { DateTime } from "luxon";
|
|
8
|
+
//#region src/ai/components/ChatActivity.tsx
|
|
9
|
+
/** An agent session's working line for the provider's reply, naming its current step. */
|
|
10
|
+
function ChatActivity() {
|
|
11
|
+
const { isStreaming, tools } = useChat();
|
|
12
|
+
const { streamingParts } = useChatStream();
|
|
13
|
+
const { strings } = useChatConfig();
|
|
14
|
+
const [startedAt, setStartedAt] = useState(null);
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
setStartedAt(isStreaming ? DateTime.now().toUTC().toISO() : null);
|
|
17
|
+
}, [isStreaming]);
|
|
18
|
+
if (!isStreaming) return null;
|
|
19
|
+
const waiting = streamingParts.some((p) => p.kind === "approval" && !p.resolved);
|
|
20
|
+
const running = streamingParts.findLast((p) => p.kind === "tool_call" && p.status === "running");
|
|
21
|
+
let word;
|
|
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;
|
|
24
|
+
return /* @__PURE__ */ jsx(ActivityLine, {
|
|
25
|
+
testId: "chat-activity",
|
|
26
|
+
startedAt,
|
|
27
|
+
word
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export { ChatActivity as default };
|
|
@@ -34,6 +34,11 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
34
34
|
const isTool = message.role === "tool";
|
|
35
35
|
const otherAuthor = isUser && showAuthor && message.author && currentUsername && message.author.username !== currentUsername ? message.author : void 0;
|
|
36
36
|
const isOtherUser = !!otherAuthor;
|
|
37
|
+
const asker = !isUser && showAuthor && currentUsername ? turnAuthor(message, allMessages) : void 0;
|
|
38
|
+
const otherAsker = asker && asker.username !== currentUsername ? asker : void 0;
|
|
39
|
+
const askerName = otherAsker ? otherAsker.name || otherAsker.username : void 0;
|
|
40
|
+
const partsMessage = askerName ? waitingOn(message, askerName) : message;
|
|
41
|
+
const approvalAnswer = askerName ? void 0 : onApprovalAnswer;
|
|
37
42
|
const isRightAligned = isUser && !isOtherUser;
|
|
38
43
|
const hasError = !!message.error;
|
|
39
44
|
const hasPastes = !!message.pastes?.length;
|
|
@@ -105,9 +110,9 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
105
110
|
return /* @__PURE__ */ jsxs("div", {
|
|
106
111
|
className: cx("group py-3 w-full", !flush && "px-4"),
|
|
107
112
|
children: [/* @__PURE__ */ jsx(AgentMessageParts, {
|
|
108
|
-
message,
|
|
113
|
+
message: partsMessage,
|
|
109
114
|
isStreaming,
|
|
110
|
-
onApprovalAnswer
|
|
115
|
+
onApprovalAnswer: approvalAnswer
|
|
111
116
|
}), !isUser && !isStreaming && (said || onRegenerate || meta) && /* @__PURE__ */ jsxs("div", {
|
|
112
117
|
className: "chat-actions -ml-1.5 mt-2 flex items-center gap-0.5",
|
|
113
118
|
children: [
|
|
@@ -185,15 +190,15 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
185
190
|
className: "mb-2",
|
|
186
191
|
children: /* @__PURE__ */ jsx("div", {
|
|
187
192
|
className: "flex items-center gap-x-1.5 text-sm theme-muted-text",
|
|
188
|
-
children: /* @__PURE__ */ jsx("span", { children: message.reasoningDuration ? tThinking.thoughtFor(formatDuration(message.reasoningDuration)) : tThinking.ranToolCalls(toolCallCount) })
|
|
193
|
+
children: /* @__PURE__ */ jsx("span", { children: message.reasoningDuration ? tThinking.thoughtFor(formatDuration(message.reasoningDuration)) : askerName ? tThinking.ranToolCallsAs(toolCallCount, askerName) : tThinking.ranToolCalls(toolCallCount) })
|
|
189
194
|
})
|
|
190
195
|
}),
|
|
191
196
|
!isUser && hasParts && /* @__PURE__ */ jsx("div", {
|
|
192
197
|
className: "mb-1",
|
|
193
198
|
children: /* @__PURE__ */ jsx(AgentMessageParts, {
|
|
194
|
-
message,
|
|
199
|
+
message: partsMessage,
|
|
195
200
|
isStreaming,
|
|
196
|
-
onApprovalAnswer
|
|
201
|
+
onApprovalAnswer: approvalAnswer
|
|
197
202
|
})
|
|
198
203
|
}),
|
|
199
204
|
otherAuthor && /* @__PURE__ */ jsx("span", {
|
|
@@ -294,5 +299,25 @@ function ChatMessage({ message, allMessages, isStreaming = false, latest = false
|
|
|
294
299
|
});
|
|
295
300
|
}
|
|
296
301
|
var ChatMessage_default = memo(ChatMessage);
|
|
302
|
+
/** The author of the user message that started the turn a reply belongs to.
|
|
303
|
+
* A reply that resumes after an approval hangs under the reply it resumes. */
|
|
304
|
+
function turnAuthor(message, allMessages) {
|
|
305
|
+
let parentId = message.parentId;
|
|
306
|
+
for (let hops = 0; parentId && hops < allMessages.length; hops++) {
|
|
307
|
+
const parent = allMessages.find((m) => m.id === parentId);
|
|
308
|
+
if (parent?.role !== "assistant") return parent?.role === "user" ? parent.author : void 0;
|
|
309
|
+
parentId = parent.parentId;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
function waitingOn(message, name) {
|
|
313
|
+
if (!message.parts?.some((p) => p.kind === "approval")) return message;
|
|
314
|
+
return {
|
|
315
|
+
...message,
|
|
316
|
+
parts: message.parts.map((p) => p.kind === "approval" ? {
|
|
317
|
+
...p,
|
|
318
|
+
waitingOn: name
|
|
319
|
+
} : p)
|
|
320
|
+
};
|
|
321
|
+
}
|
|
297
322
|
//#endregion
|
|
298
323
|
export { ChatMessage_default as default };
|
|
@@ -3,13 +3,13 @@ import { useChat } from "../core/ChatProvider.js";
|
|
|
3
3
|
import BlockedGroupBanner from "./BlockedGroupBanner.js";
|
|
4
4
|
import { ComposerControls, ComposerUsage, ConnectToolsLink } from "./ComposerChrome.js";
|
|
5
5
|
import ProviderIssueBanner from "./ProviderIssueBanner.js";
|
|
6
|
-
import { ShareButton } from "./ShareDialog.js";
|
|
6
|
+
import { ShareButton, ToolResultsSharedNote } from "./ShareDialog.js";
|
|
7
7
|
import SessionView from "./SessionView.js";
|
|
8
8
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
10
10
|
//#region src/ai/components/ChatThread.tsx
|
|
11
11
|
function ChatThread({ conversationId }) {
|
|
12
|
-
const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, isLoadingConversation, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
|
|
12
|
+
const { adapter, currentUser, currentConversation, loadConversation, loadProviders, sendMessage, retryMessage, answerToolApproval, tools, isLoadingConversation, navigateToBranch, queuedMessages, removeQueuedMessage } = useChat();
|
|
13
13
|
const { slots, strings } = useChatConfig();
|
|
14
14
|
const requestedConversationIdRef = useRef(null);
|
|
15
15
|
useEffect(() => {
|
|
@@ -52,6 +52,7 @@ function ChatThread({ conversationId }) {
|
|
|
52
52
|
onRetry: (id) => void retryMessage(id)
|
|
53
53
|
} : {},
|
|
54
54
|
onNavigateBranch: navigateToBranch,
|
|
55
|
+
...canWrite ? { onApprovalAnswer: answerToolApproval } : {},
|
|
55
56
|
showAuthor,
|
|
56
57
|
currentUsername: currentUser.username,
|
|
57
58
|
loading: isLoadingConversation && !currentConversation,
|
|
@@ -60,7 +61,7 @@ function ChatThread({ conversationId }) {
|
|
|
60
61
|
composer: {
|
|
61
62
|
disabled: isLoadingConversation || !canWrite,
|
|
62
63
|
...canWrite ? {} : { placeholder: strings.thread.viewOnly },
|
|
63
|
-
settingsLeft: /* @__PURE__ */ jsxs(Fragment, { children: [currentConversation && currentConversation.isOwner !== false
|
|
64
|
+
settingsLeft: /* @__PURE__ */ jsxs(Fragment, { children: [currentConversation && currentConversation.isOwner !== false ? /* @__PURE__ */ jsx(ShareButton, { conversationId }) : tools.length > 0 && /* @__PURE__ */ jsx(ToolResultsSharedNote, {}), /* @__PURE__ */ jsx(ConnectToolsLink, {})] }),
|
|
64
65
|
settingsRight: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(ComposerUsage, { children: slots.composerUsage }), /* @__PURE__ */ jsx(ComposerControls, {})] })
|
|
65
66
|
}
|
|
66
67
|
});
|
|
@@ -46,12 +46,23 @@ function RowSwitch() {
|
|
|
46
46
|
children: /* @__PURE__ */ jsx("span", { className: "size-3 rounded-full bg-white shadow-sm transition-transform duration-150 group-data-checked:translate-x-3 rtl:group-data-checked:-translate-x-3 motion-reduce:transition-none" })
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
|
+
function AsksFirst({ tool }) {
|
|
50
|
+
const t = useChatConfig().strings.composerMenu;
|
|
51
|
+
if (!tool.asksFirst) return null;
|
|
52
|
+
return /* @__PURE__ */ jsx("span", {
|
|
53
|
+
className: "ml-1.5 shrink-0 text-xs theme-muted-text",
|
|
54
|
+
children: t.asksFirst
|
|
55
|
+
});
|
|
56
|
+
}
|
|
49
57
|
function ToolText({ tool, descriptionId }) {
|
|
50
58
|
return /* @__PURE__ */ jsxs("span", {
|
|
51
59
|
className: "min-w-0 flex-1",
|
|
52
|
-
children: [/* @__PURE__ */
|
|
53
|
-
className: "
|
|
54
|
-
children:
|
|
60
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
61
|
+
className: "flex min-w-0 items-baseline",
|
|
62
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
63
|
+
className: "truncate",
|
|
64
|
+
children: tool.label
|
|
65
|
+
}), /* @__PURE__ */ jsx(AsksFirst, { tool })]
|
|
55
66
|
}), tool.description && /* @__PURE__ */ jsx("span", {
|
|
56
67
|
id: descriptionId,
|
|
57
68
|
className: "block truncate text-xs theme-muted-text",
|
|
@@ -106,9 +117,9 @@ function ToolsModalRow({ tool, enabled, onChange }) {
|
|
|
106
117
|
className: "flex cursor-pointer items-center gap-3 rounded-md px-2 py-2 hover:bg-(--theme-hover)",
|
|
107
118
|
children: [/* @__PURE__ */ jsxs("span", {
|
|
108
119
|
className: "min-w-0 flex-1",
|
|
109
|
-
children: [/* @__PURE__ */
|
|
110
|
-
className: "
|
|
111
|
-
children: tool.label
|
|
120
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
121
|
+
className: "flex items-baseline text-sm",
|
|
122
|
+
children: [tool.label, /* @__PURE__ */ jsx(AsksFirst, { tool })]
|
|
112
123
|
}), tool.description && /* @__PURE__ */ jsx("span", {
|
|
113
124
|
id: descriptionId,
|
|
114
125
|
className: "block text-xs theme-muted-text",
|
|
@@ -4,6 +4,7 @@ import DragOverlay from "./DragOverlay.js";
|
|
|
4
4
|
import ChatInput from "./ChatInput.js";
|
|
5
5
|
import { escapeClosesLayer } from "../core/useKeyboardShortcuts.js";
|
|
6
6
|
import ChatMessageList from "./ChatMessageList.js";
|
|
7
|
+
import ChatActivity from "./ChatActivity.js";
|
|
7
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
8
9
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
10
|
//#region src/ai/components/SessionView.tsx
|
|
@@ -36,9 +37,11 @@ function SessionView({ sessionKey, messages, allMessages, streamingMessage, queu
|
|
|
36
37
|
inputRef.current?.addFiles(files);
|
|
37
38
|
}, []);
|
|
38
39
|
const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped });
|
|
40
|
+
const activity = composer?.activity ?? (!turn && running ? /* @__PURE__ */ jsx(ChatActivity, {}) : void 0);
|
|
39
41
|
const input = /* @__PURE__ */ jsx(ChatInput, {
|
|
40
42
|
ref: inputRef,
|
|
41
43
|
...composer,
|
|
44
|
+
activity,
|
|
42
45
|
onSend,
|
|
43
46
|
conversationId: sessionKey,
|
|
44
47
|
disabled: loading || !!composer?.disabled,
|
|
@@ -87,7 +90,7 @@ function SessionView({ sessionKey, messages, allMessages, streamingMessage, queu
|
|
|
87
90
|
...footer ? { footer } : {},
|
|
88
91
|
...label ? { label } : {},
|
|
89
92
|
...flush ? { flush } : {},
|
|
90
|
-
|
|
93
|
+
workingIndicator: workingIndicator ?? !activity,
|
|
91
94
|
inputElement: input
|
|
92
95
|
}),
|
|
93
96
|
error && input
|
|
@@ -8,4 +8,7 @@ export declare function ShareButton({ conversationId, disabled, }: {
|
|
|
8
8
|
conversationId: string;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
}): import("react").JSX.Element | null;
|
|
11
|
+
/** Says that tool results in a shared conversation are visible to the
|
|
12
|
+
* people it is shared with. */
|
|
13
|
+
export declare function ToolResultsSharedNote(): import("react").JSX.Element;
|
|
11
14
|
export {};
|
|
@@ -249,21 +249,57 @@ function ShareDialog({ conversationId, isOpen, onClose }) {
|
|
|
249
249
|
}
|
|
250
250
|
function ShareButton({ conversationId, disabled = false }) {
|
|
251
251
|
const { strings } = useChatConfig();
|
|
252
|
-
const { adapter } = useChat();
|
|
252
|
+
const { adapter, tools } = useChat();
|
|
253
253
|
const [isOpen, setIsOpen] = useState(false);
|
|
254
|
+
const shared = useShared(conversationId, tools.length > 0 && !isOpen);
|
|
254
255
|
if (!adapter.sharing) return null;
|
|
255
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
256
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
257
|
+
/* @__PURE__ */ jsxs("button", {
|
|
258
|
+
type: "button",
|
|
259
|
+
onClick: () => setIsOpen(true),
|
|
260
|
+
disabled,
|
|
261
|
+
className: "flex items-center gap-1 rounded-md px-1.5 py-0.5 text-[11px] theme-muted-text transition-colors hover:theme-muted-panel hover:theme-text disabled:opacity-50",
|
|
262
|
+
title: strings.shareConversation.shareButton,
|
|
263
|
+
children: [/* @__PURE__ */ jsx(ShareIcon, { className: "h-3 w-3" }), strings.shareConversation.shareButton]
|
|
264
|
+
}),
|
|
265
|
+
shared && /* @__PURE__ */ jsx(ToolResultsSharedNote, {}),
|
|
266
|
+
/* @__PURE__ */ jsx(ShareDialog, {
|
|
267
|
+
conversationId,
|
|
268
|
+
isOpen,
|
|
269
|
+
onClose: () => setIsOpen(false)
|
|
270
|
+
})
|
|
271
|
+
] });
|
|
272
|
+
}
|
|
273
|
+
/** Says that tool results in a shared conversation are visible to the
|
|
274
|
+
* people it is shared with. */
|
|
275
|
+
function ToolResultsSharedNote() {
|
|
276
|
+
const { strings } = useChatConfig();
|
|
277
|
+
return /* @__PURE__ */ jsx("span", {
|
|
278
|
+
className: "text-[11px] theme-muted-text",
|
|
279
|
+
children: strings.shareConversation.toolResultsShared
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
/** Whether anyone else can read the conversation, checked while `check` is
|
|
283
|
+
* true: tool results in it are visible to them. */
|
|
284
|
+
function useShared(conversationId, check) {
|
|
285
|
+
const { adapter } = useChat();
|
|
286
|
+
const sharing = adapter.sharing;
|
|
287
|
+
const [shared, setShared] = useState(false);
|
|
288
|
+
useEffect(() => {
|
|
289
|
+
if (!check || !sharing) return;
|
|
290
|
+
let current = true;
|
|
291
|
+
sharing.listPermissions(conversationId).then((permissions) => {
|
|
292
|
+
if (current) setShared(permissions.length > 0);
|
|
293
|
+
}, () => {});
|
|
294
|
+
return () => {
|
|
295
|
+
current = false;
|
|
296
|
+
};
|
|
297
|
+
}, [
|
|
263
298
|
conversationId,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
299
|
+
check,
|
|
300
|
+
sharing
|
|
301
|
+
]);
|
|
302
|
+
return shared;
|
|
267
303
|
}
|
|
268
304
|
//#endregion
|
|
269
|
-
export { ShareButton, ShareDialog as default };
|
|
305
|
+
export { ShareButton, ToolResultsSharedNote, ShareDialog as default };
|
|
@@ -20,7 +20,7 @@ type RenderItem = {
|
|
|
20
20
|
kind: 'rollup';
|
|
21
21
|
members: RunMember[];
|
|
22
22
|
};
|
|
23
|
-
export declare function buildRenderItems(parts: MessagePart[]): RenderItem[];
|
|
23
|
+
export declare function buildRenderItems(parts: MessagePart[], hostDrawn?: (toolName: string) => boolean): RenderItem[];
|
|
24
24
|
export default function AgentMessageParts({ message, isStreaming, onApprovalAnswer, }: {
|
|
25
25
|
message: PartRendererProps['message'];
|
|
26
26
|
isStreaming?: boolean;
|
|
@@ -19,6 +19,7 @@ import { chatProseClasses } from "../../ui/prose.js";
|
|
|
19
19
|
import { Reasoning, ReasoningBody } from "../Reasoning.js";
|
|
20
20
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
21
21
|
import { memo, useEffect, useId, useMemo, useState } from "react";
|
|
22
|
+
import { DateTime } from "luxon";
|
|
22
23
|
//#region src/ai/components/agent/AgentMessageParts.tsx
|
|
23
24
|
var COLLAPSED_RESULT_LINES = 12;
|
|
24
25
|
function LineSpans({ text, tokens, emphasis }) {
|
|
@@ -207,6 +208,13 @@ function ToolStatusDot({ part }) {
|
|
|
207
208
|
color: "var(--theme-muted-text-color)"
|
|
208
209
|
})
|
|
209
210
|
});
|
|
211
|
+
if (part.status === "waiting") return /* @__PURE__ */ jsx(StatusMark, {
|
|
212
|
+
status: t.approvalLabel,
|
|
213
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
214
|
+
className: "leading-none text-(--theme-accent)",
|
|
215
|
+
children: "●"
|
|
216
|
+
})
|
|
217
|
+
});
|
|
210
218
|
if (part.status === "error") return /* @__PURE__ */ jsx(StatusMark, {
|
|
211
219
|
status: t.failed,
|
|
212
220
|
children: /* @__PURE__ */ jsx("span", {
|
|
@@ -224,7 +232,10 @@ function ToolStatusDot({ part }) {
|
|
|
224
232
|
}
|
|
225
233
|
/** A part's toggle row: its status mark and name, across the transcript's width. */
|
|
226
234
|
var rowSummaryClasses = "min-h-6 w-full";
|
|
227
|
-
|
|
235
|
+
/** A call's row: its status and signature, opening to its full arguments and
|
|
236
|
+
* result. `children` is a host's drawing of the call, in place of the
|
|
237
|
+
* one-line summary. */
|
|
238
|
+
function ToolCallBlock({ part, children }) {
|
|
228
239
|
const { strings } = useChatConfig();
|
|
229
240
|
const t = strings.agentTranscript;
|
|
230
241
|
const [expanded, setExpanded] = useState(false);
|
|
@@ -271,7 +282,11 @@ function ToolCallBlock({ part }) {
|
|
|
271
282
|
children: fullToolArgs(header.arg, part.args)
|
|
272
283
|
}), /* @__PURE__ */ jsx(ResultBody, { part })]
|
|
273
284
|
}),
|
|
274
|
-
|
|
285
|
+
children && /* @__PURE__ */ jsx("div", {
|
|
286
|
+
className: "ml-5 mt-1.5 min-w-0",
|
|
287
|
+
children
|
|
288
|
+
}),
|
|
289
|
+
!children && !isDiff && !expanded && part.status !== "waiting" && /* @__PURE__ */ jsx("div", {
|
|
275
290
|
className: cx("ml-5 mt-0.5 truncate text-[13px]", part.status === "error" ? "text-(--theme-error-text)" : "theme-muted-text"),
|
|
276
291
|
children: summary
|
|
277
292
|
}),
|
|
@@ -390,11 +405,31 @@ function SubagentCard({ part }) {
|
|
|
390
405
|
})]
|
|
391
406
|
});
|
|
392
407
|
}
|
|
408
|
+
/** Whether the ISO time has passed, turning true when it does. */
|
|
409
|
+
function useExpired(expiresAt) {
|
|
410
|
+
const [expired, setExpired] = useState(() => !!expiresAt && DateTime.fromISO(expiresAt) <= DateTime.now());
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (!expiresAt || expired) return;
|
|
413
|
+
let timer;
|
|
414
|
+
const arm = () => {
|
|
415
|
+
const left = DateTime.fromISO(expiresAt).diffNow().toMillis();
|
|
416
|
+
if (left <= 0) {
|
|
417
|
+
setExpired(true);
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
timer = setTimeout(arm, Math.min(left, 864e5));
|
|
421
|
+
};
|
|
422
|
+
arm();
|
|
423
|
+
return () => clearTimeout(timer);
|
|
424
|
+
}, [expiresAt, expired]);
|
|
425
|
+
return expired;
|
|
426
|
+
}
|
|
393
427
|
function ConfirmApproval({ part, onAnswer }) {
|
|
394
428
|
const { strings } = useChatConfig();
|
|
395
429
|
const { common } = useStrings();
|
|
396
430
|
const t = strings.agentTranscript;
|
|
397
431
|
const [feedback, setFeedback] = useState(null);
|
|
432
|
+
const expired = useExpired(part.expiresAt);
|
|
398
433
|
const answer = (a) => onAnswer?.(part.id, a);
|
|
399
434
|
const [allowLabel, denyLabel] = {
|
|
400
435
|
command: [t.allowOnce, t.deny],
|
|
@@ -411,9 +446,12 @@ function ConfirmApproval({ part, onAnswer }) {
|
|
|
411
446
|
className: "text-xs theme-muted-text",
|
|
412
447
|
children: part.reason
|
|
413
448
|
}),
|
|
414
|
-
|
|
449
|
+
expired ? /* @__PURE__ */ jsx("div", {
|
|
415
450
|
className: "text-xs theme-muted-text",
|
|
416
|
-
children: t.
|
|
451
|
+
children: t.approvalRequestExpired
|
|
452
|
+
}) : !onAnswer ? /* @__PURE__ */ jsx("div", {
|
|
453
|
+
className: "text-xs theme-muted-text",
|
|
454
|
+
children: part.waitingOn ? t.approvalWaitingOn(part.waitingOn) : t.approvalPending
|
|
417
455
|
}) : feedback === null ? /* @__PURE__ */ jsxs("div", {
|
|
418
456
|
className: "flex flex-wrap gap-2",
|
|
419
457
|
children: [
|
|
@@ -704,6 +742,21 @@ function TextBlock({ part, isStreaming }) {
|
|
|
704
742
|
}
|
|
705
743
|
function PartDispatch(props) {
|
|
706
744
|
const { slots } = useChatConfig();
|
|
745
|
+
const part = props.part;
|
|
746
|
+
const Tool = part.kind === "tool_call" ? slots.toolRenderers?.[part.name] : void 0;
|
|
747
|
+
if (Tool && part.kind === "tool_call") {
|
|
748
|
+
const approval = props.message.parts?.find((p) => p.kind === "approval" && p.id === part.id);
|
|
749
|
+
return /* @__PURE__ */ jsx(ToolCallBlock, {
|
|
750
|
+
part,
|
|
751
|
+
children: /* @__PURE__ */ jsx(Tool, {
|
|
752
|
+
part,
|
|
753
|
+
message: props.message,
|
|
754
|
+
...approval ? { approval } : {},
|
|
755
|
+
...props.isStreaming !== void 0 ? { isStreaming: props.isStreaming } : {},
|
|
756
|
+
...props.onApprovalAnswer ? { onApprovalAnswer: props.onApprovalAnswer } : {}
|
|
757
|
+
})
|
|
758
|
+
});
|
|
759
|
+
}
|
|
707
760
|
const Override = slots.partRenderers?.[props.part.kind];
|
|
708
761
|
if (Override) return /* @__PURE__ */ jsx(Override, { ...props });
|
|
709
762
|
return /* @__PURE__ */ jsx(BuiltinPart, {
|
|
@@ -752,7 +805,7 @@ function mergeReasoning(parts) {
|
|
|
752
805
|
...durationMs > 0 ? { durationMs } : {}
|
|
753
806
|
};
|
|
754
807
|
}
|
|
755
|
-
function buildRenderItems(parts) {
|
|
808
|
+
function buildRenderItems(parts, hostDrawn = () => false) {
|
|
756
809
|
const items = [];
|
|
757
810
|
let run = [];
|
|
758
811
|
const emitThoughts = (members) => {
|
|
@@ -793,8 +846,12 @@ function buildRenderItems(parts) {
|
|
|
793
846
|
emitThoughts(run.slice(end));
|
|
794
847
|
run = [];
|
|
795
848
|
};
|
|
849
|
+
const decided = new Set(parts.flatMap((p) => p.kind === "tool_call" && p.decision ? [p.id] : []));
|
|
850
|
+
const drawnByHost = new Set(parts.flatMap((p) => p.kind === "tool_call" && hostDrawn(p.name) ? [p.id] : []));
|
|
796
851
|
parts.forEach((part, index) => {
|
|
797
|
-
if (part.kind === "
|
|
852
|
+
if (part.kind === "approval" && part.resolved && decided.has(part.id)) return;
|
|
853
|
+
if (part.kind === "approval" && drawnByHost.has(part.id)) return;
|
|
854
|
+
if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && !part.decision && !drawnByHost.has(part.id) && isGroupableTool(part.name)) {
|
|
798
855
|
run.push({
|
|
799
856
|
part,
|
|
800
857
|
index
|
|
@@ -838,7 +895,9 @@ function RollupGroup({ members, message }) {
|
|
|
838
895
|
}
|
|
839
896
|
function AgentMessageParts({ message, isStreaming, onApprovalAnswer }) {
|
|
840
897
|
const parts = message.parts ?? [];
|
|
841
|
-
const
|
|
898
|
+
const { slots } = useChatConfig();
|
|
899
|
+
const toolRenderers = slots.toolRenderers;
|
|
900
|
+
const items = useMemo(() => buildRenderItems(parts, (name) => !!toolRenderers?.[name]), [parts, toolRenderers]);
|
|
842
901
|
const lastIndex = parts.length - 1;
|
|
843
902
|
return /* @__PURE__ */ jsx("div", {
|
|
844
903
|
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 { 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, 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 {
|
|
@@ -41,6 +41,8 @@ interface ChatContextValue {
|
|
|
41
41
|
deleteConversation: (id: string) => Promise<boolean>;
|
|
42
42
|
sendMessage: (content: string, attachmentIds?: string[], parentId?: string, attachmentMeta?: AttachmentMeta) => Promise<boolean>;
|
|
43
43
|
retryMessage: (messageId: string) => Promise<boolean>;
|
|
44
|
+
/** Answers a tool call waiting for approval, resuming the reply it paused. */
|
|
45
|
+
answerToolApproval: (approvalId: string, answer: ApprovalAnswerValue) => Promise<void>;
|
|
44
46
|
setSelectedProvider: (providerId: string | null) => void;
|
|
45
47
|
setSelectedAllocation: (allocation: string | null) => void;
|
|
46
48
|
setToolEnabled: (id: string, enabled: boolean) => void;
|
|
@@ -44,6 +44,20 @@ function heldSendArgs(held) {
|
|
|
44
44
|
preceding.length > 0 ? preceding : void 0
|
|
45
45
|
];
|
|
46
46
|
}
|
|
47
|
+
/** The wire history for the messages shown, oldest first. */
|
|
48
|
+
function historyMessages(messages, selectedProvider) {
|
|
49
|
+
return messages.filter((m) => m.content || m.toolCalls?.length || m.parts?.length).flatMap((m) => {
|
|
50
|
+
const msg = {
|
|
51
|
+
role: m.role,
|
|
52
|
+
content: m.content || null
|
|
53
|
+
};
|
|
54
|
+
if (m.role === "assistant" && m.reasoning) msg.reasoning_content = m.reasoning;
|
|
55
|
+
if (m.role === "assistant" && m.toolCalls?.length) msg.tool_calls = m.toolCalls;
|
|
56
|
+
if (m.role === "assistant" && m.responsesOutput?.length && m.model === selectedProvider) msg.responses_output = m.responsesOutput;
|
|
57
|
+
if (m.role === "tool" && m.toolCallId) msg.tool_call_id = m.toolCallId;
|
|
58
|
+
return m.role === "assistant" ? toolTurnMessages(m.parts, msg) : [msg];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
47
61
|
var NO_TOOLS = [];
|
|
48
62
|
var ChatContext = createContext(void 0);
|
|
49
63
|
var ChatStreamContext = createContext(void 0);
|
|
@@ -291,6 +305,124 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
291
305
|
state.currentConversation,
|
|
292
306
|
navigation
|
|
293
307
|
]);
|
|
308
|
+
const streamReply = useCallback(async (targetConversationId, request, parentId, onFailure, routePart) => {
|
|
309
|
+
backgroundStreamsRef.current.set(targetConversationId, {
|
|
310
|
+
content: "",
|
|
311
|
+
reasoning: "",
|
|
312
|
+
parts: [],
|
|
313
|
+
messageId: null,
|
|
314
|
+
isActive: true
|
|
315
|
+
});
|
|
316
|
+
dispatch({
|
|
317
|
+
type: "SET_STREAMING",
|
|
318
|
+
isStreaming: true,
|
|
319
|
+
conversationId: targetConversationId
|
|
320
|
+
});
|
|
321
|
+
dispatch({
|
|
322
|
+
type: "CLEAR_STREAMING_CONTENT",
|
|
323
|
+
conversationId: targetConversationId
|
|
324
|
+
});
|
|
325
|
+
const controller = new AbortController();
|
|
326
|
+
abortControllerRef.current = controller;
|
|
327
|
+
try {
|
|
328
|
+
let thinkingStart = null;
|
|
329
|
+
let thinkingEnd = null;
|
|
330
|
+
const markStreamStart = () => {
|
|
331
|
+
if (thinkingStart === null) thinkingStart = Date.now();
|
|
332
|
+
};
|
|
333
|
+
let liveParts = [];
|
|
334
|
+
const { content: assistantContent, messageId: assistantMessageId, toolCalls, model: resolvedModel, reasoning: assistantReasoning, responsesOutput } = await adapter.streamCompletion(request, {
|
|
335
|
+
onContent: (chunk) => {
|
|
336
|
+
markStreamStart();
|
|
337
|
+
onStreamingChunk(chunk);
|
|
338
|
+
},
|
|
339
|
+
onReasoning: (reasoning) => {
|
|
340
|
+
markStreamStart();
|
|
341
|
+
thinkingEnd = Date.now();
|
|
342
|
+
dispatch({
|
|
343
|
+
type: "APPEND_STREAMING_REASONING",
|
|
344
|
+
reasoning,
|
|
345
|
+
conversationId: targetConversationId
|
|
346
|
+
});
|
|
347
|
+
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
348
|
+
if (bgStream) bgStream.reasoning += reasoning;
|
|
349
|
+
},
|
|
350
|
+
onMessageId: (messageId) => {
|
|
351
|
+
markStreamStart();
|
|
352
|
+
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
353
|
+
if (bgStream) bgStream.messageId = messageId;
|
|
354
|
+
},
|
|
355
|
+
onPart: (delta) => {
|
|
356
|
+
markStreamStart();
|
|
357
|
+
if (routePart?.(delta)) return;
|
|
358
|
+
liveParts = applyPartDelta(liveParts, delta);
|
|
359
|
+
dispatch({
|
|
360
|
+
type: "APPLY_STREAMING_PART",
|
|
361
|
+
delta,
|
|
362
|
+
conversationId: targetConversationId
|
|
363
|
+
});
|
|
364
|
+
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
365
|
+
if (bgStream) bgStream.parts = liveParts;
|
|
366
|
+
}
|
|
367
|
+
}, controller.signal);
|
|
368
|
+
const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
|
|
369
|
+
const assistantMessage = {
|
|
370
|
+
id: assistantMessageId || crypto.randomUUID(),
|
|
371
|
+
parentId,
|
|
372
|
+
role: "assistant",
|
|
373
|
+
content: assistantContent || (toolCalls.length > 0 ? null : ""),
|
|
374
|
+
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
375
|
+
model: request.useResponses ? request.model : resolvedModel || request.model,
|
|
376
|
+
providerId: state.selectedProvider,
|
|
377
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
378
|
+
reasoning: assistantReasoning || void 0,
|
|
379
|
+
reasoningDuration: thinkingDuration ?? void 0,
|
|
380
|
+
responsesOutput: responsesOutput.length ? responsesOutput : void 0,
|
|
381
|
+
...liveParts.length ? { parts: finalizeParts(liveParts) } : {}
|
|
382
|
+
};
|
|
383
|
+
dispatch({
|
|
384
|
+
type: "ADD_MESSAGE",
|
|
385
|
+
message: assistantMessage,
|
|
386
|
+
conversationId: targetConversationId
|
|
387
|
+
});
|
|
388
|
+
return "sent";
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof DOMException && error.name === "AbortError") return "aborted";
|
|
391
|
+
console.error("Failed to send message:", error);
|
|
392
|
+
onFailure(errorText(error));
|
|
393
|
+
return "failed";
|
|
394
|
+
} finally {
|
|
395
|
+
abortControllerRef.current = null;
|
|
396
|
+
const { unrevealed } = stopAnimationLoop();
|
|
397
|
+
if (unrevealed) dispatch({
|
|
398
|
+
type: "APPEND_STREAMING_CONTENT",
|
|
399
|
+
content: unrevealed,
|
|
400
|
+
conversationId: targetConversationId
|
|
401
|
+
});
|
|
402
|
+
streamingConversationIdRef.current = null;
|
|
403
|
+
backgroundStreamsRef.current.delete(targetConversationId);
|
|
404
|
+
setTimeout(() => {
|
|
405
|
+
streamingToConversationRef.current = null;
|
|
406
|
+
}, 500);
|
|
407
|
+
dispatch({
|
|
408
|
+
type: "SET_STREAMING",
|
|
409
|
+
isStreaming: false,
|
|
410
|
+
conversationId: targetConversationId
|
|
411
|
+
});
|
|
412
|
+
dispatch({
|
|
413
|
+
type: "CLEAR_STREAMING_CONTENT",
|
|
414
|
+
conversationId: targetConversationId
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}, [
|
|
418
|
+
adapter,
|
|
419
|
+
state.selectedProvider,
|
|
420
|
+
onStreamingChunk,
|
|
421
|
+
stopAnimationLoop,
|
|
422
|
+
streamingConversationIdRef,
|
|
423
|
+
backgroundStreamsRef,
|
|
424
|
+
abortControllerRef
|
|
425
|
+
]);
|
|
294
426
|
const sendMessage = useCallback(async (content, attachmentIds, parentId, attachmentMeta, precedingMessages) => {
|
|
295
427
|
if (isSendingRef.current) {
|
|
296
428
|
const convId = currentConversationIdRef.current;
|
|
@@ -405,13 +537,6 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
405
537
|
message: userMessage,
|
|
406
538
|
conversationId: targetConversationId
|
|
407
539
|
});
|
|
408
|
-
backgroundStreamsRef.current.set(targetConversationId, {
|
|
409
|
-
content: "",
|
|
410
|
-
reasoning: "",
|
|
411
|
-
parts: [],
|
|
412
|
-
messageId: null,
|
|
413
|
-
isActive: true
|
|
414
|
-
});
|
|
415
540
|
const setMessageError = (errorMsg) => {
|
|
416
541
|
dispatch({
|
|
417
542
|
type: "UPDATE_MESSAGE",
|
|
@@ -421,17 +546,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
421
546
|
});
|
|
422
547
|
};
|
|
423
548
|
const apiMessages = [
|
|
424
|
-
...(isNewConversation ? [] : state.currentConversation?.messages || []
|
|
425
|
-
const msg = {
|
|
426
|
-
role: m.role,
|
|
427
|
-
content: m.content || null
|
|
428
|
-
};
|
|
429
|
-
if (m.role === "assistant" && m.reasoning) msg.reasoning_content = m.reasoning;
|
|
430
|
-
if (m.role === "assistant" && m.toolCalls?.length) msg.tool_calls = m.toolCalls;
|
|
431
|
-
if (m.role === "assistant" && m.responsesOutput?.length && m.model === state.selectedProvider) msg.responses_output = m.responsesOutput;
|
|
432
|
-
if (m.role === "tool" && m.toolCallId) msg.tool_call_id = m.toolCallId;
|
|
433
|
-
return m.role === "assistant" ? toolTurnMessages(m.parts, msg) : [msg];
|
|
434
|
-
}),
|
|
549
|
+
...historyMessages(isNewConversation ? [] : state.currentConversation?.messages || [], state.selectedProvider),
|
|
435
550
|
...precedingApiMessages,
|
|
436
551
|
{
|
|
437
552
|
role: "user",
|
|
@@ -439,28 +554,10 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
439
554
|
attachment_ids: attachmentIds?.length ? attachmentIds : void 0
|
|
440
555
|
}
|
|
441
556
|
];
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
isStreaming: true,
|
|
445
|
-
conversationId: targetConversationId
|
|
446
|
-
});
|
|
447
|
-
dispatch({
|
|
448
|
-
type: "CLEAR_STREAMING_CONTENT",
|
|
449
|
-
conversationId: targetConversationId
|
|
450
|
-
});
|
|
451
|
-
const controller = new AbortController();
|
|
452
|
-
abortControllerRef.current = controller;
|
|
453
|
-
let sendSucceeded = false;
|
|
454
|
-
let wasAborted = false;
|
|
557
|
+
const useResponses = state.models.find((m) => m.id === modelString)?.supports_responses === true;
|
|
558
|
+
let outcome = "failed";
|
|
455
559
|
try {
|
|
456
|
-
|
|
457
|
-
let thinkingStart = null;
|
|
458
|
-
let thinkingEnd = null;
|
|
459
|
-
const markStreamStart = () => {
|
|
460
|
-
if (thinkingStart === null) thinkingStart = Date.now();
|
|
461
|
-
};
|
|
462
|
-
let liveParts = [];
|
|
463
|
-
const { content: assistantContent, messageId: assistantMessageId, toolCalls, model: resolvedModel, reasoning: assistantReasoning, responsesOutput } = await adapter.streamCompletion({
|
|
560
|
+
outcome = await streamReply(targetConversationId, {
|
|
464
561
|
model: modelString,
|
|
465
562
|
messages: apiMessages,
|
|
466
563
|
conversationId,
|
|
@@ -469,94 +566,11 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
469
566
|
allocation: state.selectedAllocation,
|
|
470
567
|
useResponses,
|
|
471
568
|
...tools ? { tools: enabledToolIds } : {}
|
|
472
|
-
},
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
onStreamingChunk(chunk);
|
|
476
|
-
},
|
|
477
|
-
onReasoning: (reasoning) => {
|
|
478
|
-
markStreamStart();
|
|
479
|
-
thinkingEnd = Date.now();
|
|
480
|
-
dispatch({
|
|
481
|
-
type: "APPEND_STREAMING_REASONING",
|
|
482
|
-
reasoning,
|
|
483
|
-
conversationId: targetConversationId
|
|
484
|
-
});
|
|
485
|
-
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
486
|
-
if (bgStream) bgStream.reasoning += reasoning;
|
|
487
|
-
},
|
|
488
|
-
onMessageId: (messageId) => {
|
|
489
|
-
markStreamStart();
|
|
490
|
-
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
491
|
-
if (bgStream) bgStream.messageId = messageId;
|
|
492
|
-
},
|
|
493
|
-
onPart: (delta) => {
|
|
494
|
-
markStreamStart();
|
|
495
|
-
liveParts = applyPartDelta(liveParts, delta);
|
|
496
|
-
dispatch({
|
|
497
|
-
type: "APPLY_STREAMING_PART",
|
|
498
|
-
delta,
|
|
499
|
-
conversationId: targetConversationId
|
|
500
|
-
});
|
|
501
|
-
const bgStream = backgroundStreamsRef.current.get(targetConversationId);
|
|
502
|
-
if (bgStream) bgStream.parts = liveParts;
|
|
503
|
-
}
|
|
504
|
-
}, controller.signal);
|
|
505
|
-
const thinkingDuration = assistantReasoning && thinkingStart !== null ? (thinkingEnd ?? Date.now()) - thinkingStart : null;
|
|
506
|
-
const assistantMessage = {
|
|
507
|
-
id: assistantMessageId || crypto.randomUUID(),
|
|
508
|
-
parentId: userMessageId,
|
|
509
|
-
role: "assistant",
|
|
510
|
-
content: assistantContent || (toolCalls.length > 0 ? null : ""),
|
|
511
|
-
toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
|
|
512
|
-
model: useResponses ? modelString : resolvedModel || modelString,
|
|
513
|
-
providerId: state.selectedProvider,
|
|
514
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
515
|
-
reasoning: assistantReasoning || void 0,
|
|
516
|
-
reasoningDuration: thinkingDuration ?? void 0,
|
|
517
|
-
responsesOutput: responsesOutput.length ? responsesOutput : void 0,
|
|
518
|
-
...liveParts.length ? { parts: finalizeParts(liveParts) } : {}
|
|
519
|
-
};
|
|
520
|
-
dispatch({
|
|
521
|
-
type: "ADD_MESSAGE",
|
|
522
|
-
message: assistantMessage,
|
|
523
|
-
conversationId: targetConversationId
|
|
524
|
-
});
|
|
525
|
-
if (isNewConversation) loadConversations();
|
|
526
|
-
sendSucceeded = true;
|
|
527
|
-
return true;
|
|
528
|
-
} catch (error) {
|
|
529
|
-
if (error instanceof DOMException && error.name === "AbortError") {
|
|
530
|
-
wasAborted = true;
|
|
531
|
-
return false;
|
|
532
|
-
}
|
|
533
|
-
console.error("Failed to send message:", error);
|
|
534
|
-
setMessageError(errorText(error));
|
|
535
|
-
return false;
|
|
569
|
+
}, userMessageId, setMessageError);
|
|
570
|
+
if (outcome === "sent" && isNewConversation) loadConversations();
|
|
571
|
+
return outcome === "sent";
|
|
536
572
|
} finally {
|
|
537
|
-
|
|
538
|
-
const { unrevealed } = stopAnimationLoop();
|
|
539
|
-
if (unrevealed) dispatch({
|
|
540
|
-
type: "APPEND_STREAMING_CONTENT",
|
|
541
|
-
content: unrevealed,
|
|
542
|
-
conversationId: targetConversationId
|
|
543
|
-
});
|
|
544
|
-
streamingConversationIdRef.current = null;
|
|
545
|
-
backgroundStreamsRef.current.delete(targetConversationId);
|
|
546
|
-
setTimeout(() => {
|
|
547
|
-
streamingToConversationRef.current = null;
|
|
548
|
-
}, 500);
|
|
549
|
-
dispatch({
|
|
550
|
-
type: "SET_STREAMING",
|
|
551
|
-
isStreaming: false,
|
|
552
|
-
conversationId: targetConversationId
|
|
553
|
-
});
|
|
554
|
-
dispatch({
|
|
555
|
-
type: "CLEAR_STREAMING_CONTENT",
|
|
556
|
-
conversationId: targetConversationId
|
|
557
|
-
});
|
|
558
|
-
if (wasAborted) isSendingRef.current = false;
|
|
559
|
-
else if (sendSucceeded) {
|
|
573
|
+
if (outcome === "sent") {
|
|
560
574
|
const next = heldSendArgs(pendingQueueRef.current);
|
|
561
575
|
pendingQueueRef.current = [];
|
|
562
576
|
dispatch({ type: "CLEAR_QUEUED_MESSAGES" });
|
|
@@ -581,16 +595,93 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
581
595
|
createConversation,
|
|
582
596
|
loadConversations,
|
|
583
597
|
navigation,
|
|
584
|
-
onStreamingChunk,
|
|
585
598
|
startStreaming,
|
|
586
|
-
|
|
587
|
-
streamingConversationIdRef,
|
|
588
|
-
backgroundStreamsRef,
|
|
589
|
-
abortControllerRef
|
|
599
|
+
streamReply
|
|
590
600
|
]);
|
|
591
601
|
useEffect(() => {
|
|
592
602
|
sendMessageRef.current = sendMessage;
|
|
593
603
|
}, [sendMessage]);
|
|
604
|
+
const answerToolApproval = useCallback(async (approvalId, answer) => {
|
|
605
|
+
const conversation = state.currentConversation;
|
|
606
|
+
const modelString = state.selectedProvider;
|
|
607
|
+
if (answer.kind !== "confirm" || !conversation || !modelString || isSendingRef.current) return;
|
|
608
|
+
const pausedAt = conversation.messages.findIndex((m) => m.role === "assistant" && m.parts?.some((p) => p.kind === "approval" && p.id === approvalId && !p.resolved));
|
|
609
|
+
const paused = conversation.messages[pausedAt];
|
|
610
|
+
if (!paused) return;
|
|
611
|
+
isSendingRef.current = true;
|
|
612
|
+
const targetConversationId = conversation.id;
|
|
613
|
+
const t = resolvedConfig.strings.agentTranscript;
|
|
614
|
+
let pausedParts = paused.parts ?? [];
|
|
615
|
+
const updatePaused = (updates) => dispatch({
|
|
616
|
+
type: "UPDATE_MESSAGE",
|
|
617
|
+
messageId: paused.id,
|
|
618
|
+
updates,
|
|
619
|
+
conversationId: targetConversationId
|
|
620
|
+
});
|
|
621
|
+
const applyToPaused = (delta) => {
|
|
622
|
+
pausedParts = applyPartDelta(pausedParts, delta);
|
|
623
|
+
updatePaused({ parts: pausedParts });
|
|
624
|
+
};
|
|
625
|
+
applyToPaused({
|
|
626
|
+
type: "approval_resolved",
|
|
627
|
+
id: approvalId,
|
|
628
|
+
answer: answer.allowed ? t.approvalAllowed : t.approvalDenied
|
|
629
|
+
});
|
|
630
|
+
applyToPaused({
|
|
631
|
+
type: "tool_decision",
|
|
632
|
+
id: approvalId,
|
|
633
|
+
decision: {
|
|
634
|
+
status: answer.allowed ? "approved" : "denied",
|
|
635
|
+
by: "user",
|
|
636
|
+
name: currentUser.name || currentUser.username,
|
|
637
|
+
at: DateTime.now().toUTC().toISO()
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
const instead = answer.allowed ? answer.instead : void 0;
|
|
641
|
+
if (instead) {
|
|
642
|
+
pausedParts = pausedParts.map((p) => p.kind === "tool_call" && p.id === approvalId ? {
|
|
643
|
+
...p,
|
|
644
|
+
name: instead
|
|
645
|
+
} : p);
|
|
646
|
+
updatePaused({ parts: pausedParts });
|
|
647
|
+
}
|
|
648
|
+
streamingToConversationRef.current = targetConversationId;
|
|
649
|
+
startStreaming(targetConversationId);
|
|
650
|
+
try {
|
|
651
|
+
await streamReply(targetConversationId, {
|
|
652
|
+
model: modelString,
|
|
653
|
+
messages: historyMessages(conversation.messages.slice(0, pausedAt + 1), state.selectedProvider),
|
|
654
|
+
conversationId: targetConversationId,
|
|
655
|
+
parentMessageId: paused.id,
|
|
656
|
+
userMessageId: crypto.randomUUID(),
|
|
657
|
+
allocation: state.selectedAllocation,
|
|
658
|
+
...tools ? { tools: enabledToolIds } : {},
|
|
659
|
+
resume: {
|
|
660
|
+
messageId: paused.id,
|
|
661
|
+
toolCallId: approvalId,
|
|
662
|
+
allowed: answer.allowed,
|
|
663
|
+
...answer.denyMessage ? { feedback: answer.denyMessage } : {},
|
|
664
|
+
...instead ? { tool: instead } : {}
|
|
665
|
+
}
|
|
666
|
+
}, paused.id, (message) => updatePaused({ error: message }), (delta) => {
|
|
667
|
+
if (delta.type !== "tool_end" || delta.id !== approvalId) return false;
|
|
668
|
+
applyToPaused(delta);
|
|
669
|
+
return true;
|
|
670
|
+
});
|
|
671
|
+
} finally {
|
|
672
|
+
isSendingRef.current = false;
|
|
673
|
+
}
|
|
674
|
+
}, [
|
|
675
|
+
resolvedConfig,
|
|
676
|
+
state.currentConversation,
|
|
677
|
+
state.selectedProvider,
|
|
678
|
+
state.selectedAllocation,
|
|
679
|
+
tools,
|
|
680
|
+
enabledToolIds,
|
|
681
|
+
startStreaming,
|
|
682
|
+
streamReply,
|
|
683
|
+
currentUser
|
|
684
|
+
]);
|
|
594
685
|
const updateConversationTitle = useCallback(async (id, title) => {
|
|
595
686
|
try {
|
|
596
687
|
await adapter.conversations.rename(id, title);
|
|
@@ -807,6 +898,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
807
898
|
deleteConversation,
|
|
808
899
|
sendMessage,
|
|
809
900
|
retryMessage,
|
|
901
|
+
answerToolApproval,
|
|
810
902
|
setSelectedProvider,
|
|
811
903
|
setSelectedAllocation,
|
|
812
904
|
setToolEnabled,
|
|
@@ -854,6 +946,7 @@ function ChatProvider({ adapter, currentUser, navigation, activeConversationId =
|
|
|
854
946
|
deleteConversation,
|
|
855
947
|
sendMessage,
|
|
856
948
|
retryMessage,
|
|
949
|
+
answerToolApproval,
|
|
857
950
|
setSelectedProvider,
|
|
858
951
|
setSelectedAllocation,
|
|
859
952
|
setToolEnabled,
|
package/dist/ai/core/config.d.ts
CHANGED
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { type ComponentType, type MouseEventHandler, type ReactNode } from 'react';
|
|
2
2
|
import type { Components } from 'streamdown';
|
|
3
3
|
import { type ChatStrings } from '../strings.js';
|
|
4
|
-
import type { ApprovalAnswerValue, ChatLinkTarget, ChatMessage, CspKind, MessagePart, MessagePartKind } from '../types.js';
|
|
4
|
+
import type { ApprovalAnswerValue, ApprovalPart, ChatLinkTarget, ChatMessage, CspKind, MessagePart, MessagePartKind, ToolCallPart } from '../types.js';
|
|
5
5
|
export interface PartRendererProps {
|
|
6
6
|
part: MessagePart;
|
|
7
7
|
message: ChatMessage;
|
|
8
8
|
isStreaming?: boolean;
|
|
9
9
|
onApprovalAnswer?: (id: string, answer: ApprovalAnswerValue) => void;
|
|
10
10
|
}
|
|
11
|
+
export interface ToolRendererProps {
|
|
12
|
+
part: ToolCallPart;
|
|
13
|
+
approval?: ApprovalPart;
|
|
14
|
+
message: ChatMessage;
|
|
15
|
+
isStreaming?: boolean;
|
|
16
|
+
/** Absent when the reader may not answer, such as a call someone else asked for. */
|
|
17
|
+
onApprovalAnswer?: (id: string, answer: ApprovalAnswerValue) => void;
|
|
18
|
+
}
|
|
11
19
|
export interface ChatLinkProps {
|
|
12
20
|
target: ChatLinkTarget;
|
|
13
21
|
className?: string;
|
|
@@ -27,6 +35,7 @@ export interface ChatUIConfig {
|
|
|
27
35
|
composerUsage?: ReactNode;
|
|
28
36
|
providerIcon?: (cspKind: CspKind, className?: string) => ReactNode;
|
|
29
37
|
partRenderers?: Partial<Record<MessagePartKind, ComponentType<PartRendererProps>>>;
|
|
38
|
+
toolRenderers?: Record<string, ComponentType<ToolRendererProps>>;
|
|
30
39
|
};
|
|
31
40
|
strings?: {
|
|
32
41
|
[S in keyof ChatStrings]?: Partial<ChatStrings[S]>;
|
package/dist/ai/core/parts.js
CHANGED
|
@@ -34,6 +34,10 @@ function applyPartDelta(parts, delta) {
|
|
|
34
34
|
...p,
|
|
35
35
|
args: p.args + delta.args
|
|
36
36
|
} : p);
|
|
37
|
+
case "tool_wait": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
|
|
38
|
+
...p,
|
|
39
|
+
status: "waiting"
|
|
40
|
+
} : p);
|
|
37
41
|
case "tool_end": return parts.map((p) => p.kind === "tool_call" && p.id === delta.id ? {
|
|
38
42
|
...p,
|
|
39
43
|
status: delta.isError ? "error" : "ok",
|
|
@@ -99,7 +103,7 @@ function isStreamedText(part) {
|
|
|
99
103
|
* calls comes back as the answer alone. */
|
|
100
104
|
function toolTurnMessages(parts, answer) {
|
|
101
105
|
const calls = (parts ?? []).filter((p) => p.kind === "tool_call" && p.status !== "running" && p.result !== void 0);
|
|
102
|
-
if (calls.length === 0) return [answer];
|
|
106
|
+
if (calls.length === 0) return answer.content || answer.tool_calls?.length ? [answer] : [];
|
|
103
107
|
return [
|
|
104
108
|
{
|
|
105
109
|
role: "assistant",
|
package/dist/ai/index.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export { type SlashCommandOption, type SlashMenuConfig, slashMatches, } from './
|
|
|
27
27
|
export type { ComposerPastes, PasteUploadResult, } from './components/usePasteCards.js';
|
|
28
28
|
export { ChatProvider, type ChatProviderProps, type ChatStream, useChat, useChatStream, } from './core/ChatProvider.js';
|
|
29
29
|
export { type ChatAction, type ChatState, chatReducer, initialState, } from './core/chatReducer.js';
|
|
30
|
-
export type { PartRendererProps } from './core/config.js';
|
|
30
|
+
export type { PartRendererProps, ToolRendererProps } from './core/config.js';
|
|
31
31
|
export { type ChatLinkProps, type ChatUIConfig, resolveChatConfig, useChatConfig, } from './core/config.js';
|
|
32
32
|
export { FOCUS_SIDEBAR_SEARCH_EVENT } from './core/events.js';
|
|
33
33
|
export { getGreeting } from './core/greeting.js';
|
package/dist/ai/strings.d.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface ChatStrings {
|
|
|
15
15
|
addGroups: string;
|
|
16
16
|
info: ReactNode;
|
|
17
17
|
shareButton: string;
|
|
18
|
+
/** Beside the share button in a shared conversation that can use tools. */
|
|
19
|
+
toolResultsShared: string;
|
|
18
20
|
};
|
|
19
21
|
blockedGroup: {
|
|
20
22
|
title: (group: string) => string;
|
|
@@ -67,6 +69,8 @@ export interface ChatStrings {
|
|
|
67
69
|
turnAllOff: string;
|
|
68
70
|
/** Closes the tools modal; each switch takes effect as it is flipped. */
|
|
69
71
|
done: string;
|
|
72
|
+
/** Marks a tool whose calls wait for the reader's approval. */
|
|
73
|
+
asksFirst: string;
|
|
70
74
|
};
|
|
71
75
|
allocationPicker: {
|
|
72
76
|
choose: string;
|
|
@@ -87,6 +91,8 @@ export interface ChatStrings {
|
|
|
87
91
|
stop: string;
|
|
88
92
|
/** The status words an agent's working line picks from, one per turn. */
|
|
89
93
|
words: readonly string[];
|
|
94
|
+
/** The working line while a call waits for the reader to allow it. */
|
|
95
|
+
waitingForAnswer: string;
|
|
90
96
|
};
|
|
91
97
|
emptyState: {
|
|
92
98
|
greeting: string | null;
|
|
@@ -121,6 +127,9 @@ export interface ChatStrings {
|
|
|
121
127
|
activity: string;
|
|
122
128
|
/** A finished turn's label when it ran tools without reasoning. */
|
|
123
129
|
ranToolCalls: (count: number) => string;
|
|
130
|
+
/** The same, in a shared conversation, for a turn someone else started:
|
|
131
|
+
* its tools ran with that person's access. */
|
|
132
|
+
ranToolCallsAs: (count: number, name: string) => string;
|
|
124
133
|
};
|
|
125
134
|
thread: {
|
|
126
135
|
viewOnly: string;
|
|
@@ -253,6 +262,13 @@ export interface ChatStrings {
|
|
|
253
262
|
approvalOptions: string;
|
|
254
263
|
/** Names a question the agent waits on that has no heading of its own. */
|
|
255
264
|
approvalLabel: string;
|
|
265
|
+
/** A call the reader approved or declined. */
|
|
266
|
+
approvalAllowed: string;
|
|
267
|
+
approvalDenied: string;
|
|
268
|
+
/** A call nobody answered in time. */
|
|
269
|
+
approvalRequestExpired: string;
|
|
270
|
+
/** A call only the person who asked for it may answer. */
|
|
271
|
+
approvalWaitingOn: (name: string) => string;
|
|
256
272
|
/** Names the field for the reason a request is denied. */
|
|
257
273
|
feedbackLabel: string;
|
|
258
274
|
/** Names the field for an answer of the reader's own. */
|
package/dist/ai/strings.js
CHANGED
|
@@ -20,7 +20,8 @@ var defaultChatStrings = {
|
|
|
20
20
|
/* @__PURE__ */ jsx("strong", { children: "Collaborate" }),
|
|
21
21
|
" permission allows adding messages."
|
|
22
22
|
] }),
|
|
23
|
-
shareButton: "Share conversation"
|
|
23
|
+
shareButton: "Share conversation",
|
|
24
|
+
toolResultsShared: "Tool results here are visible to the people it's shared with"
|
|
24
25
|
},
|
|
25
26
|
blockedGroup: {
|
|
26
27
|
title: (group) => `Billing group \u201c${group}\u201d is over its allocation`,
|
|
@@ -59,12 +60,13 @@ var defaultChatStrings = {
|
|
|
59
60
|
tools: "Tools",
|
|
60
61
|
someToolsOff: "Some tools are off",
|
|
61
62
|
toolsOn: (enabled, total) => `${enabled.toLocaleString()}/${total.toLocaleString()}`,
|
|
62
|
-
toolsDescription: "What the assistant may look up for you.",
|
|
63
|
+
toolsDescription: "What the assistant may look up or do for you.",
|
|
63
64
|
searchTools: "Search tools",
|
|
64
65
|
noToolsMatch: "No tools match",
|
|
65
66
|
turnAllOn: "Turn all on",
|
|
66
67
|
turnAllOff: "Turn all off",
|
|
67
|
-
done: "Done"
|
|
68
|
+
done: "Done",
|
|
69
|
+
asksFirst: "Asks first"
|
|
68
70
|
},
|
|
69
71
|
allocationPicker: {
|
|
70
72
|
choose: "Allocation",
|
|
@@ -114,7 +116,8 @@ var defaultChatStrings = {
|
|
|
114
116
|
"Crunching",
|
|
115
117
|
"Forging",
|
|
116
118
|
"Cooking"
|
|
117
|
-
]
|
|
119
|
+
],
|
|
120
|
+
waitingForAnswer: "Waiting for your answer"
|
|
118
121
|
},
|
|
119
122
|
emptyState: {
|
|
120
123
|
greeting: null,
|
|
@@ -150,7 +153,8 @@ var defaultChatStrings = {
|
|
|
150
153
|
thoughtFor: (duration) => `Thought for ${duration}`,
|
|
151
154
|
thoughtProcess: "Thought process",
|
|
152
155
|
activity: "Activity",
|
|
153
|
-
ranToolCalls: (count) => `Ran ${countOf(count, "tool call", "tool calls")}
|
|
156
|
+
ranToolCalls: (count) => `Ran ${countOf(count, "tool call", "tool calls")}`,
|
|
157
|
+
ranToolCallsAs: (count, name) => `Ran ${countOf(count, "tool call", "tool calls")} as ${name}`
|
|
154
158
|
},
|
|
155
159
|
thread: {
|
|
156
160
|
viewOnly: "You have view only access",
|
|
@@ -270,6 +274,10 @@ var defaultChatStrings = {
|
|
|
270
274
|
approvalPending: "Waiting for an answer in the terminal",
|
|
271
275
|
approvalOptions: "Offered choices",
|
|
272
276
|
approvalLabel: "Waiting for your answer",
|
|
277
|
+
approvalAllowed: "Allowed",
|
|
278
|
+
approvalDenied: "Declined",
|
|
279
|
+
approvalRequestExpired: "This request expired. Ask again to run it.",
|
|
280
|
+
approvalWaitingOn: (name) => `Waiting for ${name} to answer`,
|
|
273
281
|
feedbackLabel: "Feedback for the agent",
|
|
274
282
|
ownAnswerLabel: "Your answer",
|
|
275
283
|
todoPending: "To do",
|
package/dist/ai/types.d.ts
CHANGED
|
@@ -45,7 +45,7 @@ export interface TokenUsage {
|
|
|
45
45
|
completion_tokens?: number;
|
|
46
46
|
total_tokens?: number;
|
|
47
47
|
}
|
|
48
|
-
export type ToolCallStatus = 'running' | 'ok' | 'error';
|
|
48
|
+
export type ToolCallStatus = 'running' | 'waiting' | 'ok' | 'error';
|
|
49
49
|
export interface TextPart {
|
|
50
50
|
kind: 'text';
|
|
51
51
|
text: string;
|
|
@@ -100,6 +100,9 @@ export type ApprovalAnswerValue = {
|
|
|
100
100
|
allowed: boolean;
|
|
101
101
|
allowDir?: boolean;
|
|
102
102
|
denyMessage?: string;
|
|
103
|
+
/** A sibling tool to run in place of the one asked for, with the same
|
|
104
|
+
* arguments, such as destroying a cluster the model asked to stop. */
|
|
105
|
+
instead?: string;
|
|
103
106
|
} | {
|
|
104
107
|
kind: 'ask';
|
|
105
108
|
text: string;
|
|
@@ -122,6 +125,8 @@ export interface ApprovalPart {
|
|
|
122
125
|
options?: ApprovalOption[];
|
|
123
126
|
plan?: string;
|
|
124
127
|
canAllowDirectory?: boolean;
|
|
128
|
+
expiresAt?: string;
|
|
129
|
+
waitingOn?: string;
|
|
125
130
|
resolved?: boolean;
|
|
126
131
|
answer?: string;
|
|
127
132
|
}
|
|
@@ -65,7 +65,7 @@ function useRelativeTime(value, format = "long") {
|
|
|
65
65
|
const unit = unitOf(age);
|
|
66
66
|
const key = `${format}:${at}:${unit}:${Math.trunc(age / unit)}`;
|
|
67
67
|
if (last.current?.key !== key) {
|
|
68
|
-
const label = format === "compact" ? compactLabel(age, t) : DateTime.fromMillis(at).toRelative({ base: DateTime.fromMillis(current) }) ?? "";
|
|
68
|
+
const label = format === "compact" ? compactLabel(age, t) : Math.abs(age) < SECOND ? t.now : DateTime.fromMillis(at).toRelative({ base: DateTime.fromMillis(current) }) ?? "";
|
|
69
69
|
last.current = {
|
|
70
70
|
key,
|
|
71
71
|
label
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parallelworks/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.77.0",
|
|
4
4
|
"description": "React UI components: a theme contract, icons, brand logos, primitives, lists, forms, a job graph, a code editor, a log viewer, a file explorer and an AI chat, each on its own subpath",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|