Skip to content

Commit 8f3b4fe

Browse files
committed
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.
1 parent 80e2f18 commit 8f3b4fe

15 files changed

Lines changed: 638 additions & 8 deletions

frontend/src/i18n/embed.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,23 @@ const messages = {
335335
"queryKnowledgeGraph": "知识图谱查询",
336336
"readSkill": "读取技能",
337337
"executeSkillScript": "执行技能脚本",
338+
"listSandboxFiles": "列出沙箱文件",
339+
"readSandboxFile": "读取沙箱文件",
338340
"shellExec": "执行沙箱命令",
339341
"dataAnalysis": "数据分析",
340342
"dataSchema": "数据结构",
341343
"databaseQuery": "数据库查询"
342344
},
345+
"skillFiles": {
346+
"heading": "技能文件",
347+
"script": "脚本",
348+
"instructions": "技能说明"
349+
},
350+
"sandboxFiles": {
351+
"found": "找到 {count} 个文件",
352+
"empty": "暂无文件",
353+
"truncated": "列表已截断"
354+
},
343355
"shellExec": {
344356
"workDir": "目录",
345357
"exitCode": "退出码",
@@ -844,11 +856,23 @@ const messages = {
844856
"queryKnowledgeGraph": "Knowledge Graph Query",
845857
"readSkill": "Read Skill",
846858
"executeSkillScript": "Execute Skill Script",
859+
"listSandboxFiles": "List sandbox files",
860+
"readSandboxFile": "Read sandbox file",
847861
"shellExec": "Run sandbox command",
848862
"dataAnalysis": "Data Analysis",
849863
"dataSchema": "Data Schema",
850864
"databaseQuery": "Database Query"
851865
},
866+
"skillFiles": {
867+
"heading": "Skill files",
868+
"script": "script",
869+
"instructions": "Instructions"
870+
},
871+
"sandboxFiles": {
872+
"found": "Found {count} file(s)",
873+
"empty": "No files",
874+
"truncated": "List truncated"
875+
},
852876
"shellExec": {
853877
"workDir": "Directory",
854878
"exitCode": "Exit code",

frontend/src/i18n/locales/en-US.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5330,11 +5330,23 @@ export default {
53305330
queryKnowledgeGraph: 'Knowledge Graph Query',
53315331
readSkill: 'Read Skill',
53325332
executeSkillScript: 'Execute Skill Script',
5333+
listSandboxFiles: 'List sandbox files',
5334+
readSandboxFile: 'Read sandbox file',
53335335
shellExec: 'Run sandbox command',
53345336
dataAnalysis: 'Data Analysis',
53355337
dataSchema: 'Data Schema',
53365338
databaseQuery: 'Database Query'
53375339
},
5340+
skillFiles: {
5341+
heading: 'Skill files',
5342+
script: 'script',
5343+
instructions: 'Instructions'
5344+
},
5345+
sandboxFiles: {
5346+
found: 'Found {count} file(s)',
5347+
empty: 'No files',
5348+
truncated: 'List truncated'
5349+
},
53385350
shellExec: {
53395351
workDir: 'Directory',
53405352
exitCode: 'Exit code',

frontend/src/i18n/locales/ko-KR.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,11 +1308,23 @@ export default {
13081308
queryKnowledgeGraph: '지식 그래프 조회',
13091309
readSkill: '스킬 읽기',
13101310
executeSkillScript: '스킬 스크립트 실행',
1311+
listSandboxFiles: '샌드박스 파일 목록',
1312+
readSandboxFile: '샌드박스 파일 읽기',
13111313
shellExec: '샌드박스 명령 실행',
13121314
dataAnalysis: '데이터 분석',
13131315
dataSchema: '데이터 구조',
13141316
databaseQuery: '데이터베이스 조회'
13151317
},
1318+
skillFiles: {
1319+
heading: '스킬 파일',
1320+
script: '스크립트',
1321+
instructions: '스킬 안내'
1322+
},
1323+
sandboxFiles: {
1324+
found: '파일 {count}개 발견',
1325+
empty: '파일 없음',
1326+
truncated: '목록이 잘림'
1327+
},
13161328
shellExec: {
13171329
workDir: '디렉터리',
13181330
exitCode: '종료 코드',

frontend/src/i18n/locales/ru-RU.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1308,11 +1308,23 @@ export default {
13081308
queryKnowledgeGraph: 'Запрос графа знаний',
13091309
readSkill: 'Чтение навыка',
13101310
executeSkillScript: 'Выполнение скрипта навыка',
1311+
listSandboxFiles: 'Список файлов песочницы',
1312+
readSandboxFile: 'Чтение файла песочницы',
13111313
shellExec: 'Выполнение команды в песочнице',
13121314
dataAnalysis: 'Анализ данных',
13131315
dataSchema: 'Структура данных',
13141316
databaseQuery: 'Запрос к базе данных'
13151317
},
1318+
skillFiles: {
1319+
heading: 'Файлы навыка',
1320+
script: 'скрипт',
1321+
instructions: 'Инструкции навыка'
1322+
},
1323+
sandboxFiles: {
1324+
found: 'Найдено файлов: {count}',
1325+
empty: 'Нет файлов',
1326+
truncated: 'Список обрезан'
1327+
},
13161328
shellExec: {
13171329
workDir: 'Каталог',
13181330
exitCode: 'Код выхода',

frontend/src/i18n/locales/zh-CN.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,11 +1310,23 @@ export default {
13101310
queryKnowledgeGraph: '知识图谱查询',
13111311
readSkill: '读取技能',
13121312
executeSkillScript: '执行技能脚本',
1313+
listSandboxFiles: '列出沙箱文件',
1314+
readSandboxFile: '读取沙箱文件',
13131315
shellExec: '执行沙箱命令',
13141316
dataAnalysis: '数据分析',
13151317
dataSchema: '数据结构',
13161318
databaseQuery: '数据库查询'
13171319
},
1320+
skillFiles: {
1321+
heading: '技能文件',
1322+
script: '脚本',
1323+
instructions: '技能说明'
1324+
},
1325+
sandboxFiles: {
1326+
found: '找到 {count} 个文件',
1327+
empty: '暂无文件',
1328+
truncated: '列表已截断'
1329+
},
13181330
shellExec: {
13191331
workDir: '目录',
13201332
exitCode: '退出码',

frontend/src/types/tool-results.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ export type DisplayType =
2626
| 'wiki_replace_text'
2727
| 'wiki_rename_page'
2828
| 'wiki_delete_page'
29-
| 'shell_exec';
29+
| 'shell_exec'
30+
| 'list_sandbox_files'
31+
| 'read_skill';
3032

3133
// Search result item
3234
export interface SearchResultItem {
@@ -338,6 +340,34 @@ export interface ShellExecData {
338340
stderr_truncated?: boolean;
339341
}
340342

343+
export interface SandboxFileEntry {
344+
name?: string;
345+
path: string;
346+
size?: number;
347+
modified_at?: string;
348+
}
349+
350+
export interface ListSandboxFilesData {
351+
display_type?: 'list_sandbox_files';
352+
session_id?: string;
353+
path?: string;
354+
root?: string;
355+
entries?: SandboxFileEntry[];
356+
count?: number;
357+
truncated?: boolean;
358+
}
359+
360+
export interface ReadSkillData {
361+
display_type?: 'read_skill';
362+
skill_name?: string;
363+
file_path?: string;
364+
description?: string;
365+
instructions?: string;
366+
content?: string;
367+
files?: string[];
368+
skill_dir?: string;
369+
}
370+
341371
// Union type for all wiki edit data
342372
export type WikiEditData = WikiWritePageData | WikiReplaceTextData | WikiRenamePageData | WikiDeletePageData;
343373

@@ -360,7 +390,9 @@ export type ToolResultData =
360390
| WikiReplaceTextData
361391
| WikiRenamePageData
362392
| WikiDeletePageData
363-
| ShellExecData;
393+
| ShellExecData
394+
| ListSandboxFilesData
395+
| ReadSkillData;
364396

365397
// Action data (from index.vue)
366398
export interface ActionData {

frontend/src/utils/agent-tool-display.test.mjs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ test('getAgentToolIconName maps sandbox shell tools to the terminal icon', () =>
3030
assert.equal(getAgentToolIconName('shell_exec'), 'terminal')
3131
})
3232

33+
test('getAgentToolIconName maps skill and sandbox file tools', () => {
34+
assert.equal(getAgentToolIconName('read_skill'), 'file')
35+
assert.equal(getAgentToolIconName('list_sandbox_files'), 'folder')
36+
assert.equal(getAgentToolIconName('read_sandbox_file'), 'file')
37+
assert.equal(getAgentToolIconName('execute_skill_script'), 'code')
38+
})
39+
3340
test('getAgentToolIconName maps Wiki tools to semantic search and reading icons', () => {
3441
assert.equal(getAgentToolIconName('wiki_search'), 'search')
3542
assert.equal(getAgentToolIconName('wiki_read_page'), 'file-search')

frontend/src/utils/agent-tool-icons.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,15 @@ export function getAgentToolIconName(
3939
if (toolName.startsWith('mcp_')) {
4040
return 'terminal'
4141
}
42-
if (toolName === 'shell_exec' || toolName === 'list_sandbox_files' || toolName === 'read_sandbox_file') {
42+
if (toolName === 'shell_exec') {
4343
return 'terminal'
4444
}
45+
if (toolName === 'list_sandbox_files') {
46+
return 'folder'
47+
}
48+
if (toolName === 'read_sandbox_file' || toolName === 'read_skill') {
49+
return 'file'
50+
}
4551
if (toolName === 'execute_skill_script') {
4652
return 'code'
4753
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import assert from 'node:assert/strict'
2+
import test from 'node:test'
3+
import {
4+
formatToolTitleWithDetail,
5+
getEventSkillName,
6+
getReadSkillTarget,
7+
getSandboxToolPath,
8+
sandboxFileListItems,
9+
skillFileListItems,
10+
skillScriptTitleCommand,
11+
} from './skillToolDisplay.ts'
12+
13+
test('formatToolTitleWithDetail quotes a skill or path detail', () => {
14+
assert.equal(formatToolTitleWithDetail('读取技能', 'pdf-processing'), '读取技能:「pdf-processing」')
15+
assert.equal(formatToolTitleWithDetail('读取技能', ' '), '读取技能')
16+
})
17+
18+
test('getReadSkillTarget prefers skill/file from tool_data over arguments', () => {
19+
assert.equal(
20+
getReadSkillTarget({
21+
arguments: { skill_name: 'old', file_path: 'SKILL.md' },
22+
tool_data: { skill_name: 'pdf-processing', file_path: 'scripts/run.py' },
23+
}),
24+
'pdf-processing/scripts/run.py',
25+
)
26+
assert.equal(
27+
getReadSkillTarget({ arguments: { skill_name: 'brandkit' } }),
28+
'brandkit',
29+
)
30+
})
31+
32+
test('getEventSkillName parses JSON-encoded arguments', () => {
33+
assert.equal(
34+
getEventSkillName({ arguments: '{"skill_name":"smart-charts"}' }),
35+
'smart-charts',
36+
)
37+
})
38+
39+
test('skillScriptTitleCommand drops a duplicated skill prefix', () => {
40+
assert.equal(
41+
skillScriptTitleCommand('pdf-processing', 'pdf-processing/scripts/run.py --fast'),
42+
'scripts/run.py --fast',
43+
)
44+
assert.equal(skillScriptTitleCommand('pdf-processing', 'ls'), 'ls')
45+
})
46+
47+
test('skillFileListItems skips SKILL.md and marks scripts', () => {
48+
const items = skillFileListItems({
49+
files: ['SKILL.md', 'scripts/run.py', 'docs/guide.md', ''],
50+
})
51+
assert.deepEqual(
52+
items.map((item) => ({ path: item.path, isScript: item.isScript })),
53+
[
54+
{ path: 'scripts/run.py', isScript: true },
55+
{ path: 'docs/guide.md', isScript: false },
56+
],
57+
)
58+
})
59+
60+
test('sandboxFileListItems uses paths relative to the artifact root', () => {
61+
const items = sandboxFileListItems({
62+
root: '/workspace/output',
63+
path: '/workspace/output',
64+
entries: [
65+
{ name: 'report.html', path: '/workspace/output/report.html', size: 12, modified_at: '2026-08-28T01:02:03Z' },
66+
{ name: 'chart.png', path: '/workspace/output/charts/chart.png', size: 2048 },
67+
],
68+
})
69+
assert.equal(items[0].name, 'report.html')
70+
assert.equal(items[1].name, 'charts/chart.png')
71+
assert.equal(items[0].size, 12)
72+
})
73+
74+
test('getSandboxToolPath reads the listed directory', () => {
75+
assert.equal(
76+
getSandboxToolPath({ tool_data: { path: '/workspace/output/charts' } }),
77+
'/workspace/output/charts',
78+
)
79+
})

0 commit comments

Comments
 (0)