@springbrand/message-panel 0.1.2 → 0.1.3-alpha.1
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/demo/camel-chat-showcase.tsx +1265 -0
- package/demo/fixtures.ts +292 -0
- package/demo/message-panel-chat-showcase.tsx +250 -0
- package/demo/message-panel-gallery.tsx +389 -0
- package/package.json +6 -16
- package/src/adapters/ai-sdk.ts +32 -0
- package/src/camel/camel-chat-messages.tsx +1435 -0
- package/src/camel/camel-model-select.tsx +88 -0
- package/src/camel/camel-plan-bar.tsx +115 -0
- package/src/camel/camel-prompt-input.tsx +547 -0
- package/src/camel/camel-tool-presentation.tsx +1270 -0
- package/src/camel/camel-turn.ts +334 -0
- package/src/camel/turn-summary-bar.tsx +81 -0
- package/src/chat-summary-panel.tsx +229 -0
- package/src/composer/chat-composer.tsx +216 -0
- package/src/composer/composer-attachments.tsx +138 -0
- package/src/composer/composer-trigger-popover.tsx +236 -0
- package/src/composer/composer.tsx +425 -0
- package/src/composer/drop-controller.ts +80 -0
- package/src/composer/types.ts +79 -0
- package/src/contracts.ts +138 -0
- package/src/conversation.tsx +76 -0
- package/src/internal/cn.ts +3 -0
- package/src/message-panel.tsx +368 -0
- package/src/message.tsx +254 -0
- package/src/parts/index.tsx +1149 -0
- package/src/parts/kind.ts +67 -0
- package/src/parts/plan.ts +64 -0
- package/src/parts/primitives.tsx +439 -0
- package/src/registry.ts +44 -0
- package/src/status.tsx +132 -0
- package/dist/demo/camel-chat-showcase.d.ts +0 -1
- package/dist/demo/camel-chat-showcase.js +0 -1007
- package/dist/demo/fixtures.d.ts +0 -32
- package/dist/demo/fixtures.js +0 -250
- package/dist/demo/index.js +0 -4
- package/dist/demo/message-panel-chat-showcase.d.ts +0 -1
- package/dist/demo/message-panel-chat-showcase.js +0 -114
- package/dist/demo/message-panel-gallery.d.ts +0 -1
- package/dist/demo/message-panel-gallery.js +0 -130
- package/dist/src/adapters/ai-sdk.d.ts +0 -4
- package/dist/src/adapters/ai-sdk.js +0 -25
- package/dist/src/camel/camel-chat-messages.d.ts +0 -44
- package/dist/src/camel/camel-chat-messages.js +0 -421
- package/dist/src/camel/camel-model-select.d.ts +0 -13
- package/dist/src/camel/camel-model-select.js +0 -7
- package/dist/src/camel/camel-plan-bar.d.ts +0 -5
- package/dist/src/camel/camel-plan-bar.js +0 -24
- package/dist/src/camel/camel-prompt-input.d.ts +0 -32
- package/dist/src/camel/camel-prompt-input.js +0 -182
- package/dist/src/camel/camel-tool-presentation.d.ts +0 -21
- package/dist/src/camel/camel-tool-presentation.js +0 -784
- package/dist/src/camel/camel-turn.d.ts +0 -57
- package/dist/src/camel/camel-turn.js +0 -203
- package/dist/src/camel/index.js +0 -7
- package/dist/src/camel/turn-summary-bar.d.ts +0 -8
- package/dist/src/camel/turn-summary-bar.js +0 -12
- package/dist/src/chat-summary-panel.d.ts +0 -22
- package/dist/src/chat-summary-panel.js +0 -26
- package/dist/src/composer/chat-composer.d.ts +0 -4
- package/dist/src/composer/chat-composer.js +0 -90
- package/dist/src/composer/composer-attachments.d.ts +0 -13
- package/dist/src/composer/composer-attachments.js +0 -47
- package/dist/src/composer/composer-trigger-popover.d.ts +0 -31
- package/dist/src/composer/composer-trigger-popover.js +0 -29
- package/dist/src/composer/composer.d.ts +0 -15
- package/dist/src/composer/composer.js +0 -180
- package/dist/src/composer/drop-controller.d.ts +0 -8
- package/dist/src/composer/drop-controller.js +0 -69
- package/dist/src/composer/index.js +0 -6
- package/dist/src/composer/types.d.ts +0 -62
- package/dist/src/composer/types.js +0 -1
- package/dist/src/contracts.d.ts +0 -94
- package/dist/src/contracts.js +0 -1
- package/dist/src/conversation.d.ts +0 -13
- package/dist/src/conversation.js +0 -18
- package/dist/src/index.js +0 -12
- package/dist/src/internal/cn.d.ts +0 -1
- package/dist/src/internal/cn.js +0 -3
- package/dist/src/message-panel.d.ts +0 -4
- package/dist/src/message-panel.js +0 -144
- package/dist/src/message.d.ts +0 -36
- package/dist/src/message.js +0 -83
- package/dist/src/parts/index.d.ts +0 -187
- package/dist/src/parts/index.js +0 -332
- package/dist/src/parts/kind.d.ts +0 -3
- package/dist/src/parts/kind.js +0 -69
- package/dist/src/parts/plan.d.ts +0 -10
- package/dist/src/parts/plan.js +0 -40
- package/dist/src/parts/primitives.d.ts +0 -70
- package/dist/src/parts/primitives.js +0 -153
- package/dist/src/registry.d.ts +0 -4
- package/dist/src/registry.js +0 -31
- package/dist/src/status.d.ts +0 -26
- package/dist/src/status.js +0 -74
- /package/{dist/demo/index.d.ts → demo/index.ts} +0 -0
- /package/{dist/src/camel/index.d.ts → src/camel/index.ts} +0 -0
- /package/{dist/src/composer/index.d.ts → src/composer/index.ts} +0 -0
- /package/{dist/src/index.d.ts → src/index.ts} +0 -0
|
@@ -0,0 +1,1270 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CheckIcon,
|
|
3
|
+
CopyIcon,
|
|
4
|
+
} from "lucide-react";
|
|
5
|
+
import {
|
|
6
|
+
useEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { MessageResponse } from "../message";
|
|
12
|
+
import { toolNameOfPart } from "../parts";
|
|
13
|
+
import { cn } from "../internal/cn";
|
|
14
|
+
|
|
15
|
+
type UnknownRecord = Record<string, unknown>;
|
|
16
|
+
|
|
17
|
+
export type CamelToolKind =
|
|
18
|
+
| "read"
|
|
19
|
+
| "write"
|
|
20
|
+
| "edit"
|
|
21
|
+
| "bash"
|
|
22
|
+
| "glob"
|
|
23
|
+
| "grep"
|
|
24
|
+
| "list"
|
|
25
|
+
| "find"
|
|
26
|
+
| "web-search"
|
|
27
|
+
| "web-fetch"
|
|
28
|
+
| "javascript"
|
|
29
|
+
| "tasks"
|
|
30
|
+
| "agent"
|
|
31
|
+
| "skill"
|
|
32
|
+
| "skill-resource"
|
|
33
|
+
| "ask-user"
|
|
34
|
+
| "suggestions"
|
|
35
|
+
| "notebook"
|
|
36
|
+
| "mcp"
|
|
37
|
+
| "generic";
|
|
38
|
+
|
|
39
|
+
export interface CamelToolPresentation {
|
|
40
|
+
kind: CamelToolKind;
|
|
41
|
+
name: string;
|
|
42
|
+
summary: string;
|
|
43
|
+
input: UnknownRecord;
|
|
44
|
+
output: unknown;
|
|
45
|
+
outputText: string;
|
|
46
|
+
errorText: string;
|
|
47
|
+
running: boolean;
|
|
48
|
+
failed: boolean;
|
|
49
|
+
isSubAgent: boolean;
|
|
50
|
+
hasDetails: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface CamelToolDetailsProps {
|
|
54
|
+
presentation: CamelToolPresentation;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function recordOf(value: unknown): UnknownRecord {
|
|
58
|
+
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
59
|
+
? (value as UnknownRecord)
|
|
60
|
+
: {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function safeStringify(value: unknown): string {
|
|
64
|
+
if (typeof value === "string") return value;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.stringify(value, null, 2);
|
|
67
|
+
} catch {
|
|
68
|
+
return String(value);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function stringField(record: UnknownRecord, ...keys: string[]): string {
|
|
73
|
+
for (const key of keys) {
|
|
74
|
+
const value = record[key];
|
|
75
|
+
if (typeof value === "string" && value.trim()) return value.trim();
|
|
76
|
+
}
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function numberField(record: UnknownRecord, ...keys: string[]): number | undefined {
|
|
81
|
+
for (const key of keys) {
|
|
82
|
+
const value = record[key];
|
|
83
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeOutputText(value: unknown): string {
|
|
89
|
+
if (value == null) return "";
|
|
90
|
+
if (typeof value === "string") return value;
|
|
91
|
+
if (Array.isArray(value)) {
|
|
92
|
+
return value
|
|
93
|
+
.map((item) => {
|
|
94
|
+
const record = recordOf(item);
|
|
95
|
+
return typeof record.text === "string"
|
|
96
|
+
? record.text
|
|
97
|
+
: safeStringify(item);
|
|
98
|
+
})
|
|
99
|
+
.filter(Boolean)
|
|
100
|
+
.join("\n\n");
|
|
101
|
+
}
|
|
102
|
+
const record = recordOf(value);
|
|
103
|
+
if (typeof record.text === "string") return record.text;
|
|
104
|
+
if (typeof record.content === "string") return record.content;
|
|
105
|
+
if (Array.isArray(record.content)) return normalizeOutputText(record.content);
|
|
106
|
+
return safeStringify(value);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function stripAnsi(value: string): string {
|
|
110
|
+
// ANSI CSI and OSC sequences should never affect chat layout.
|
|
111
|
+
// eslint-disable-next-line no-control-regex
|
|
112
|
+
return value.replace(/\x1b\[[0-9;]*[a-zA-Z]|\x1b\].*?(?:\x07|\x1b\\)/g, "");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function previewLines(value: string, maxLines: number): string {
|
|
116
|
+
if (!value) return "";
|
|
117
|
+
const lines = value.split(/\r?\n/);
|
|
118
|
+
return lines.length <= maxLines
|
|
119
|
+
? value
|
|
120
|
+
: [...lines.slice(0, maxLines), "..."].join("\n");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function truncate(value: string, maxLength: number): string {
|
|
124
|
+
return value.length <= maxLength
|
|
125
|
+
? value
|
|
126
|
+
: `${value.slice(0, maxLength)}...`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function filenameOf(path: string): string {
|
|
130
|
+
const trimmed = path.trim().replace(/\/+$/, "");
|
|
131
|
+
return trimmed.split("/").pop() || trimmed;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hostnameOf(url: string): string {
|
|
135
|
+
try {
|
|
136
|
+
return new URL(url).hostname;
|
|
137
|
+
} catch {
|
|
138
|
+
return url;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function humanize(name: string): string {
|
|
143
|
+
const spaced = name
|
|
144
|
+
.replace(/^mcp__/, "")
|
|
145
|
+
.replace(/__/g, " ")
|
|
146
|
+
.replace(/[_-]+/g, " ")
|
|
147
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
148
|
+
.trim();
|
|
149
|
+
return spaced
|
|
150
|
+
? spaced.replace(/\b\w/g, (character) => character.toUpperCase())
|
|
151
|
+
: "Tool";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function canonicalToolKind(name: string): CamelToolKind {
|
|
155
|
+
const normalized = name.trim().replaceAll("-", "_").toLowerCase();
|
|
156
|
+
if (normalized.startsWith("mcp__")) return "mcp";
|
|
157
|
+
if (
|
|
158
|
+
[
|
|
159
|
+
"read",
|
|
160
|
+
"read_file",
|
|
161
|
+
"read_text_file",
|
|
162
|
+
"read_document",
|
|
163
|
+
].includes(normalized)
|
|
164
|
+
) {
|
|
165
|
+
return "read";
|
|
166
|
+
}
|
|
167
|
+
if (
|
|
168
|
+
[
|
|
169
|
+
"write",
|
|
170
|
+
"write_file",
|
|
171
|
+
"create_file",
|
|
172
|
+
"save_file",
|
|
173
|
+
].includes(normalized)
|
|
174
|
+
) {
|
|
175
|
+
return "write";
|
|
176
|
+
}
|
|
177
|
+
if (
|
|
178
|
+
[
|
|
179
|
+
"edit",
|
|
180
|
+
"edit_file",
|
|
181
|
+
"apply_patch",
|
|
182
|
+
"patch_file",
|
|
183
|
+
].includes(normalized)
|
|
184
|
+
) {
|
|
185
|
+
return "edit";
|
|
186
|
+
}
|
|
187
|
+
if (
|
|
188
|
+
[
|
|
189
|
+
"bash",
|
|
190
|
+
"shell",
|
|
191
|
+
"exec",
|
|
192
|
+
"exec_command",
|
|
193
|
+
"run_command",
|
|
194
|
+
"terminal",
|
|
195
|
+
].includes(normalized)
|
|
196
|
+
) {
|
|
197
|
+
return "bash";
|
|
198
|
+
}
|
|
199
|
+
if (["glob", "find_files", "list_files_recursive"].includes(normalized)) {
|
|
200
|
+
return "glob";
|
|
201
|
+
}
|
|
202
|
+
if (
|
|
203
|
+
[
|
|
204
|
+
"grep",
|
|
205
|
+
"rg",
|
|
206
|
+
"search_files",
|
|
207
|
+
"search_code",
|
|
208
|
+
].includes(normalized)
|
|
209
|
+
) {
|
|
210
|
+
return "grep";
|
|
211
|
+
}
|
|
212
|
+
if (["ls", "list_directory", "list_files"].includes(normalized)) {
|
|
213
|
+
return "list";
|
|
214
|
+
}
|
|
215
|
+
if (["find", "find_file"].includes(normalized)) return "find";
|
|
216
|
+
if (
|
|
217
|
+
[
|
|
218
|
+
"web_search",
|
|
219
|
+
"search_web",
|
|
220
|
+
"search_query",
|
|
221
|
+
].includes(normalized)
|
|
222
|
+
) {
|
|
223
|
+
return "web-search";
|
|
224
|
+
}
|
|
225
|
+
if (
|
|
226
|
+
[
|
|
227
|
+
"web_fetch",
|
|
228
|
+
"fetch_url",
|
|
229
|
+
"open_url",
|
|
230
|
+
"fetch_page",
|
|
231
|
+
].includes(normalized)
|
|
232
|
+
) {
|
|
233
|
+
return "web-fetch";
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
[
|
|
237
|
+
"javascript",
|
|
238
|
+
"js_exec",
|
|
239
|
+
"node_repl",
|
|
240
|
+
"execute_javascript",
|
|
241
|
+
].includes(normalized)
|
|
242
|
+
) {
|
|
243
|
+
return "javascript";
|
|
244
|
+
}
|
|
245
|
+
if (
|
|
246
|
+
[
|
|
247
|
+
"todo_write",
|
|
248
|
+
"update_todo",
|
|
249
|
+
"update_plan",
|
|
250
|
+
].includes(normalized)
|
|
251
|
+
) {
|
|
252
|
+
return "tasks";
|
|
253
|
+
}
|
|
254
|
+
if (
|
|
255
|
+
[
|
|
256
|
+
"task",
|
|
257
|
+
"task_output",
|
|
258
|
+
"agent",
|
|
259
|
+
"run_agent",
|
|
260
|
+
"run_temporary_agent",
|
|
261
|
+
"subagents",
|
|
262
|
+
"fanout",
|
|
263
|
+
"extract",
|
|
264
|
+
"dispatch_background",
|
|
265
|
+
"explore",
|
|
266
|
+
"research",
|
|
267
|
+
"oracle",
|
|
268
|
+
].includes(normalized)
|
|
269
|
+
) {
|
|
270
|
+
return "agent";
|
|
271
|
+
}
|
|
272
|
+
if (["skill", "read_skill"].includes(normalized)) return "skill";
|
|
273
|
+
if (normalized === "read_skill_resource") return "skill-resource";
|
|
274
|
+
if (
|
|
275
|
+
[
|
|
276
|
+
"ask_user",
|
|
277
|
+
"ask_user_question",
|
|
278
|
+
"request_user_input",
|
|
279
|
+
].includes(normalized)
|
|
280
|
+
) {
|
|
281
|
+
return "ask-user";
|
|
282
|
+
}
|
|
283
|
+
if (["suggest_followups", "suggestions"].includes(normalized)) {
|
|
284
|
+
return "suggestions";
|
|
285
|
+
}
|
|
286
|
+
if (["notebook_edit", "edit_notebook"].includes(normalized)) {
|
|
287
|
+
return "notebook";
|
|
288
|
+
}
|
|
289
|
+
return "generic";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function parseMcpName(name: string): { server: string; tool: string } | null {
|
|
293
|
+
if (!name.startsWith("mcp__")) return null;
|
|
294
|
+
const [, server = "", ...toolParts] = name.split("__");
|
|
295
|
+
if (!server || toolParts.length === 0) return null;
|
|
296
|
+
return {
|
|
297
|
+
server: humanize(server),
|
|
298
|
+
tool: humanize(toolParts.join(" ")),
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function toolInput(part: UnknownRecord): UnknownRecord {
|
|
303
|
+
if (part.input != null) return recordOf(part.input);
|
|
304
|
+
const data = recordOf(part.data);
|
|
305
|
+
if (data.input != null) return recordOf(data.input);
|
|
306
|
+
return part.type === "tool" ? data : {};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function toolOutput(part: UnknownRecord): unknown {
|
|
310
|
+
const data = recordOf(part.data);
|
|
311
|
+
return part.output ?? data.output ?? part.result;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function parseCount(outputText: string, key: "files" | "matches"): number | null {
|
|
315
|
+
const match = outputText.match(
|
|
316
|
+
new RegExp(`Found\\s+(\\d+)\\s+${key}`, "i"),
|
|
317
|
+
);
|
|
318
|
+
return match ? Number.parseInt(match[1] ?? "0", 10) : null;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function fileSummary(
|
|
322
|
+
input: UnknownRecord,
|
|
323
|
+
state: { running: boolean; failed: boolean },
|
|
324
|
+
labels: {
|
|
325
|
+
running: string;
|
|
326
|
+
runningGeneric: string;
|
|
327
|
+
failed: string;
|
|
328
|
+
complete: string;
|
|
329
|
+
},
|
|
330
|
+
): string {
|
|
331
|
+
const path = stringField(input, "file_path", "path", "filename");
|
|
332
|
+
const filename = path ? filenameOf(path) : "";
|
|
333
|
+
if (state.running) {
|
|
334
|
+
return filename
|
|
335
|
+
? `${labels.running} ${filename}...`
|
|
336
|
+
: labels.runningGeneric;
|
|
337
|
+
}
|
|
338
|
+
if (state.failed) {
|
|
339
|
+
return filename ? `${labels.failed} ${filename}` : labels.failed;
|
|
340
|
+
}
|
|
341
|
+
return filename ? `${labels.complete} ${filename}` : labels.complete;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function questionsOf(input: UnknownRecord): UnknownRecord[] {
|
|
345
|
+
if (Array.isArray(input.questions)) return input.questions.map(recordOf);
|
|
346
|
+
if (typeof input.question === "string") {
|
|
347
|
+
return [
|
|
348
|
+
{
|
|
349
|
+
question: input.question,
|
|
350
|
+
header: input.header,
|
|
351
|
+
options: input.options,
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
}
|
|
355
|
+
return [];
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function summaryFor(
|
|
359
|
+
kind: CamelToolKind,
|
|
360
|
+
name: string,
|
|
361
|
+
input: UnknownRecord,
|
|
362
|
+
outputText: string,
|
|
363
|
+
running: boolean,
|
|
364
|
+
failed: boolean,
|
|
365
|
+
): string {
|
|
366
|
+
const state = { running, failed };
|
|
367
|
+
switch (kind) {
|
|
368
|
+
case "read":
|
|
369
|
+
return fileSummary(input, state, {
|
|
370
|
+
running: "Reading",
|
|
371
|
+
runningGeneric: "Reading file...",
|
|
372
|
+
failed: "Failed to read",
|
|
373
|
+
complete: "Read",
|
|
374
|
+
});
|
|
375
|
+
case "write":
|
|
376
|
+
return fileSummary(input, state, {
|
|
377
|
+
running: "Creating",
|
|
378
|
+
runningGeneric: "Creating file...",
|
|
379
|
+
failed: "Failed to create",
|
|
380
|
+
complete: "Created",
|
|
381
|
+
});
|
|
382
|
+
case "edit":
|
|
383
|
+
return fileSummary(input, state, {
|
|
384
|
+
running: "Editing",
|
|
385
|
+
runningGeneric: "Editing file...",
|
|
386
|
+
failed: "Failed to edit",
|
|
387
|
+
complete: "Edited",
|
|
388
|
+
});
|
|
389
|
+
case "bash": {
|
|
390
|
+
const command = stringField(input, "command", "cmd");
|
|
391
|
+
const description = stringField(input, "description");
|
|
392
|
+
const label = description || truncate(command || "command", 30);
|
|
393
|
+
return running
|
|
394
|
+
? command || description
|
|
395
|
+
? `Running ${label}...`
|
|
396
|
+
: "Running command..."
|
|
397
|
+
: failed
|
|
398
|
+
? `Failed to run ${label}`
|
|
399
|
+
: `Ran ${label}`;
|
|
400
|
+
}
|
|
401
|
+
case "glob": {
|
|
402
|
+
const pattern = stringField(input, "pattern", "glob");
|
|
403
|
+
if (running) {
|
|
404
|
+
return pattern
|
|
405
|
+
? `Searching for "${truncate(pattern, 20)}"...`
|
|
406
|
+
: "Searching for files...";
|
|
407
|
+
}
|
|
408
|
+
if (failed) return "Failed to search files";
|
|
409
|
+
const count = parseCount(outputText, "files");
|
|
410
|
+
if (count != null) return `Found ${count} files`;
|
|
411
|
+
return /no files found/i.test(outputText)
|
|
412
|
+
? "No files found"
|
|
413
|
+
: "Searched files";
|
|
414
|
+
}
|
|
415
|
+
case "grep": {
|
|
416
|
+
const pattern = stringField(input, "pattern", "query");
|
|
417
|
+
if (running) {
|
|
418
|
+
return pattern
|
|
419
|
+
? `Searching for "${truncate(pattern, 20)}"...`
|
|
420
|
+
: "Searching...";
|
|
421
|
+
}
|
|
422
|
+
if (failed) return "Failed to search codebase";
|
|
423
|
+
const count = parseCount(outputText, "matches");
|
|
424
|
+
if (count != null) return `Found ${count} matches`;
|
|
425
|
+
return /no matches found/i.test(outputText)
|
|
426
|
+
? "No matches found"
|
|
427
|
+
: "Searched codebase";
|
|
428
|
+
}
|
|
429
|
+
case "list": {
|
|
430
|
+
const path = stringField(input, "path");
|
|
431
|
+
const filename = path ? filenameOf(path) : "";
|
|
432
|
+
if (running) return filename ? `Listing ${filename}...` : "Listing files...";
|
|
433
|
+
if (failed) return filename ? `Failed to list ${filename}` : "Failed to list files";
|
|
434
|
+
return filename ? `Listed ${filename}` : "Listed files";
|
|
435
|
+
}
|
|
436
|
+
case "find": {
|
|
437
|
+
const pattern = stringField(input, "pattern", "query");
|
|
438
|
+
if (running) {
|
|
439
|
+
return pattern
|
|
440
|
+
? `Finding "${truncate(pattern, 20)}"...`
|
|
441
|
+
: "Finding files...";
|
|
442
|
+
}
|
|
443
|
+
return failed ? "Failed to find files" : "Found files";
|
|
444
|
+
}
|
|
445
|
+
case "web-search":
|
|
446
|
+
return running
|
|
447
|
+
? "Searching web..."
|
|
448
|
+
: failed
|
|
449
|
+
? "Failed to search web"
|
|
450
|
+
: "Searched web";
|
|
451
|
+
case "web-fetch": {
|
|
452
|
+
const url = stringField(input, "url");
|
|
453
|
+
const host = url ? hostnameOf(url) : "web page";
|
|
454
|
+
return running
|
|
455
|
+
? `Fetching ${host}...`
|
|
456
|
+
: failed
|
|
457
|
+
? `Failed to fetch ${host}`
|
|
458
|
+
: `Fetched ${host}`;
|
|
459
|
+
}
|
|
460
|
+
case "javascript": {
|
|
461
|
+
const label = stringField(input, "description") || "JavaScript";
|
|
462
|
+
return running
|
|
463
|
+
? `Running ${label}...`
|
|
464
|
+
: failed
|
|
465
|
+
? `${label} failed`
|
|
466
|
+
: `Ran ${label}`;
|
|
467
|
+
}
|
|
468
|
+
case "tasks":
|
|
469
|
+
return running
|
|
470
|
+
? "Updating tasks..."
|
|
471
|
+
: failed
|
|
472
|
+
? "Failed to update tasks"
|
|
473
|
+
: "Updated tasks";
|
|
474
|
+
case "agent": {
|
|
475
|
+
const description =
|
|
476
|
+
stringField(input, "description", "question", "prompt") || "subtask";
|
|
477
|
+
return running
|
|
478
|
+
? `Working on ${truncate(description, 52)}`
|
|
479
|
+
: failed
|
|
480
|
+
? `Could not finish ${truncate(description, 52)}`
|
|
481
|
+
: `Finished ${truncate(description, 52)}`;
|
|
482
|
+
}
|
|
483
|
+
case "skill": {
|
|
484
|
+
const skill = stringField(input, "skill", "name");
|
|
485
|
+
return running
|
|
486
|
+
? skill
|
|
487
|
+
? `Reading instructions for ${skill}...`
|
|
488
|
+
: "Reading instructions..."
|
|
489
|
+
: failed
|
|
490
|
+
? skill
|
|
491
|
+
? `Could not read instructions for ${skill}`
|
|
492
|
+
: "Could not read instructions"
|
|
493
|
+
: `Read instructions for ${skill || "task"}`;
|
|
494
|
+
}
|
|
495
|
+
case "skill-resource": {
|
|
496
|
+
const path = stringField(input, "path");
|
|
497
|
+
const resource = path ? filenameOf(path) : "resource";
|
|
498
|
+
return running
|
|
499
|
+
? `Reading ${resource}...`
|
|
500
|
+
: failed
|
|
501
|
+
? `Could not read ${resource}`
|
|
502
|
+
: `Read ${resource}`;
|
|
503
|
+
}
|
|
504
|
+
case "ask-user": {
|
|
505
|
+
const questions = questionsOf(input);
|
|
506
|
+
if (running) return "Waiting for your input";
|
|
507
|
+
if (questions.length === 1) {
|
|
508
|
+
const header = stringField(questions[0] ?? {}, "header");
|
|
509
|
+
return header || "Asked a question";
|
|
510
|
+
}
|
|
511
|
+
return questions.length > 1
|
|
512
|
+
? `Asked ${questions.length} questions`
|
|
513
|
+
: "Asked a question";
|
|
514
|
+
}
|
|
515
|
+
case "suggestions":
|
|
516
|
+
return running ? "Preparing follow-ups..." : "Suggested follow-ups";
|
|
517
|
+
case "notebook":
|
|
518
|
+
return running
|
|
519
|
+
? "Editing notebook cell..."
|
|
520
|
+
: failed
|
|
521
|
+
? "Failed to edit notebook cell"
|
|
522
|
+
: "Edited notebook cell";
|
|
523
|
+
case "mcp": {
|
|
524
|
+
const parts = parseMcpName(name);
|
|
525
|
+
if (!parts) break;
|
|
526
|
+
return running
|
|
527
|
+
? `Calling ${parts.tool} on ${parts.server}...`
|
|
528
|
+
: failed
|
|
529
|
+
? `Failed to call ${parts.tool} on ${parts.server}`
|
|
530
|
+
: `Called ${parts.tool} on ${parts.server}`;
|
|
531
|
+
}
|
|
532
|
+
case "generic":
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (name === "create_agent") {
|
|
536
|
+
const agent = stringField(input, "name") || "agent";
|
|
537
|
+
return running
|
|
538
|
+
? `Creating ${agent}...`
|
|
539
|
+
: failed
|
|
540
|
+
? `Could not create ${agent}`
|
|
541
|
+
: `Created ${agent}`;
|
|
542
|
+
}
|
|
543
|
+
if (name === "publish_extension") {
|
|
544
|
+
const extension = humanize(stringField(input, "name") || "extension");
|
|
545
|
+
return running
|
|
546
|
+
? `Publishing ${extension}...`
|
|
547
|
+
: failed
|
|
548
|
+
? `Could not publish ${extension}`
|
|
549
|
+
: `Published ${extension}`;
|
|
550
|
+
}
|
|
551
|
+
if (name === "cancel_schedule") {
|
|
552
|
+
return running
|
|
553
|
+
? "Cancelling schedule..."
|
|
554
|
+
: failed
|
|
555
|
+
? "Schedule cancellation was denied"
|
|
556
|
+
: "Cancelled schedule";
|
|
557
|
+
}
|
|
558
|
+
const displayName = humanize(name);
|
|
559
|
+
return running
|
|
560
|
+
? `Using ${displayName}...`
|
|
561
|
+
: failed
|
|
562
|
+
? `Could not use ${displayName}`
|
|
563
|
+
: `Used ${displayName}`;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function getCamelToolPresentation(
|
|
567
|
+
part: unknown,
|
|
568
|
+
isActive = false,
|
|
569
|
+
): CamelToolPresentation {
|
|
570
|
+
const record = recordOf(part);
|
|
571
|
+
const name =
|
|
572
|
+
toolNameOfPart(part) ?? (stringField(record, "name") || "tool");
|
|
573
|
+
const input = toolInput(record);
|
|
574
|
+
const output = toolOutput(record);
|
|
575
|
+
const outputText = normalizeOutputText(output);
|
|
576
|
+
const errorText = stringField(record, "errorText", "error");
|
|
577
|
+
const state = stringField(record, "state", "status");
|
|
578
|
+
const failed =
|
|
579
|
+
state === "output-error" ||
|
|
580
|
+
state === "output-denied" ||
|
|
581
|
+
state === "failed" ||
|
|
582
|
+
Boolean(errorText);
|
|
583
|
+
const running =
|
|
584
|
+
isActive &&
|
|
585
|
+
[
|
|
586
|
+
"input-streaming",
|
|
587
|
+
"input-available",
|
|
588
|
+
"approval-responded",
|
|
589
|
+
"approval-requested",
|
|
590
|
+
"running",
|
|
591
|
+
"",
|
|
592
|
+
].includes(state);
|
|
593
|
+
const kind = canonicalToolKind(name);
|
|
594
|
+
const hasDetails =
|
|
595
|
+
Object.keys(input).length > 0 ||
|
|
596
|
+
output != null ||
|
|
597
|
+
Boolean(errorText);
|
|
598
|
+
return {
|
|
599
|
+
kind,
|
|
600
|
+
name,
|
|
601
|
+
summary: summaryFor(
|
|
602
|
+
kind,
|
|
603
|
+
name,
|
|
604
|
+
input,
|
|
605
|
+
errorText || outputText,
|
|
606
|
+
running,
|
|
607
|
+
failed,
|
|
608
|
+
),
|
|
609
|
+
input,
|
|
610
|
+
output,
|
|
611
|
+
outputText,
|
|
612
|
+
errorText,
|
|
613
|
+
running,
|
|
614
|
+
failed,
|
|
615
|
+
isSubAgent: kind === "agent",
|
|
616
|
+
hasDetails,
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function ToolCopyButton({ value, label }: { value: string; label: string }) {
|
|
621
|
+
const [copied, setCopied] = useState(false);
|
|
622
|
+
const timerRef = useRef<number | null>(null);
|
|
623
|
+
|
|
624
|
+
useEffect(
|
|
625
|
+
() => () => {
|
|
626
|
+
if (timerRef.current != null) window.clearTimeout(timerRef.current);
|
|
627
|
+
},
|
|
628
|
+
[],
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
if (!value) return null;
|
|
632
|
+
return (
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
className="inline-flex size-5 shrink-0 items-center justify-center rounded text-muted-foreground/60 opacity-0 transition-opacity hover:bg-muted group-hover/details:opacity-100 group-focus-within/details:opacity-100"
|
|
636
|
+
onClick={() => {
|
|
637
|
+
void navigator.clipboard.writeText(value).then(() => {
|
|
638
|
+
setCopied(true);
|
|
639
|
+
if (timerRef.current != null) window.clearTimeout(timerRef.current);
|
|
640
|
+
timerRef.current = window.setTimeout(() => setCopied(false), 1500);
|
|
641
|
+
});
|
|
642
|
+
}}
|
|
643
|
+
aria-label={copied ? "Copied" : label}
|
|
644
|
+
>
|
|
645
|
+
{copied ? <CheckIcon className="size-3" /> : <CopyIcon className="size-3" />}
|
|
646
|
+
</button>
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function DetailRow({
|
|
651
|
+
label,
|
|
652
|
+
value,
|
|
653
|
+
copyValue,
|
|
654
|
+
mono = false,
|
|
655
|
+
}: {
|
|
656
|
+
label: string;
|
|
657
|
+
value?: ReactNode;
|
|
658
|
+
copyValue?: string;
|
|
659
|
+
mono?: boolean;
|
|
660
|
+
}) {
|
|
661
|
+
if (value == null || value === "") return null;
|
|
662
|
+
return (
|
|
663
|
+
<div className="flex items-start gap-2 py-0.5">
|
|
664
|
+
<span className="shrink-0 text-muted-foreground/60">{label}</span>
|
|
665
|
+
<div className={cn("min-w-0 flex-1 break-words", mono && "font-mono")}>
|
|
666
|
+
{value}
|
|
667
|
+
</div>
|
|
668
|
+
{copyValue ? <ToolCopyButton value={copyValue} label={`Copy ${label}`} /> : null}
|
|
669
|
+
</div>
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function OutputBlock({
|
|
674
|
+
label,
|
|
675
|
+
value,
|
|
676
|
+
className,
|
|
677
|
+
}: {
|
|
678
|
+
label: string;
|
|
679
|
+
value?: string;
|
|
680
|
+
className?: string;
|
|
681
|
+
}) {
|
|
682
|
+
if (!value) return null;
|
|
683
|
+
const displayValue = stripAnsi(value);
|
|
684
|
+
return (
|
|
685
|
+
<div className={cn("mt-2", className)}>
|
|
686
|
+
<div className="mb-1 flex items-center justify-between text-[0.7rem] text-muted-foreground/60">
|
|
687
|
+
<span>{label}</span>
|
|
688
|
+
<ToolCopyButton value={displayValue} label={`Copy ${label}`} />
|
|
689
|
+
</div>
|
|
690
|
+
<div className="max-h-32 overflow-auto rounded bg-muted/30 p-2 font-mono text-xs">
|
|
691
|
+
<pre className="whitespace-pre-wrap break-words font-mono text-xs text-muted-foreground/80">
|
|
692
|
+
{displayValue}
|
|
693
|
+
</pre>
|
|
694
|
+
</div>
|
|
695
|
+
</div>
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function ProjectRow({ input }: { input: UnknownRecord }) {
|
|
700
|
+
const project = stringField(input, "project");
|
|
701
|
+
return project ? (
|
|
702
|
+
<DetailRow label="Project:" value={project} copyValue={project} mono />
|
|
703
|
+
) : null;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function ReadDetails({ presentation }: CamelToolDetailsProps) {
|
|
707
|
+
const { input, outputText } = presentation;
|
|
708
|
+
const path = stringField(input, "file_path", "path", "filename");
|
|
709
|
+
const lines = outputText ? outputText.split(/\r?\n/).length : 0;
|
|
710
|
+
return (
|
|
711
|
+
<div className="space-y-1">
|
|
712
|
+
<DetailRow label="Path:" value={path} copyValue={path} mono />
|
|
713
|
+
<ProjectRow input={input} />
|
|
714
|
+
<DetailRow label="Lines:" value={String(lines)} />
|
|
715
|
+
<OutputBlock label="Preview" value={previewLines(outputText, 10)} />
|
|
716
|
+
</div>
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
function WriteDetails({ presentation }: CamelToolDetailsProps) {
|
|
721
|
+
const { input } = presentation;
|
|
722
|
+
const path = stringField(input, "file_path", "path", "filename");
|
|
723
|
+
const content = normalizeOutputText(input.content);
|
|
724
|
+
const bytes = new TextEncoder().encode(content).byteLength;
|
|
725
|
+
return (
|
|
726
|
+
<div className="space-y-1">
|
|
727
|
+
<DetailRow label="Path:" value={path} copyValue={path} mono />
|
|
728
|
+
<ProjectRow input={input} />
|
|
729
|
+
{content ? <DetailRow label="Size:" value={formatBytes(bytes)} /> : null}
|
|
730
|
+
<OutputBlock label="Preview" value={previewLines(content, 10)} />
|
|
731
|
+
</div>
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
interface DiffLine {
|
|
736
|
+
kind: "add" | "remove" | "context";
|
|
737
|
+
text: string;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function diffLines(input: UnknownRecord, output: unknown): DiffLine[] {
|
|
741
|
+
const outputRecord = recordOf(output);
|
|
742
|
+
const rawDiff =
|
|
743
|
+
stringField(outputRecord, "diff") ||
|
|
744
|
+
stringField(recordOf(outputRecord.details), "diff");
|
|
745
|
+
if (rawDiff) {
|
|
746
|
+
const lines = rawDiff.split(/\r?\n/);
|
|
747
|
+
return [
|
|
748
|
+
...lines.slice(0, 200).map((text): DiffLine => ({
|
|
749
|
+
kind: text.startsWith("+")
|
|
750
|
+
? "add"
|
|
751
|
+
: text.startsWith("-")
|
|
752
|
+
? "remove"
|
|
753
|
+
: "context",
|
|
754
|
+
text,
|
|
755
|
+
})),
|
|
756
|
+
...(lines.length > 200
|
|
757
|
+
? [{ kind: "context" as const, text: "... [diff truncated]" }]
|
|
758
|
+
: []),
|
|
759
|
+
];
|
|
760
|
+
}
|
|
761
|
+
const edits = Array.isArray(input.edits)
|
|
762
|
+
? input.edits.map(recordOf)
|
|
763
|
+
: [input];
|
|
764
|
+
const result: DiffLine[] = [];
|
|
765
|
+
for (const edit of edits) {
|
|
766
|
+
const oldText = stringField(edit, "old_string", "oldText");
|
|
767
|
+
const newText = stringField(edit, "new_string", "newText");
|
|
768
|
+
for (const text of oldText.split(/\r?\n/).filter(Boolean).slice(0, 6)) {
|
|
769
|
+
result.push({ kind: "remove", text: `- ${text}` });
|
|
770
|
+
}
|
|
771
|
+
for (const text of newText.split(/\r?\n/).filter(Boolean).slice(0, 6)) {
|
|
772
|
+
result.push({ kind: "add", text: `+ ${text}` });
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return result;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function EditDetails({ presentation }: CamelToolDetailsProps) {
|
|
779
|
+
const { input, output } = presentation;
|
|
780
|
+
const path = stringField(input, "file_path", "path", "filename");
|
|
781
|
+
const edits = Array.isArray(input.edits)
|
|
782
|
+
? input.edits.length
|
|
783
|
+
: stringField(input, "old_string", "oldText", "new_string", "newText")
|
|
784
|
+
? 1
|
|
785
|
+
: 0;
|
|
786
|
+
const outputRecord = recordOf(output);
|
|
787
|
+
const replacementCount =
|
|
788
|
+
numberField(outputRecord, "replacementCount") ??
|
|
789
|
+
numberField(recordOf(outputRecord.details), "replacementCount") ??
|
|
790
|
+
edits;
|
|
791
|
+
const lines = diffLines(input, output);
|
|
792
|
+
return (
|
|
793
|
+
<div className="space-y-1">
|
|
794
|
+
<DetailRow label="Path:" value={path} copyValue={path} mono />
|
|
795
|
+
<ProjectRow input={input} />
|
|
796
|
+
<DetailRow
|
|
797
|
+
label="Changes:"
|
|
798
|
+
value={`${replacementCount} replacements`}
|
|
799
|
+
/>
|
|
800
|
+
{lines.length > 0 ? (
|
|
801
|
+
<div className="mt-2 max-h-32 overflow-auto rounded bg-muted/30 p-2 font-mono text-xs">
|
|
802
|
+
{lines.map((line, index) => (
|
|
803
|
+
<div
|
|
804
|
+
key={`${line.kind}:${index}`}
|
|
805
|
+
className={cn(
|
|
806
|
+
"whitespace-pre-wrap break-words",
|
|
807
|
+
line.kind === "add" && "text-green-600/80 dark:text-green-400/80",
|
|
808
|
+
line.kind === "remove" && "text-red-600/80 dark:text-red-400/80",
|
|
809
|
+
line.kind === "context" && "text-muted-foreground/60",
|
|
810
|
+
)}
|
|
811
|
+
>
|
|
812
|
+
{line.text}
|
|
813
|
+
</div>
|
|
814
|
+
))}
|
|
815
|
+
</div>
|
|
816
|
+
) : (
|
|
817
|
+
<DetailRow label="Diff:" value="No diff available" />
|
|
818
|
+
)}
|
|
819
|
+
</div>
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
function BashDetails({ presentation }: CamelToolDetailsProps) {
|
|
824
|
+
const { input, output, outputText } = presentation;
|
|
825
|
+
const command = stringField(input, "command", "cmd");
|
|
826
|
+
const outputRecord = recordOf(output);
|
|
827
|
+
const parsedFromText = (() => {
|
|
828
|
+
if (!outputText.trim().startsWith("{")) return {};
|
|
829
|
+
try {
|
|
830
|
+
return recordOf(JSON.parse(outputText));
|
|
831
|
+
} catch {
|
|
832
|
+
return {};
|
|
833
|
+
}
|
|
834
|
+
})();
|
|
835
|
+
const result = Object.keys(outputRecord).length > 0
|
|
836
|
+
? outputRecord
|
|
837
|
+
: parsedFromText;
|
|
838
|
+
const stdout = stringField(result, "stdout");
|
|
839
|
+
const stderr = stringField(result, "stderr");
|
|
840
|
+
const exitCode = numberField(result, "exit_code", "exitCode");
|
|
841
|
+
return (
|
|
842
|
+
<div className="space-y-1">
|
|
843
|
+
<DetailRow label="Command:" value={command} copyValue={command} mono />
|
|
844
|
+
<ProjectRow input={input} />
|
|
845
|
+
<DetailRow label="Description:" value={stringField(input, "description")} />
|
|
846
|
+
{exitCode != null ? (
|
|
847
|
+
<DetailRow label="Exit code:" value={String(exitCode)} />
|
|
848
|
+
) : null}
|
|
849
|
+
<OutputBlock label="Stdout" value={stdout} />
|
|
850
|
+
<OutputBlock label="Stderr" value={stderr} />
|
|
851
|
+
{!stdout && !stderr ? (
|
|
852
|
+
<OutputBlock
|
|
853
|
+
label={presentation.failed ? "Error" : "Output"}
|
|
854
|
+
value={presentation.errorText || outputText}
|
|
855
|
+
/>
|
|
856
|
+
) : null}
|
|
857
|
+
</div>
|
|
858
|
+
);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function SearchDetails({
|
|
862
|
+
presentation,
|
|
863
|
+
label,
|
|
864
|
+
}: CamelToolDetailsProps & { label: "Files" | "Matches" }) {
|
|
865
|
+
const { input, outputText } = presentation;
|
|
866
|
+
const cleanedOutput = outputText
|
|
867
|
+
.split(/\r?\n/)
|
|
868
|
+
.filter((line) => !/^Found\s+\d+/i.test(line.trim()))
|
|
869
|
+
.join("\n");
|
|
870
|
+
const count =
|
|
871
|
+
parseCount(outputText, label === "Files" ? "files" : "matches");
|
|
872
|
+
return (
|
|
873
|
+
<div className="space-y-1">
|
|
874
|
+
<DetailRow
|
|
875
|
+
label="Pattern:"
|
|
876
|
+
value={stringField(input, "pattern", "glob", "query")}
|
|
877
|
+
mono
|
|
878
|
+
/>
|
|
879
|
+
<DetailRow
|
|
880
|
+
label="Path:"
|
|
881
|
+
value={stringField(input, "path", "cwd")}
|
|
882
|
+
mono
|
|
883
|
+
/>
|
|
884
|
+
<ProjectRow input={input} />
|
|
885
|
+
<DetailRow label="Mode:" value={stringField(input, "output_mode")} />
|
|
886
|
+
{count != null ? <DetailRow label="Count:" value={String(count)} /> : null}
|
|
887
|
+
<OutputBlock label={label} value={cleanedOutput || outputText} />
|
|
888
|
+
</div>
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function ListDetails({ presentation }: CamelToolDetailsProps) {
|
|
893
|
+
const { input, outputText } = presentation;
|
|
894
|
+
return (
|
|
895
|
+
<div className="space-y-1">
|
|
896
|
+
<DetailRow
|
|
897
|
+
label="Path:"
|
|
898
|
+
value={stringField(input, "path")}
|
|
899
|
+
mono
|
|
900
|
+
/>
|
|
901
|
+
<ProjectRow input={input} />
|
|
902
|
+
<OutputBlock label="Files" value={outputText} />
|
|
903
|
+
</div>
|
|
904
|
+
);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
function WebDetails({
|
|
908
|
+
presentation,
|
|
909
|
+
mode,
|
|
910
|
+
}: CamelToolDetailsProps & { mode: "search" | "fetch" }) {
|
|
911
|
+
const { input, outputText } = presentation;
|
|
912
|
+
return (
|
|
913
|
+
<div className="space-y-1">
|
|
914
|
+
{mode === "fetch" ? (
|
|
915
|
+
<DetailRow
|
|
916
|
+
label="URL:"
|
|
917
|
+
value={stringField(input, "url")}
|
|
918
|
+
copyValue={stringField(input, "url")}
|
|
919
|
+
mono
|
|
920
|
+
/>
|
|
921
|
+
) : (
|
|
922
|
+
<DetailRow
|
|
923
|
+
label="Query:"
|
|
924
|
+
value={stringField(input, "query", "q")}
|
|
925
|
+
copyValue={stringField(input, "query", "q")}
|
|
926
|
+
/>
|
|
927
|
+
)}
|
|
928
|
+
<DetailRow
|
|
929
|
+
label="Prompt:"
|
|
930
|
+
value={stringField(input, "prompt")}
|
|
931
|
+
/>
|
|
932
|
+
<OutputBlock
|
|
933
|
+
label={mode === "fetch" ? "Response" : "Response"}
|
|
934
|
+
value={outputText}
|
|
935
|
+
/>
|
|
936
|
+
</div>
|
|
937
|
+
);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function TaskDetails({ presentation }: CamelToolDetailsProps) {
|
|
941
|
+
const { input, output, outputText } = presentation;
|
|
942
|
+
const tasks = Array.isArray(input.todos)
|
|
943
|
+
? input.todos.map(recordOf)
|
|
944
|
+
: Array.isArray(input.steps)
|
|
945
|
+
? input.steps.map(recordOf)
|
|
946
|
+
: [];
|
|
947
|
+
return (
|
|
948
|
+
<div className="space-y-2">
|
|
949
|
+
{tasks.length > 0 ? (
|
|
950
|
+
<div className="space-y-1">
|
|
951
|
+
<div className="text-xs text-muted-foreground/60">Tasks:</div>
|
|
952
|
+
<div className="space-y-1 pl-4">
|
|
953
|
+
{tasks.map((task, index) => {
|
|
954
|
+
const status = stringField(task, "status") || "pending";
|
|
955
|
+
const text =
|
|
956
|
+
stringField(task, "content", "text", "activeForm") ||
|
|
957
|
+
"Untitled task";
|
|
958
|
+
return (
|
|
959
|
+
<div
|
|
960
|
+
key={`${status}:${index}`}
|
|
961
|
+
className="flex items-start gap-2 text-xs"
|
|
962
|
+
>
|
|
963
|
+
<span className="font-mono text-muted-foreground/60">
|
|
964
|
+
{["done", "completed"].includes(status) ? "[x]" : "[ ]"}
|
|
965
|
+
</span>
|
|
966
|
+
<span className="min-w-0 flex-1 text-muted-foreground/80">
|
|
967
|
+
{text}
|
|
968
|
+
</span>
|
|
969
|
+
<span className="text-[0.65rem] uppercase text-muted-foreground/50">
|
|
970
|
+
{status}
|
|
971
|
+
</span>
|
|
972
|
+
</div>
|
|
973
|
+
);
|
|
974
|
+
})}
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
) : (
|
|
978
|
+
<DetailRow label="Tasks:" value="No tasks provided" />
|
|
979
|
+
)}
|
|
980
|
+
<OutputBlock
|
|
981
|
+
label="Result"
|
|
982
|
+
value={
|
|
983
|
+
typeof output === "object"
|
|
984
|
+
? summarizeResult(recordOf(output))
|
|
985
|
+
: outputText
|
|
986
|
+
}
|
|
987
|
+
/>
|
|
988
|
+
</div>
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function summarizeResult(output: UnknownRecord): string {
|
|
993
|
+
const preferred = [
|
|
994
|
+
"message",
|
|
995
|
+
"summary",
|
|
996
|
+
"status",
|
|
997
|
+
"result",
|
|
998
|
+
].flatMap((key) => {
|
|
999
|
+
const value = output[key];
|
|
1000
|
+
return value == null ? [] : [`${humanize(key)}: ${normalizeOutputText(value)}`];
|
|
1001
|
+
});
|
|
1002
|
+
if (preferred.length > 0) return preferred.join("\n");
|
|
1003
|
+
const counts = ["done", "total", "success", "ok"].flatMap((key) =>
|
|
1004
|
+
output[key] == null ? [] : [`${humanize(key)}: ${String(output[key])}`],
|
|
1005
|
+
);
|
|
1006
|
+
return counts.join("\n");
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
function AgentDetails({ presentation }: CamelToolDetailsProps) {
|
|
1010
|
+
const { input, output, outputText, running, failed } = presentation;
|
|
1011
|
+
const outputRecord = recordOf(output);
|
|
1012
|
+
const activities = Array.isArray(outputRecord.activities)
|
|
1013
|
+
? outputRecord.activities
|
|
1014
|
+
: Array.isArray(recordOf(outputRecord.details).activities)
|
|
1015
|
+
? recordOf(outputRecord.details).activities as unknown[]
|
|
1016
|
+
: [];
|
|
1017
|
+
const request = stringField(input, "question", "prompt", "description");
|
|
1018
|
+
return (
|
|
1019
|
+
<div className="space-y-1">
|
|
1020
|
+
<DetailRow label="Task:" value={stringField(input, "description")} />
|
|
1021
|
+
<DetailRow label="Agent:" value={stringField(input, "agent", "name")} />
|
|
1022
|
+
<DetailRow label="Model:" value={stringField(input, "model")} />
|
|
1023
|
+
<DetailRow
|
|
1024
|
+
label="Request:"
|
|
1025
|
+
value={
|
|
1026
|
+
request ? (
|
|
1027
|
+
<span className="whitespace-pre-wrap break-words text-foreground/80">
|
|
1028
|
+
{request}
|
|
1029
|
+
</span>
|
|
1030
|
+
) : undefined
|
|
1031
|
+
}
|
|
1032
|
+
/>
|
|
1033
|
+
<div className="mt-2">
|
|
1034
|
+
<div className="mb-1 text-[0.7rem] text-muted-foreground/60">
|
|
1035
|
+
Activity
|
|
1036
|
+
</div>
|
|
1037
|
+
{activities.length > 0 ? (
|
|
1038
|
+
<div className="space-y-1">
|
|
1039
|
+
{activities.slice(-10).map((activity, index) => (
|
|
1040
|
+
<div
|
|
1041
|
+
key={`${normalizeOutputText(activity)}:${index}`}
|
|
1042
|
+
className="flex min-w-0 items-center gap-2"
|
|
1043
|
+
>
|
|
1044
|
+
<span
|
|
1045
|
+
className={cn(
|
|
1046
|
+
"size-1.5 shrink-0 rounded-full bg-muted-foreground/40",
|
|
1047
|
+
running && index === activities.slice(-10).length - 1 &&
|
|
1048
|
+
"animate-pulse bg-blue-500",
|
|
1049
|
+
failed && "bg-red-500",
|
|
1050
|
+
)}
|
|
1051
|
+
/>
|
|
1052
|
+
<span className="min-w-0 truncate text-muted-foreground/70">
|
|
1053
|
+
{normalizeOutputText(activity)}
|
|
1054
|
+
</span>
|
|
1055
|
+
</div>
|
|
1056
|
+
))}
|
|
1057
|
+
</div>
|
|
1058
|
+
) : (
|
|
1059
|
+
<div className="text-muted-foreground/60">
|
|
1060
|
+
{running
|
|
1061
|
+
? "Waiting for the first tool call..."
|
|
1062
|
+
: failed
|
|
1063
|
+
? "No tool activity was recorded before this run stopped."
|
|
1064
|
+
: "Detailed tool activity was not recorded."}
|
|
1065
|
+
</div>
|
|
1066
|
+
)}
|
|
1067
|
+
</div>
|
|
1068
|
+
{outputText ? (
|
|
1069
|
+
<div className="mt-2">
|
|
1070
|
+
<div className="mb-1 text-[0.7rem] text-muted-foreground/60">
|
|
1071
|
+
Agent result
|
|
1072
|
+
</div>
|
|
1073
|
+
<div className="max-h-72 overflow-auto rounded bg-muted/30 p-2 text-sm text-foreground/90">
|
|
1074
|
+
<MessageResponse>{outputText}</MessageResponse>
|
|
1075
|
+
</div>
|
|
1076
|
+
</div>
|
|
1077
|
+
) : null}
|
|
1078
|
+
{numberField(recordOf(outputRecord.details), "durationMs") != null ? (
|
|
1079
|
+
<DetailRow
|
|
1080
|
+
label="Duration:"
|
|
1081
|
+
value={`${Math.max(
|
|
1082
|
+
1,
|
|
1083
|
+
Math.round(
|
|
1084
|
+
numberField(recordOf(outputRecord.details), "durationMs")! / 1000,
|
|
1085
|
+
),
|
|
1086
|
+
)}s`}
|
|
1087
|
+
/>
|
|
1088
|
+
) : null}
|
|
1089
|
+
</div>
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function SkillDetails({
|
|
1094
|
+
presentation,
|
|
1095
|
+
resource = false,
|
|
1096
|
+
}: CamelToolDetailsProps & { resource?: boolean }) {
|
|
1097
|
+
const { input, outputText } = presentation;
|
|
1098
|
+
const skill = stringField(input, "skill", "name");
|
|
1099
|
+
const path = stringField(input, "path");
|
|
1100
|
+
return (
|
|
1101
|
+
<div className="space-y-1">
|
|
1102
|
+
<DetailRow label="Skill:" value={skill} mono />
|
|
1103
|
+
{resource ? (
|
|
1104
|
+
<DetailRow label="Resource:" value={path} copyValue={path} mono />
|
|
1105
|
+
) : null}
|
|
1106
|
+
<OutputBlock
|
|
1107
|
+
label={resource ? "Preview" : "Status"}
|
|
1108
|
+
value={resource ? previewLines(outputText, 10) : outputText}
|
|
1109
|
+
/>
|
|
1110
|
+
</div>
|
|
1111
|
+
);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
function AskUserDetails({ presentation }: CamelToolDetailsProps) {
|
|
1115
|
+
const questions = questionsOf(presentation.input);
|
|
1116
|
+
const answer = presentation.outputText;
|
|
1117
|
+
return (
|
|
1118
|
+
<div className="space-y-2">
|
|
1119
|
+
{questions.length > 0 ? (
|
|
1120
|
+
questions.map((question, index) => (
|
|
1121
|
+
<div key={`${stringField(question, "question", "prompt")}:${index}`}>
|
|
1122
|
+
<p className="font-medium text-foreground/85">
|
|
1123
|
+
{stringField(question, "question", "prompt") || "Question"}
|
|
1124
|
+
</p>
|
|
1125
|
+
<p className="text-muted-foreground/60">
|
|
1126
|
+
{answer || "—"}
|
|
1127
|
+
</p>
|
|
1128
|
+
</div>
|
|
1129
|
+
))
|
|
1130
|
+
) : (
|
|
1131
|
+
<GenericDetails presentation={presentation} />
|
|
1132
|
+
)}
|
|
1133
|
+
</div>
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
function JavaScriptDetails({ presentation }: CamelToolDetailsProps) {
|
|
1138
|
+
const { input, outputText } = presentation;
|
|
1139
|
+
const timeout = numberField(input, "timeoutMs", "timeout_ms");
|
|
1140
|
+
const outputLimit = numberField(
|
|
1141
|
+
input,
|
|
1142
|
+
"maxOutputCharacters",
|
|
1143
|
+
"max_output_characters",
|
|
1144
|
+
);
|
|
1145
|
+
return (
|
|
1146
|
+
<div className="space-y-1">
|
|
1147
|
+
<DetailRow label="Runtime:" value="JavaScript code mode" />
|
|
1148
|
+
<DetailRow label="Description:" value={stringField(input, "description")} />
|
|
1149
|
+
{timeout != null ? (
|
|
1150
|
+
<DetailRow label="Timeout:" value={`${timeout}ms`} />
|
|
1151
|
+
) : null}
|
|
1152
|
+
{outputLimit != null ? (
|
|
1153
|
+
<DetailRow label="Output limit:" value={`${outputLimit} chars`} />
|
|
1154
|
+
) : null}
|
|
1155
|
+
<OutputBlock label="Code" value={stringField(input, "code")} />
|
|
1156
|
+
<OutputBlock label="Output" value={outputText} />
|
|
1157
|
+
</div>
|
|
1158
|
+
);
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function McpDetails({ presentation }: CamelToolDetailsProps) {
|
|
1162
|
+
const parsed = parseMcpName(presentation.name);
|
|
1163
|
+
return (
|
|
1164
|
+
<div className="space-y-1">
|
|
1165
|
+
{parsed ? (
|
|
1166
|
+
<>
|
|
1167
|
+
<DetailRow label="MCP Server:" value={parsed.server} />
|
|
1168
|
+
<DetailRow label="Tool:" value={parsed.tool} />
|
|
1169
|
+
</>
|
|
1170
|
+
) : null}
|
|
1171
|
+
<GenericDetails presentation={presentation} hideProject />
|
|
1172
|
+
</div>
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function GenericDetails({
|
|
1177
|
+
presentation,
|
|
1178
|
+
hideProject = false,
|
|
1179
|
+
}: CamelToolDetailsProps & { hideProject?: boolean }) {
|
|
1180
|
+
const { input, outputText, errorText } = presentation;
|
|
1181
|
+
const inputText = Object.keys(input).length > 0 ? safeStringify(input) : "";
|
|
1182
|
+
return (
|
|
1183
|
+
<div className="space-y-1">
|
|
1184
|
+
{!hideProject ? <ProjectRow input={input} /> : null}
|
|
1185
|
+
<OutputBlock label="Input" value={inputText} />
|
|
1186
|
+
<OutputBlock
|
|
1187
|
+
label={presentation.failed ? "Error" : "Output"}
|
|
1188
|
+
value={errorText || outputText}
|
|
1189
|
+
/>
|
|
1190
|
+
{!inputText && !outputText && !errorText ? (
|
|
1191
|
+
<DetailRow label="Details:" value="No additional data" />
|
|
1192
|
+
) : null}
|
|
1193
|
+
</div>
|
|
1194
|
+
);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
export function CamelToolDetails({
|
|
1198
|
+
presentation,
|
|
1199
|
+
}: CamelToolDetailsProps) {
|
|
1200
|
+
switch (presentation.kind) {
|
|
1201
|
+
case "read":
|
|
1202
|
+
return <ReadDetails presentation={presentation} />;
|
|
1203
|
+
case "write":
|
|
1204
|
+
return <WriteDetails presentation={presentation} />;
|
|
1205
|
+
case "edit":
|
|
1206
|
+
return <EditDetails presentation={presentation} />;
|
|
1207
|
+
case "bash":
|
|
1208
|
+
return <BashDetails presentation={presentation} />;
|
|
1209
|
+
case "glob":
|
|
1210
|
+
return <SearchDetails presentation={presentation} label="Files" />;
|
|
1211
|
+
case "grep":
|
|
1212
|
+
return <SearchDetails presentation={presentation} label="Matches" />;
|
|
1213
|
+
case "list":
|
|
1214
|
+
case "find":
|
|
1215
|
+
return <ListDetails presentation={presentation} />;
|
|
1216
|
+
case "web-search":
|
|
1217
|
+
return <WebDetails presentation={presentation} mode="search" />;
|
|
1218
|
+
case "web-fetch":
|
|
1219
|
+
return <WebDetails presentation={presentation} mode="fetch" />;
|
|
1220
|
+
case "javascript":
|
|
1221
|
+
return <JavaScriptDetails presentation={presentation} />;
|
|
1222
|
+
case "tasks":
|
|
1223
|
+
return <TaskDetails presentation={presentation} />;
|
|
1224
|
+
case "agent":
|
|
1225
|
+
return <AgentDetails presentation={presentation} />;
|
|
1226
|
+
case "skill":
|
|
1227
|
+
return <SkillDetails presentation={presentation} />;
|
|
1228
|
+
case "skill-resource":
|
|
1229
|
+
return <SkillDetails presentation={presentation} resource />;
|
|
1230
|
+
case "ask-user":
|
|
1231
|
+
return <AskUserDetails presentation={presentation} />;
|
|
1232
|
+
case "suggestions":
|
|
1233
|
+
return (
|
|
1234
|
+
<OutputBlock
|
|
1235
|
+
label="Suggestions"
|
|
1236
|
+
value={presentation.outputText || safeStringify(presentation.input)}
|
|
1237
|
+
/>
|
|
1238
|
+
);
|
|
1239
|
+
case "notebook":
|
|
1240
|
+
return (
|
|
1241
|
+
<div className="space-y-1">
|
|
1242
|
+
<DetailRow
|
|
1243
|
+
label="Notebook:"
|
|
1244
|
+
value={stringField(presentation.input, "notebook_path", "path")}
|
|
1245
|
+
mono
|
|
1246
|
+
/>
|
|
1247
|
+
<DetailRow
|
|
1248
|
+
label="Cell:"
|
|
1249
|
+
value={stringField(presentation.input, "cell_id", "cell_number")}
|
|
1250
|
+
/>
|
|
1251
|
+
<OutputBlock
|
|
1252
|
+
label="Code"
|
|
1253
|
+
value={stringField(presentation.input, "code", "new_source")}
|
|
1254
|
+
/>
|
|
1255
|
+
</div>
|
|
1256
|
+
);
|
|
1257
|
+
case "mcp":
|
|
1258
|
+
return <McpDetails presentation={presentation} />;
|
|
1259
|
+
case "generic":
|
|
1260
|
+
return <GenericDetails presentation={presentation} />;
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function formatBytes(bytes: number): string {
|
|
1265
|
+
if (!Number.isFinite(bytes)) return "";
|
|
1266
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1267
|
+
const kilobytes = bytes / 1024;
|
|
1268
|
+
if (kilobytes < 1024) return `${kilobytes.toFixed(1)} KB`;
|
|
1269
|
+
return `${(kilobytes / 1024).toFixed(1)} MB`;
|
|
1270
|
+
}
|