@springbrand/message-panel 0.1.0 → 0.1.2

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.
Files changed (99) hide show
  1. package/dist/demo/camel-chat-showcase.d.ts +1 -0
  2. package/dist/demo/camel-chat-showcase.js +1007 -0
  3. package/dist/demo/fixtures.d.ts +32 -0
  4. package/dist/demo/fixtures.js +250 -0
  5. package/dist/demo/index.js +4 -0
  6. package/dist/demo/message-panel-chat-showcase.d.ts +1 -0
  7. package/dist/demo/message-panel-chat-showcase.js +114 -0
  8. package/dist/demo/message-panel-gallery.d.ts +1 -0
  9. package/dist/demo/message-panel-gallery.js +130 -0
  10. package/dist/src/adapters/ai-sdk.d.ts +4 -0
  11. package/dist/src/adapters/ai-sdk.js +25 -0
  12. package/dist/src/camel/camel-chat-messages.d.ts +44 -0
  13. package/dist/src/camel/camel-chat-messages.js +421 -0
  14. package/dist/src/camel/camel-model-select.d.ts +13 -0
  15. package/dist/src/camel/camel-model-select.js +7 -0
  16. package/dist/src/camel/camel-plan-bar.d.ts +5 -0
  17. package/dist/src/camel/camel-plan-bar.js +24 -0
  18. package/dist/src/camel/camel-prompt-input.d.ts +32 -0
  19. package/dist/src/camel/camel-prompt-input.js +182 -0
  20. package/dist/src/camel/camel-tool-presentation.d.ts +21 -0
  21. package/dist/src/camel/camel-tool-presentation.js +784 -0
  22. package/dist/src/camel/camel-turn.d.ts +57 -0
  23. package/dist/src/camel/camel-turn.js +203 -0
  24. package/dist/src/camel/index.js +7 -0
  25. package/dist/src/camel/turn-summary-bar.d.ts +8 -0
  26. package/dist/src/camel/turn-summary-bar.js +12 -0
  27. package/dist/src/chat-summary-panel.d.ts +22 -0
  28. package/dist/src/chat-summary-panel.js +26 -0
  29. package/dist/src/composer/chat-composer.d.ts +4 -0
  30. package/dist/src/composer/chat-composer.js +90 -0
  31. package/dist/src/composer/composer-attachments.d.ts +13 -0
  32. package/dist/src/composer/composer-attachments.js +47 -0
  33. package/dist/src/composer/composer-trigger-popover.d.ts +31 -0
  34. package/dist/src/composer/composer-trigger-popover.js +29 -0
  35. package/dist/src/composer/composer.d.ts +15 -0
  36. package/dist/src/composer/composer.js +180 -0
  37. package/dist/src/composer/drop-controller.d.ts +8 -0
  38. package/dist/src/composer/drop-controller.js +69 -0
  39. package/dist/src/composer/index.js +6 -0
  40. package/dist/src/composer/types.d.ts +62 -0
  41. package/dist/src/composer/types.js +1 -0
  42. package/dist/src/contracts.d.ts +94 -0
  43. package/dist/src/contracts.js +1 -0
  44. package/dist/src/conversation.d.ts +13 -0
  45. package/dist/src/conversation.js +18 -0
  46. package/dist/src/index.js +12 -0
  47. package/dist/src/internal/cn.d.ts +1 -0
  48. package/dist/src/internal/cn.js +3 -0
  49. package/dist/src/message-panel.d.ts +4 -0
  50. package/dist/src/message-panel.js +144 -0
  51. package/dist/src/message.d.ts +36 -0
  52. package/dist/src/message.js +83 -0
  53. package/dist/src/parts/index.d.ts +187 -0
  54. package/dist/src/parts/index.js +332 -0
  55. package/dist/src/parts/kind.d.ts +3 -0
  56. package/dist/src/parts/kind.js +69 -0
  57. package/dist/src/parts/plan.d.ts +10 -0
  58. package/dist/src/parts/plan.js +40 -0
  59. package/dist/src/parts/primitives.d.ts +70 -0
  60. package/dist/src/parts/primitives.js +153 -0
  61. package/dist/src/registry.d.ts +4 -0
  62. package/dist/src/registry.js +31 -0
  63. package/dist/src/status.d.ts +26 -0
  64. package/dist/src/status.js +74 -0
  65. package/package.json +17 -8
  66. package/demo/camel-chat-showcase.tsx +0 -1265
  67. package/demo/fixtures.ts +0 -292
  68. package/demo/message-panel-chat-showcase.tsx +0 -250
  69. package/demo/message-panel-gallery.tsx +0 -389
  70. package/src/adapters/ai-sdk.ts +0 -32
  71. package/src/camel/camel-chat-messages.tsx +0 -1420
  72. package/src/camel/camel-model-select.tsx +0 -88
  73. package/src/camel/camel-plan-bar.tsx +0 -115
  74. package/src/camel/camel-prompt-input.tsx +0 -540
  75. package/src/camel/camel-tool-presentation.tsx +0 -1270
  76. package/src/camel/camel-turn.ts +0 -315
  77. package/src/camel/turn-summary-bar.tsx +0 -81
  78. package/src/chat-summary-panel.tsx +0 -229
  79. package/src/composer/chat-composer.tsx +0 -216
  80. package/src/composer/composer-attachments.tsx +0 -138
  81. package/src/composer/composer-trigger-popover.tsx +0 -236
  82. package/src/composer/composer.tsx +0 -425
  83. package/src/composer/drop-controller.ts +0 -80
  84. package/src/composer/types.ts +0 -79
  85. package/src/contracts.ts +0 -139
  86. package/src/conversation.tsx +0 -76
  87. package/src/internal/cn.ts +0 -3
  88. package/src/message-panel.tsx +0 -368
  89. package/src/message.tsx +0 -254
  90. package/src/parts/index.tsx +0 -1149
  91. package/src/parts/kind.ts +0 -67
  92. package/src/parts/plan.ts +0 -63
  93. package/src/parts/primitives.tsx +0 -439
  94. package/src/registry.ts +0 -44
  95. package/src/status.tsx +0 -132
  96. /package/{demo/index.ts → dist/demo/index.d.ts} +0 -0
  97. /package/{src/camel/index.ts → dist/src/camel/index.d.ts} +0 -0
  98. /package/{src/composer/index.ts → dist/src/composer/index.d.ts} +0 -0
  99. /package/{src/index.ts → dist/src/index.d.ts} +0 -0
@@ -1,1270 +0,0 @@
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
- }