mirror of
https://github.com/Tencent/WeKnora.git
synced 2026-08-28 17:43:11 +08:00
ui(chat): show skill names on tool cards and render sandbox file lists
Skill steps were hard to tell apart in the stream, and list_sandbox_files fell through to raw output. Quote the skill or path in the title and add dedicated cards for read_skill and sandbox listings.
This commit is contained in:
@@ -335,11 +335,23 @@ const messages = {
|
||||
"queryKnowledgeGraph": "知识图谱查询",
|
||||
"readSkill": "读取技能",
|
||||
"executeSkillScript": "执行技能脚本",
|
||||
"listSandboxFiles": "列出沙箱文件",
|
||||
"readSandboxFile": "读取沙箱文件",
|
||||
"shellExec": "执行沙箱命令",
|
||||
"dataAnalysis": "数据分析",
|
||||
"dataSchema": "数据结构",
|
||||
"databaseQuery": "数据库查询"
|
||||
},
|
||||
"skillFiles": {
|
||||
"heading": "技能文件",
|
||||
"script": "脚本",
|
||||
"instructions": "技能说明"
|
||||
},
|
||||
"sandboxFiles": {
|
||||
"found": "找到 {count} 个文件",
|
||||
"empty": "暂无文件",
|
||||
"truncated": "列表已截断"
|
||||
},
|
||||
"shellExec": {
|
||||
"workDir": "目录",
|
||||
"exitCode": "退出码",
|
||||
@@ -844,11 +856,23 @@ const messages = {
|
||||
"queryKnowledgeGraph": "Knowledge Graph Query",
|
||||
"readSkill": "Read Skill",
|
||||
"executeSkillScript": "Execute Skill Script",
|
||||
"listSandboxFiles": "List sandbox files",
|
||||
"readSandboxFile": "Read sandbox file",
|
||||
"shellExec": "Run sandbox command",
|
||||
"dataAnalysis": "Data Analysis",
|
||||
"dataSchema": "Data Schema",
|
||||
"databaseQuery": "Database Query"
|
||||
},
|
||||
"skillFiles": {
|
||||
"heading": "Skill files",
|
||||
"script": "script",
|
||||
"instructions": "Instructions"
|
||||
},
|
||||
"sandboxFiles": {
|
||||
"found": "Found {count} file(s)",
|
||||
"empty": "No files",
|
||||
"truncated": "List truncated"
|
||||
},
|
||||
"shellExec": {
|
||||
"workDir": "Directory",
|
||||
"exitCode": "Exit code",
|
||||
|
||||
@@ -5330,11 +5330,23 @@ export default {
|
||||
queryKnowledgeGraph: 'Knowledge Graph Query',
|
||||
readSkill: 'Read Skill',
|
||||
executeSkillScript: 'Execute Skill Script',
|
||||
listSandboxFiles: 'List sandbox files',
|
||||
readSandboxFile: 'Read sandbox file',
|
||||
shellExec: 'Run sandbox command',
|
||||
dataAnalysis: 'Data Analysis',
|
||||
dataSchema: 'Data Schema',
|
||||
databaseQuery: 'Database Query'
|
||||
},
|
||||
skillFiles: {
|
||||
heading: 'Skill files',
|
||||
script: 'script',
|
||||
instructions: 'Instructions'
|
||||
},
|
||||
sandboxFiles: {
|
||||
found: 'Found {count} file(s)',
|
||||
empty: 'No files',
|
||||
truncated: 'List truncated'
|
||||
},
|
||||
shellExec: {
|
||||
workDir: 'Directory',
|
||||
exitCode: 'Exit code',
|
||||
|
||||
@@ -1308,11 +1308,23 @@ export default {
|
||||
queryKnowledgeGraph: '지식 그래프 조회',
|
||||
readSkill: '스킬 읽기',
|
||||
executeSkillScript: '스킬 스크립트 실행',
|
||||
listSandboxFiles: '샌드박스 파일 목록',
|
||||
readSandboxFile: '샌드박스 파일 읽기',
|
||||
shellExec: '샌드박스 명령 실행',
|
||||
dataAnalysis: '데이터 분석',
|
||||
dataSchema: '데이터 구조',
|
||||
databaseQuery: '데이터베이스 조회'
|
||||
},
|
||||
skillFiles: {
|
||||
heading: '스킬 파일',
|
||||
script: '스크립트',
|
||||
instructions: '스킬 안내'
|
||||
},
|
||||
sandboxFiles: {
|
||||
found: '파일 {count}개 발견',
|
||||
empty: '파일 없음',
|
||||
truncated: '목록이 잘림'
|
||||
},
|
||||
shellExec: {
|
||||
workDir: '디렉터리',
|
||||
exitCode: '종료 코드',
|
||||
|
||||
@@ -1308,11 +1308,23 @@ export default {
|
||||
queryKnowledgeGraph: 'Запрос графа знаний',
|
||||
readSkill: 'Чтение навыка',
|
||||
executeSkillScript: 'Выполнение скрипта навыка',
|
||||
listSandboxFiles: 'Список файлов песочницы',
|
||||
readSandboxFile: 'Чтение файла песочницы',
|
||||
shellExec: 'Выполнение команды в песочнице',
|
||||
dataAnalysis: 'Анализ данных',
|
||||
dataSchema: 'Структура данных',
|
||||
databaseQuery: 'Запрос к базе данных'
|
||||
},
|
||||
skillFiles: {
|
||||
heading: 'Файлы навыка',
|
||||
script: 'скрипт',
|
||||
instructions: 'Инструкции навыка'
|
||||
},
|
||||
sandboxFiles: {
|
||||
found: 'Найдено файлов: {count}',
|
||||
empty: 'Нет файлов',
|
||||
truncated: 'Список обрезан'
|
||||
},
|
||||
shellExec: {
|
||||
workDir: 'Каталог',
|
||||
exitCode: 'Код выхода',
|
||||
|
||||
@@ -1310,11 +1310,23 @@ export default {
|
||||
queryKnowledgeGraph: '知识图谱查询',
|
||||
readSkill: '读取技能',
|
||||
executeSkillScript: '执行技能脚本',
|
||||
listSandboxFiles: '列出沙箱文件',
|
||||
readSandboxFile: '读取沙箱文件',
|
||||
shellExec: '执行沙箱命令',
|
||||
dataAnalysis: '数据分析',
|
||||
dataSchema: '数据结构',
|
||||
databaseQuery: '数据库查询'
|
||||
},
|
||||
skillFiles: {
|
||||
heading: '技能文件',
|
||||
script: '脚本',
|
||||
instructions: '技能说明'
|
||||
},
|
||||
sandboxFiles: {
|
||||
found: '找到 {count} 个文件',
|
||||
empty: '暂无文件',
|
||||
truncated: '列表已截断'
|
||||
},
|
||||
shellExec: {
|
||||
workDir: '目录',
|
||||
exitCode: '退出码',
|
||||
|
||||
@@ -26,7 +26,9 @@ export type DisplayType =
|
||||
| 'wiki_replace_text'
|
||||
| 'wiki_rename_page'
|
||||
| 'wiki_delete_page'
|
||||
| 'shell_exec';
|
||||
| 'shell_exec'
|
||||
| 'list_sandbox_files'
|
||||
| 'read_skill';
|
||||
|
||||
// Search result item
|
||||
export interface SearchResultItem {
|
||||
@@ -338,6 +340,34 @@ export interface ShellExecData {
|
||||
stderr_truncated?: boolean;
|
||||
}
|
||||
|
||||
export interface SandboxFileEntry {
|
||||
name?: string;
|
||||
path: string;
|
||||
size?: number;
|
||||
modified_at?: string;
|
||||
}
|
||||
|
||||
export interface ListSandboxFilesData {
|
||||
display_type?: 'list_sandbox_files';
|
||||
session_id?: string;
|
||||
path?: string;
|
||||
root?: string;
|
||||
entries?: SandboxFileEntry[];
|
||||
count?: number;
|
||||
truncated?: boolean;
|
||||
}
|
||||
|
||||
export interface ReadSkillData {
|
||||
display_type?: 'read_skill';
|
||||
skill_name?: string;
|
||||
file_path?: string;
|
||||
description?: string;
|
||||
instructions?: string;
|
||||
content?: string;
|
||||
files?: string[];
|
||||
skill_dir?: string;
|
||||
}
|
||||
|
||||
// Union type for all wiki edit data
|
||||
export type WikiEditData = WikiWritePageData | WikiReplaceTextData | WikiRenamePageData | WikiDeletePageData;
|
||||
|
||||
@@ -360,7 +390,9 @@ export type ToolResultData =
|
||||
| WikiReplaceTextData
|
||||
| WikiRenamePageData
|
||||
| WikiDeletePageData
|
||||
| ShellExecData;
|
||||
| ShellExecData
|
||||
| ListSandboxFilesData
|
||||
| ReadSkillData;
|
||||
|
||||
// Action data (from index.vue)
|
||||
export interface ActionData {
|
||||
|
||||
@@ -30,6 +30,13 @@ test('getAgentToolIconName maps sandbox shell tools to the terminal icon', () =>
|
||||
assert.equal(getAgentToolIconName('shell_exec'), 'terminal')
|
||||
})
|
||||
|
||||
test('getAgentToolIconName maps skill and sandbox file tools', () => {
|
||||
assert.equal(getAgentToolIconName('read_skill'), 'file')
|
||||
assert.equal(getAgentToolIconName('list_sandbox_files'), 'folder')
|
||||
assert.equal(getAgentToolIconName('read_sandbox_file'), 'file')
|
||||
assert.equal(getAgentToolIconName('execute_skill_script'), 'code')
|
||||
})
|
||||
|
||||
test('getAgentToolIconName maps Wiki tools to semantic search and reading icons', () => {
|
||||
assert.equal(getAgentToolIconName('wiki_search'), 'search')
|
||||
assert.equal(getAgentToolIconName('wiki_read_page'), 'file-search')
|
||||
|
||||
@@ -39,9 +39,15 @@ export function getAgentToolIconName(
|
||||
if (toolName.startsWith('mcp_')) {
|
||||
return 'terminal'
|
||||
}
|
||||
if (toolName === 'shell_exec' || toolName === 'list_sandbox_files' || toolName === 'read_sandbox_file') {
|
||||
if (toolName === 'shell_exec') {
|
||||
return 'terminal'
|
||||
}
|
||||
if (toolName === 'list_sandbox_files') {
|
||||
return 'folder'
|
||||
}
|
||||
if (toolName === 'read_sandbox_file' || toolName === 'read_skill') {
|
||||
return 'file'
|
||||
}
|
||||
if (toolName === 'execute_skill_script') {
|
||||
return 'code'
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import {
|
||||
formatToolTitleWithDetail,
|
||||
getEventSkillName,
|
||||
getReadSkillTarget,
|
||||
getSandboxToolPath,
|
||||
sandboxFileListItems,
|
||||
skillFileListItems,
|
||||
skillScriptTitleCommand,
|
||||
} from './skillToolDisplay.ts'
|
||||
|
||||
test('formatToolTitleWithDetail quotes a skill or path detail', () => {
|
||||
assert.equal(formatToolTitleWithDetail('读取技能', 'pdf-processing'), '读取技能:「pdf-processing」')
|
||||
assert.equal(formatToolTitleWithDetail('读取技能', ' '), '读取技能')
|
||||
})
|
||||
|
||||
test('getReadSkillTarget prefers skill/file from tool_data over arguments', () => {
|
||||
assert.equal(
|
||||
getReadSkillTarget({
|
||||
arguments: { skill_name: 'old', file_path: 'SKILL.md' },
|
||||
tool_data: { skill_name: 'pdf-processing', file_path: 'scripts/run.py' },
|
||||
}),
|
||||
'pdf-processing/scripts/run.py',
|
||||
)
|
||||
assert.equal(
|
||||
getReadSkillTarget({ arguments: { skill_name: 'brandkit' } }),
|
||||
'brandkit',
|
||||
)
|
||||
})
|
||||
|
||||
test('getEventSkillName parses JSON-encoded arguments', () => {
|
||||
assert.equal(
|
||||
getEventSkillName({ arguments: '{"skill_name":"smart-charts"}' }),
|
||||
'smart-charts',
|
||||
)
|
||||
})
|
||||
|
||||
test('skillScriptTitleCommand drops a duplicated skill prefix', () => {
|
||||
assert.equal(
|
||||
skillScriptTitleCommand('pdf-processing', 'pdf-processing/scripts/run.py --fast'),
|
||||
'scripts/run.py --fast',
|
||||
)
|
||||
assert.equal(skillScriptTitleCommand('pdf-processing', 'ls'), 'ls')
|
||||
})
|
||||
|
||||
test('skillFileListItems skips SKILL.md and marks scripts', () => {
|
||||
const items = skillFileListItems({
|
||||
files: ['SKILL.md', 'scripts/run.py', 'docs/guide.md', ''],
|
||||
})
|
||||
assert.deepEqual(
|
||||
items.map((item) => ({ path: item.path, isScript: item.isScript })),
|
||||
[
|
||||
{ path: 'scripts/run.py', isScript: true },
|
||||
{ path: 'docs/guide.md', isScript: false },
|
||||
],
|
||||
)
|
||||
})
|
||||
|
||||
test('sandboxFileListItems uses paths relative to the artifact root', () => {
|
||||
const items = sandboxFileListItems({
|
||||
root: '/workspace/output',
|
||||
path: '/workspace/output',
|
||||
entries: [
|
||||
{ name: 'report.html', path: '/workspace/output/report.html', size: 12, modified_at: '2026-08-28T01:02:03Z' },
|
||||
{ name: 'chart.png', path: '/workspace/output/charts/chart.png', size: 2048 },
|
||||
],
|
||||
})
|
||||
assert.equal(items[0].name, 'report.html')
|
||||
assert.equal(items[1].name, 'charts/chart.png')
|
||||
assert.equal(items[0].size, 12)
|
||||
})
|
||||
|
||||
test('getSandboxToolPath reads the listed directory', () => {
|
||||
assert.equal(
|
||||
getSandboxToolPath({ tool_data: { path: '/workspace/output/charts' } }),
|
||||
'/workspace/output/charts',
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,141 @@
|
||||
/** Display helpers for skill and sandbox file tools in the agent stream. */
|
||||
|
||||
const SKILL_SCRIPT_EXT = /\.(py|sh|bash|js|ts|rb|pl|php)$/i
|
||||
const SKILL_MD = /(^|\/)SKILL\.md$/i
|
||||
|
||||
type ToolEventLike = {
|
||||
tool_data?: unknown
|
||||
arguments?: unknown
|
||||
}
|
||||
|
||||
export type SkillFileListItem = {
|
||||
name: string
|
||||
path: string
|
||||
size?: number
|
||||
modifiedAt?: string
|
||||
isScript?: boolean
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> {
|
||||
if (!value) return {}
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(value)
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
return parsed as Record<string, unknown>
|
||||
}
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
if (typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value as Record<string, unknown>
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function stringField(record: Record<string, unknown>, key: string): string {
|
||||
const value = record[key]
|
||||
return typeof value === 'string' ? value.trim() : value == null ? '' : String(value).trim()
|
||||
}
|
||||
|
||||
function eventFields(event: ToolEventLike | null | undefined): Record<string, unknown> {
|
||||
return {
|
||||
...asRecord(event?.arguments),
|
||||
...asRecord(event?.tool_data),
|
||||
}
|
||||
}
|
||||
|
||||
export function isSkillScriptPath(path: string): boolean {
|
||||
return SKILL_SCRIPT_EXT.test(path)
|
||||
}
|
||||
|
||||
export function formatToolTitleWithDetail(baseTitle: string, detail: string): string {
|
||||
const trimmed = detail.trim()
|
||||
if (!trimmed) return baseTitle
|
||||
return `${baseTitle}:「${trimmed}」`
|
||||
}
|
||||
|
||||
export function getEventSkillName(event: ToolEventLike | null | undefined): string {
|
||||
return stringField(eventFields(event), 'skill_name')
|
||||
}
|
||||
|
||||
export function getReadSkillTarget(event: ToolEventLike | null | undefined): string {
|
||||
const fields = eventFields(event)
|
||||
const skill = stringField(fields, 'skill_name')
|
||||
const file = stringField(fields, 'file_path')
|
||||
if (skill && file) return `${skill}/${file}`
|
||||
return skill || file
|
||||
}
|
||||
|
||||
export function getSandboxToolPath(event: ToolEventLike | null | undefined): string {
|
||||
return stringField(eventFields(event), 'path')
|
||||
}
|
||||
|
||||
export function skillScriptTitleCommand(skillName: string, command: string): string {
|
||||
const trimmed = command.trim()
|
||||
if (!trimmed) return ''
|
||||
if (skillName && trimmed.startsWith(`${skillName}/`)) {
|
||||
return trimmed.slice(skillName.length + 1)
|
||||
}
|
||||
return trimmed
|
||||
}
|
||||
|
||||
function basename(path: string): string {
|
||||
const trimmed = path.replace(/\/+$/, '')
|
||||
const parts = trimmed.split('/').filter(Boolean)
|
||||
return parts[parts.length - 1] || trimmed || path
|
||||
}
|
||||
|
||||
function relativeToRoot(path: string, root: string): string {
|
||||
if (!root) return path
|
||||
if (path === root) return '.'
|
||||
const prefix = root.endsWith('/') ? root : `${root}/`
|
||||
if (path.startsWith(prefix)) return path.slice(prefix.length)
|
||||
return path
|
||||
}
|
||||
|
||||
export function skillFileListItems(data: unknown): SkillFileListItem[] {
|
||||
const record = asRecord(data)
|
||||
const files = record.files
|
||||
if (!Array.isArray(files)) return []
|
||||
return files
|
||||
.map((file) => String(file || '').trim())
|
||||
.filter(Boolean)
|
||||
.filter((path) => !SKILL_MD.test(path))
|
||||
.map((path) => ({
|
||||
name: basename(path),
|
||||
path,
|
||||
isScript: isSkillScriptPath(path),
|
||||
}))
|
||||
}
|
||||
|
||||
export function sandboxFileListItems(data: unknown): SkillFileListItem[] {
|
||||
const record = asRecord(data)
|
||||
const root = stringField(record, 'root') || stringField(record, 'path')
|
||||
const entries = record.entries
|
||||
if (!Array.isArray(entries)) return []
|
||||
return entries.map((entry) => {
|
||||
const item = asRecord(entry)
|
||||
const path = stringField(item, 'path')
|
||||
const name = stringField(item, 'name') || basename(path)
|
||||
const sizeRaw = item.size
|
||||
const size = typeof sizeRaw === 'number' && Number.isFinite(sizeRaw) ? sizeRaw : undefined
|
||||
return {
|
||||
name: relativeToRoot(path, root) || name,
|
||||
path,
|
||||
size,
|
||||
modifiedAt: stringField(item, 'modified_at'),
|
||||
isScript: isSkillScriptPath(path || name),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function formatSandboxModifiedAt(value: string): string {
|
||||
const trimmed = value.trim()
|
||||
if (!trimmed) return ''
|
||||
const date = new Date(trimmed)
|
||||
if (Number.isNaN(date.getTime())) return trimmed
|
||||
const pad = (n: number) => String(n).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`
|
||||
}
|
||||
@@ -59,6 +59,10 @@ test('tool rows use line icon names instead of legacy asset masks', () => {
|
||||
assert.match(source, /wiki_search: 'agentEditor\.tools\.wikiSearch'/)
|
||||
assert.match(source, /wiki_read_page: 'agentEditor\.tools\.wikiReadPage'/)
|
||||
assert.match(source, /wiki_read_source_doc: 'agentStream\.tools\.wikiReadSourceDoc'/)
|
||||
assert.match(source, /list_sandbox_files: 'agentStream\.tools\.listSandboxFiles'/)
|
||||
assert.match(source, /read_sandbox_file: 'agentStream\.tools\.readSandboxFile'/)
|
||||
assert.match(source, /getReadSkillTarget/)
|
||||
assert.match(source, /tool_name === 'list_sandbox_files'/)
|
||||
assert.match(source, /toolName === 'get_document_content' \|\| toolName === 'wiki_read_source_doc'/)
|
||||
assert.doesNotMatch(source, /getToolIcon\(event\.tool_name\)/)
|
||||
})
|
||||
|
||||
@@ -564,6 +564,13 @@ import type { ProtectedFileAccessContext } from '@/utils/protectedFileAccess';
|
||||
import { unwrapFinalAnswerWrappers, thinkingEqualsAnswer } from '@/utils/finalAnswer';
|
||||
import { getAgentToolIconName } from '@/utils/agent-tool-icons';
|
||||
import { getQueryText, getWikiPageText } from '@/utils/agent-tool-display';
|
||||
import {
|
||||
formatToolTitleWithDetail,
|
||||
getEventSkillName,
|
||||
getReadSkillTarget,
|
||||
getSandboxToolPath,
|
||||
skillScriptTitleCommand,
|
||||
} from '@/utils/skillToolDisplay';
|
||||
import { previewShellCommand } from '@/utils/shellExecResult';
|
||||
import type { DisplayType } from '@/types/tool-results';
|
||||
import { parseWikiToolReferences } from '@/utils/wikiToolReferences';
|
||||
@@ -631,6 +638,8 @@ const TOOL_NAME_KEYS: Record<string, string> = {
|
||||
query_knowledge_graph: 'agentStream.tools.queryKnowledgeGraph',
|
||||
read_skill: 'agentStream.tools.readSkill',
|
||||
execute_skill_script: 'agentStream.tools.executeSkillScript',
|
||||
list_sandbox_files: 'agentStream.tools.listSandboxFiles',
|
||||
read_sandbox_file: 'agentStream.tools.readSandboxFile',
|
||||
shell_exec: 'agentStream.tools.shellExec',
|
||||
data_analysis: 'agentStream.tools.dataAnalysis',
|
||||
data_schema: 'agentStream.tools.dataSchema',
|
||||
@@ -1074,6 +1083,12 @@ const resolveToolDisplayType = (event: any): DisplayType | undefined => {
|
||||
if (event?.tool_name === 'shell_exec' || event?.tool_name === 'execute_skill_script') {
|
||||
return 'shell_exec'
|
||||
}
|
||||
if (event?.tool_name === 'list_sandbox_files' && event?.success !== false) {
|
||||
return 'list_sandbox_files'
|
||||
}
|
||||
if (event?.tool_name === 'read_skill' && event?.success !== false) {
|
||||
return 'read_skill'
|
||||
}
|
||||
return undefined
|
||||
};
|
||||
|
||||
@@ -2678,7 +2693,19 @@ const getToolTitle = (event: any): string => {
|
||||
if (event.tool_name === 'wiki_search' || event.tool_name === 'wiki_read_page') {
|
||||
return `${getLocalizedToolName(event.tool_name)}...`;
|
||||
}
|
||||
if (event.tool_name === 'shell_exec' || event.tool_name === 'execute_skill_script') {
|
||||
if (event.tool_name === 'read_skill') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getReadSkillTarget(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'execute_skill_script') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getEventSkillName(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'list_sandbox_files' || event.tool_name === 'read_sandbox_file') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getSandboxToolPath(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'shell_exec') {
|
||||
return t('agentStream.toolStatus.shellExecRunning');
|
||||
}
|
||||
const localizedName = getLocalizedToolName(event.tool_name);
|
||||
@@ -2776,7 +2803,22 @@ const getToolTitle = (event: any): string => {
|
||||
return pageLabel ? `${baseTitle}:「${sanitizeForDisplay(pageLabel)}」` : baseTitle;
|
||||
}
|
||||
|
||||
if (toolName === 'shell_exec' || toolName === 'execute_skill_script') {
|
||||
if (toolName === 'read_skill') {
|
||||
return formatToolTitleWithDetail(getToolDescription(event), getReadSkillTarget(event));
|
||||
}
|
||||
|
||||
if (toolName === 'list_sandbox_files' || toolName === 'read_sandbox_file') {
|
||||
return formatToolTitleWithDetail(getToolDescription(event), getSandboxToolPath(event));
|
||||
}
|
||||
|
||||
if (toolName === 'execute_skill_script') {
|
||||
const command = previewShellCommand(skillScriptCommandLabel(event))
|
||||
const baseTitle = formatToolTitleWithDetail(getToolDescription(event), getEventSkillName(event))
|
||||
const rest = skillScriptTitleCommand(getEventSkillName(event), command)
|
||||
return rest ? `${baseTitle}:${rest}` : baseTitle
|
||||
}
|
||||
|
||||
if (toolName === 'shell_exec') {
|
||||
const command = previewShellCommand(skillScriptCommandLabel(event))
|
||||
const baseTitle = getToolDescription(event)
|
||||
return command ? `${baseTitle}:${command}` : baseTitle
|
||||
@@ -2812,7 +2854,19 @@ const getToolDescription = (event: any): string => {
|
||||
if (event.tool_name === 'query_understand') {
|
||||
return t('agentStream.toolStatus.queryUnderstanding');
|
||||
}
|
||||
if (event.tool_name === 'shell_exec' || event.tool_name === 'execute_skill_script') {
|
||||
if (event.tool_name === 'read_skill') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getReadSkillTarget(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'execute_skill_script') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getEventSkillName(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'list_sandbox_files' || event.tool_name === 'read_sandbox_file') {
|
||||
const name = getLocalizedToolName(event.tool_name);
|
||||
return `${formatToolTitleWithDetail(name, getSandboxToolPath(event))}...`;
|
||||
}
|
||||
if (event.tool_name === 'shell_exec') {
|
||||
return t('agentStream.toolStatus.shellExecRunning');
|
||||
}
|
||||
const localizedName = getLocalizedToolName(event.tool_name);
|
||||
@@ -2845,7 +2899,7 @@ const getToolDescription = (event: any): string => {
|
||||
return success ? t('agentStream.toolStatus.attachmentParsingDone') : t('agentStream.toolStatus.attachmentParsingFailed');
|
||||
} else if (toolName === 'query_understand') {
|
||||
return success ? t('agentStream.toolStatus.queryUnderstandDone') : t('agentStream.toolStatus.calledFailed', { name: getLocalizedToolName(toolName) });
|
||||
} else if (toolName === 'shell_exec' || toolName === 'execute_skill_script') {
|
||||
} else if (toolName === 'shell_exec' || toolName === 'execute_skill_script' || toolName === 'read_skill' || toolName === 'list_sandbox_files' || toolName === 'read_sandbox_file') {
|
||||
const localizedName = getLocalizedToolName(toolName);
|
||||
return success ? localizedName : t('agentStream.toolStatus.calledFailed', { name: localizedName });
|
||||
} else {
|
||||
|
||||
@@ -53,6 +53,16 @@
|
||||
:arguments="toolArguments"
|
||||
/>
|
||||
|
||||
<SandboxFilesResult
|
||||
v-else-if="displayType === 'list_sandbox_files'"
|
||||
:data="toolData as ListSandboxFilesData"
|
||||
/>
|
||||
|
||||
<ReadSkillResult
|
||||
v-else-if="displayType === 'read_skill'"
|
||||
:data="toolData as ReadSkillData"
|
||||
/>
|
||||
|
||||
<!-- Fallback: Display raw output -->
|
||||
<div v-else class="fallback-output">
|
||||
<div class="fallback-header">
|
||||
@@ -83,7 +93,9 @@ import type {
|
||||
GrepResultsData,
|
||||
KnowledgeChunksListData,
|
||||
WikiEditData,
|
||||
ShellExecData
|
||||
ShellExecData,
|
||||
ListSandboxFilesData,
|
||||
ReadSkillData
|
||||
} from '@/types/tool-results';
|
||||
|
||||
import SearchResults from './tool-results/SearchResults.vue';
|
||||
@@ -101,6 +113,8 @@ import GrepResults from './tool-results/GrepResults.vue';
|
||||
import KnowledgeChunksList from './tool-results/KnowledgeChunksList.vue';
|
||||
import WikiEditResult from './tool-results/WikiEditResult.vue';
|
||||
import ShellExecResult from './tool-results/ShellExecResult.vue';
|
||||
import SandboxFilesResult from './tool-results/SandboxFilesResult.vue';
|
||||
import ReadSkillResult from './tool-results/ReadSkillResult.vue';
|
||||
|
||||
interface Props {
|
||||
displayType?: DisplayType;
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="read-skill-result">
|
||||
<p v-if="description" class="read-skill-desc">{{ description }}</p>
|
||||
|
||||
<div v-if="files.length" class="read-skill-files">
|
||||
<div class="read-skill-heading">{{ $t('agentStream.skillFiles.heading') }}</div>
|
||||
<div class="results-list">
|
||||
<ResultRow
|
||||
v-for="(item, index) in files"
|
||||
:key="item.path || `${item.name}-${index}`"
|
||||
:index="index + 1"
|
||||
:title="item.path || item.name"
|
||||
:meta="item.isScript ? $t('agentStream.skillFiles.script') : ''"
|
||||
:popup-key="item.path || index"
|
||||
:show-popup="false"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="body" class="read-skill-stream">
|
||||
<div class="read-skill-stream-label">{{ bodyLabel }}</div>
|
||||
<pre class="read-skill-stream-body">{{ body }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { skillFileListItems } from '@/utils/skillToolDisplay'
|
||||
import type { ReadSkillData } from '@/types/tool-results'
|
||||
import ResultRow from './ResultRow.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
data: ReadSkillData | Record<string, unknown>
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const record = computed(() => (props.data || {}) as Record<string, unknown>)
|
||||
|
||||
const description = computed(() => {
|
||||
const value = record.value.description
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
})
|
||||
|
||||
const files = computed(() => skillFileListItems(record.value))
|
||||
|
||||
const filePath = computed(() => {
|
||||
const value = record.value.file_path
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
})
|
||||
|
||||
const body = computed(() => {
|
||||
const content = record.value.content
|
||||
if (typeof content === 'string' && content.trim()) return content
|
||||
if (filePath.value) return ''
|
||||
const instructions = record.value.instructions
|
||||
return typeof instructions === 'string' ? instructions : ''
|
||||
})
|
||||
|
||||
const bodyLabel = computed(() =>
|
||||
filePath.value
|
||||
? filePath.value
|
||||
: t('agentStream.skillFiles.instructions'),
|
||||
)
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.read-skill-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.read-skill-desc {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.read-skill-heading {
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.read-skill-stream {
|
||||
min-width: 0;
|
||||
border: 1px solid var(--td-component-stroke);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
background: var(--td-bg-color-container);
|
||||
}
|
||||
|
||||
.read-skill-stream-label {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1.4;
|
||||
color: var(--td-text-color-secondary);
|
||||
background: var(--td-bg-color-secondarycontainer);
|
||||
border-bottom: 1px solid var(--td-component-stroke);
|
||||
}
|
||||
|
||||
.read-skill-stream-body {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
max-height: 280px;
|
||||
overflow: auto;
|
||||
font-family: var(--app-font-family-mono);
|
||||
font-size: 12px;
|
||||
line-height: 1.55;
|
||||
color: var(--td-text-color-primary);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--td-component-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="sandbox-files-result">
|
||||
<div v-if="summary" class="results-summary-text">{{ summary }}</div>
|
||||
<div v-if="items.length" class="results-list">
|
||||
<ResultRow
|
||||
v-for="(item, index) in items"
|
||||
:key="item.path || `${item.name}-${index}`"
|
||||
:index="index + 1"
|
||||
:title="item.name || item.path"
|
||||
:meta="metaFor(item)"
|
||||
:popup-key="item.path || index"
|
||||
:show-popup="false"
|
||||
/>
|
||||
</div>
|
||||
<div v-else class="empty-state">{{ $t('agentStream.sandboxFiles.empty') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { formatFileSize } from '@/utils/files'
|
||||
import {
|
||||
formatSandboxModifiedAt,
|
||||
sandboxFileListItems,
|
||||
type SkillFileListItem,
|
||||
} from '@/utils/skillToolDisplay'
|
||||
import type { ListSandboxFilesData } from '@/types/tool-results'
|
||||
import ResultRow from './ResultRow.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
data: ListSandboxFilesData | Record<string, unknown>
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const items = computed(() => sandboxFileListItems(props.data))
|
||||
|
||||
const summary = computed(() => {
|
||||
if (!items.value.length) return ''
|
||||
const record = (props.data || {}) as Record<string, unknown>
|
||||
const count = typeof record.count === 'number' ? record.count : items.value.length
|
||||
const parts = [t('agentStream.sandboxFiles.found', { count })]
|
||||
if (record.truncated) {
|
||||
parts.push(t('agentStream.sandboxFiles.truncated'))
|
||||
}
|
||||
return parts.join(' · ')
|
||||
})
|
||||
|
||||
function sizeLabel(size?: number): string {
|
||||
if (size == null || !Number.isFinite(size) || size < 0) return ''
|
||||
if (size === 0) return '0 B'
|
||||
return formatFileSize(size)
|
||||
}
|
||||
|
||||
function metaFor(item: SkillFileListItem): string {
|
||||
const parts = [sizeLabel(item.size), item.modifiedAt ? formatSandboxModifiedAt(item.modifiedAt) : '']
|
||||
return parts.filter(Boolean).join(' · ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@import './tool-results.less';
|
||||
|
||||
.sandbox-files-result {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.results-summary-text {
|
||||
font-size: var(--agent-step-summary-size, 12px);
|
||||
font-weight: 400;
|
||||
color: var(--td-text-color-secondary);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.results-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: var(--td-text-color-placeholder);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user