File size: 4,342 Bytes
0b9dc2e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
import type { ToolCallBlock, ToolResultBlock } from '@agentscope-ai/agentscope/message';
import { ChevronRight } from 'lucide-react';
import * as mime from 'mime-types';
import type { ReactNode } from 'react';

import { CornerLine, ToolStateIcon } from './_shared';
import type { TFunction, ToolCallWithResult } from './types';
import { Button } from '@/components/ui/button.tsx';
import {
	Collapsible,
	CollapsibleContent,
	CollapsibleTrigger,
} from '@/components/ui/collapsible.tsx';

function processToolInput(input: string): string {
	try {
		const obj = JSON.parse(input);
		const entries = Object.entries(obj).map(([k, v]) => `${k}: "${v}"`);
		return entries.join('\n');
	} catch {
		return input;
	}
}

export function defaultGetDisplayName(call: ToolCallBlock): string {
	return call.name;
}

export function defaultRenderCallArgs(call: ToolCallBlock): ReactNode {
	if (call.input.length <= 2) return null;
	return processToolInput(call.input);
}

export function defaultRenderResult(

	call: ToolCallBlock,

	result: ToolResultBlock,

	t: TFunction,

	maxLines = 7,

): ReactNode {
	if (call.state === 'asking' || !result || result.state === 'running') {
		return <span>{t('common.running')} ...</span>;
	}
	if (result.state === 'interrupted') {
		return <span>{t('common.interrupted')}</span>;
	}

	let resultStr: string;
	if (typeof result.output === 'string') {
		resultStr = result.output;
	} else {
		const parts = result.output.map((b) => {
			if (b.type === 'text') return b.text;
			const mainType = b.source.media_type.split('/')[0].toUpperCase();
			const ext = (mime.extension(b.source.media_type) || 'bin').toLowerCase();
			return `[${mainType}.${ext}]`;
		});
		resultStr = parts.join('\n');
	}

	let lines = resultStr.split('\n');
	if (lines.length > maxLines) {
		const total = lines.length;
		lines = lines.slice(0, maxLines);
		lines.push(t('tool.moreLines', { count: total - maxLines }));
	}

	return (
		<div className="flex flex-col flex-1 min-w-0">

			{lines.map((line, i) => (

				<div key={i} className="truncate">

					{line}

				</div>

			))}

		</div>
	);
}

export function defaultRenderConfirmBody(call: ToolCallBlock): ReactNode {
	return (
		<div className="w-full max-w-full overflow-hidden text-ellipsis truncate">

			<div className="text-secondary-foreground">{call.input}</div>

		</div>
	);
}

/**

 * Default group layout: each call is an independent block with a state icon,

 * `displayName(args)` header line, and (optionally) a corner-line-prefixed

 * result block beneath it. Used by tools without a custom `renderGroup`,

 * e.g. arbitrary MCP tools.

 *

 * `getDisplayName` / `renderCallArgs` / `renderResult` are passed in from

 * `index.ts` so this function stays decoupled from the renderer registry.

 */
export function defaultRenderGroup(

	calls: ToolCallWithResult[],

	_t: TFunction,

	resolvers: {

		getDisplayName: (call: ToolCallBlock) => string;

		renderCallArgs: (call: ToolCallBlock) => ReactNode;

		renderResult: (call: ToolCallBlock, result: ToolResultBlock) => ReactNode;

	},

): ReactNode {
	return (
		<>

			{calls.map(({ call, result }) => {

				const displayName = resolvers.getDisplayName(call);

				const args = resolvers.renderCallArgs(call);

				const resultContent = result ? resolvers.renderResult(call, result) : null;

				return (

					<Collapsible key={call.id} className="flex flex-col w-full max-w-full text-sm">

						<CollapsibleTrigger asChild>

							<Button

								variant={'ghost'}

								className="group hover:bg-transparent data-[state=open]:bg-transparent justify-start px-0 gap-2 active:!translate-y-0"

							>

								<ToolStateIcon states={[result?.state]} />

								<strong className="shrink-0 text-primary">{displayName}</strong>

								{args && <span className="truncate min-w-0 text-left">{args}</span>}

								{resultContent && (

									<ChevronRight className="size-3 shrink-0 group-data-[state=open]:rotate-90" />

								)}

							</Button>

						</CollapsibleTrigger>

						{resultContent && (

							<CollapsibleContent className="flex flex-row gap-x-2 pl-6 max-w-full">

								<CornerLine />

								{resultContent}

							</CollapsibleContent>

						)}

					</Collapsible>

				);

			})}

		</>
	);
}