@parallelworks/ui 0.75.0 → 0.76.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 +11 -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/ShareDialog.d.ts +3 -0
- package/dist/ai/components/ShareDialog.js +49 -13
- package/dist/ai/components/agent/AgentMessageParts.js +62 -23
- package/dist/ai/core/ChatProvider.d.ts +3 -1
- package/dist/ai/core/ChatProvider.js +215 -131
- package/dist/ai/core/parts.js +5 -1
- package/dist/ai/strings.d.ts +15 -0
- package/dist/ai/strings.js +14 -6
- package/dist/ai/types.d.ts +5 -1
- package/dist/components/relativeTime.js +1 -1
- package/dist/styles.css +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,13 @@ 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;
|
|
24
31
|
}
|
|
25
32
|
export interface StreamHandlers {
|
|
26
33
|
onContent: (content: string) => void;
|
|
@@ -43,6 +50,9 @@ export type PartDelta = {
|
|
|
43
50
|
type: 'tool_update';
|
|
44
51
|
id: string;
|
|
45
52
|
args: string;
|
|
53
|
+
} | {
|
|
54
|
+
type: 'tool_wait';
|
|
55
|
+
id: string;
|
|
46
56
|
} | {
|
|
47
57
|
type: 'tool_end';
|
|
48
58
|
id: string;
|
|
@@ -133,6 +143,7 @@ export interface ChatToolOption {
|
|
|
133
143
|
id: string;
|
|
134
144
|
label: string;
|
|
135
145
|
description?: string;
|
|
146
|
+
asksFirst?: boolean;
|
|
136
147
|
}
|
|
137
148
|
export interface ChatToolsAdapter {
|
|
138
149
|
list(): Promise<ChatToolOption[]>;
|
|
@@ -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",
|
|
@@ -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 };
|
|
@@ -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", {
|
|
@@ -271,7 +279,7 @@ function ToolCallBlock({ part }) {
|
|
|
271
279
|
children: fullToolArgs(header.arg, part.args)
|
|
272
280
|
}), /* @__PURE__ */ jsx(ResultBody, { part })]
|
|
273
281
|
}),
|
|
274
|
-
!isDiff && !expanded && /* @__PURE__ */ jsx("div", {
|
|
282
|
+
!isDiff && !expanded && part.status !== "waiting" && /* @__PURE__ */ jsx("div", {
|
|
275
283
|
className: cx("ml-5 mt-0.5 truncate text-[13px]", part.status === "error" ? "text-(--theme-error-text)" : "theme-muted-text"),
|
|
276
284
|
children: summary
|
|
277
285
|
}),
|
|
@@ -292,32 +300,38 @@ function decisionLabel(d, t) {
|
|
|
292
300
|
if (d.by === "auto-reviewer") return t.deniedByReviewer;
|
|
293
301
|
return d.name ? t.deniedBy(d.name) : t.denied;
|
|
294
302
|
case "expired": return t.approvalExpired;
|
|
295
|
-
case "timed_out": return t.approvalTimedOut;
|
|
296
|
-
case "failed": return t.reviewFailed;
|
|
303
|
+
case "timed_out": return d.by === "auto-reviewer" ? t.reviewTimedOut : t.approvalTimedOut;
|
|
304
|
+
case "failed": return d.by === "auto-reviewer" ? t.reviewFailed : t.denied;
|
|
297
305
|
}
|
|
298
306
|
}
|
|
299
307
|
function DecisionLine({ decision }) {
|
|
300
308
|
const { strings } = useChatConfig();
|
|
301
309
|
return /* @__PURE__ */ jsxs("div", {
|
|
302
|
-
className: "ml-5 mt-0.5
|
|
310
|
+
className: "ml-5 mt-0.5 text-xs theme-muted-text",
|
|
303
311
|
"data-testid": "tool-decision",
|
|
304
|
-
children: [
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
"
|
|
308
|
-
children: "
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
children: "
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
312
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
313
|
+
className: "flex min-w-0 flex-wrap items-baseline gap-x-1",
|
|
314
|
+
children: [
|
|
315
|
+
/* @__PURE__ */ jsx("span", { children: decisionLabel(decision, strings.agentTranscript) }),
|
|
316
|
+
decision.at && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
317
|
+
"aria-hidden": "true",
|
|
318
|
+
children: "·"
|
|
319
|
+
}), /* @__PURE__ */ jsx(RelativeTime, {
|
|
320
|
+
value: decision.at,
|
|
321
|
+
className: "shrink-0"
|
|
322
|
+
})] }),
|
|
323
|
+
decision.note && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
|
|
324
|
+
"aria-hidden": "true",
|
|
325
|
+
children: "·"
|
|
326
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
327
|
+
className: "min-w-0 max-w-full truncate",
|
|
328
|
+
children: decision.note
|
|
329
|
+
})] })
|
|
330
|
+
]
|
|
331
|
+
}), decision.reason && /* @__PURE__ */ jsx("p", {
|
|
332
|
+
className: "mt-0.5 line-clamp-2",
|
|
333
|
+
children: decision.reason
|
|
334
|
+
})]
|
|
321
335
|
});
|
|
322
336
|
}
|
|
323
337
|
/** Drawn the way the TUI draws a delegated Task: a tool-shaped header
|
|
@@ -384,11 +398,31 @@ function SubagentCard({ part }) {
|
|
|
384
398
|
})]
|
|
385
399
|
});
|
|
386
400
|
}
|
|
401
|
+
/** Whether the ISO time has passed, turning true when it does. */
|
|
402
|
+
function useExpired(expiresAt) {
|
|
403
|
+
const [expired, setExpired] = useState(() => !!expiresAt && DateTime.fromISO(expiresAt) <= DateTime.now());
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
if (!expiresAt || expired) return;
|
|
406
|
+
let timer;
|
|
407
|
+
const arm = () => {
|
|
408
|
+
const left = DateTime.fromISO(expiresAt).diffNow().toMillis();
|
|
409
|
+
if (left <= 0) {
|
|
410
|
+
setExpired(true);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
timer = setTimeout(arm, Math.min(left, 864e5));
|
|
414
|
+
};
|
|
415
|
+
arm();
|
|
416
|
+
return () => clearTimeout(timer);
|
|
417
|
+
}, [expiresAt, expired]);
|
|
418
|
+
return expired;
|
|
419
|
+
}
|
|
387
420
|
function ConfirmApproval({ part, onAnswer }) {
|
|
388
421
|
const { strings } = useChatConfig();
|
|
389
422
|
const { common } = useStrings();
|
|
390
423
|
const t = strings.agentTranscript;
|
|
391
424
|
const [feedback, setFeedback] = useState(null);
|
|
425
|
+
const expired = useExpired(part.expiresAt);
|
|
392
426
|
const answer = (a) => onAnswer?.(part.id, a);
|
|
393
427
|
const [allowLabel, denyLabel] = {
|
|
394
428
|
command: [t.allowOnce, t.deny],
|
|
@@ -405,9 +439,12 @@ function ConfirmApproval({ part, onAnswer }) {
|
|
|
405
439
|
className: "text-xs theme-muted-text",
|
|
406
440
|
children: part.reason
|
|
407
441
|
}),
|
|
408
|
-
|
|
442
|
+
expired ? /* @__PURE__ */ jsx("div", {
|
|
409
443
|
className: "text-xs theme-muted-text",
|
|
410
|
-
children: t.
|
|
444
|
+
children: t.approvalRequestExpired
|
|
445
|
+
}) : !onAnswer ? /* @__PURE__ */ jsx("div", {
|
|
446
|
+
className: "text-xs theme-muted-text",
|
|
447
|
+
children: part.waitingOn ? t.approvalWaitingOn(part.waitingOn) : t.approvalPending
|
|
411
448
|
}) : feedback === null ? /* @__PURE__ */ jsxs("div", {
|
|
412
449
|
className: "flex flex-wrap gap-2",
|
|
413
450
|
children: [
|
|
@@ -787,7 +824,9 @@ function buildRenderItems(parts) {
|
|
|
787
824
|
emitThoughts(run.slice(end));
|
|
788
825
|
run = [];
|
|
789
826
|
};
|
|
827
|
+
const decided = new Set(parts.flatMap((p) => p.kind === "tool_call" && p.decision ? [p.id] : []));
|
|
790
828
|
parts.forEach((part, index) => {
|
|
829
|
+
if (part.kind === "approval" && part.resolved && decided.has(part.id)) return;
|
|
791
830
|
if (part.kind === "reasoning" || part.kind === "tool_call" && part.status === "ok" && !part.decision && isGroupableTool(part.name)) {
|
|
792
831
|
run.push({
|
|
793
832
|
part,
|
|
@@ -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;
|