feat: add tool status management and localization for skill installation

- Updated chat message types to include tool statuses.
- Enhanced localization files for English, Thai, and Chinese to support new tool status messages.
- Modified HomePage and SkillsPage components to handle tool statuses in chat messages.
- Implemented tool status merging and updating logic in the chat store.
- Added handling for tool status events in the gateway event processing.
- Created tests for chat message rendering with tool statuses and skill installation shortcuts.
- Improved gateway event dispatching for tool lifecycle events.
This commit is contained in:
duanshuwen
2026-04-23 20:27:54 +08:00
parent 979fb0a0f6
commit df600272d6
29 changed files with 2041 additions and 384 deletions

View File

@@ -0,0 +1,153 @@
import React from 'react';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { setLocale } from '../src/i18n';
import ChatMessageList from '../src/components/chat/ChatMessageList';
const mocks = vi.hoisted(() => ({
apiOpenSkillPath: vi.fn(),
apiOpenSkillReadme: vi.fn(),
writeText: vi.fn(),
}));
vi.mock('../src/lib/skills-api', () => ({
apiOpenSkillPath: mocks.apiOpenSkillPath,
apiOpenSkillReadme: mocks.apiOpenSkillReadme,
}));
describe('ChatMessageList', () => {
beforeEach(() => {
setLocale('zh');
vi.clearAllMocks();
mocks.apiOpenSkillPath.mockResolvedValue(undefined);
mocks.apiOpenSkillReadme.mockResolvedValue(undefined);
mocks.writeText.mockResolvedValue(undefined);
Object.defineProperty(window.navigator, 'clipboard', {
value: {
writeText: mocks.writeText,
},
configurable: true,
});
});
it('renders standalone streaming tool status cards when there is no streaming assistant text yet', () => {
render(
<ChatMessageList
messages={[
{
id: 'user-1',
role: 'user',
name: '你',
time: '10:00',
content: '帮我安装这个 skill',
},
]}
streamingTools={[
{
id: 'tool-1',
toolCallId: 'skills.install:run-1',
name: 'skills.install',
status: 'running',
summary: 'Installing minimax-xlsx',
updatedAt: Date.now(),
},
]}
/>,
);
expect(screen.getByText('正在执行工具...')).toBeTruthy();
expect(screen.getByText('安装 Skill')).toBeTruthy();
expect(screen.getByText('运行中')).toBeTruthy();
expect(screen.getByText('Installing minimax-xlsx')).toBeTruthy();
});
it('renders streaming tool status cards above a streaming assistant message', () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-stream',
role: 'assistant',
name: 'YINIAN',
time: '10:01',
content: '已安装完成',
isStreaming: true,
},
]}
streamingTools={[
{
id: 'tool-2',
toolCallId: 'browser.open_url:run-2',
name: 'browser.open_url',
status: 'completed',
durationMs: 1200,
summary: '已为你打开 http://www.baidu.com/',
updatedAt: Date.now(),
},
]}
/>,
);
expect(screen.getByText('打开网页')).toBeTruthy();
expect(screen.getByText('已完成')).toBeTruthy();
expect(screen.getByText('1.2s')).toBeTruthy();
expect(screen.getByText('已为你打开 http://www.baidu.com/')).toBeTruthy();
expect(screen.getByText('已安装完成')).toBeTruthy();
});
it('renders persistent skill-install tool cards with follow-up actions', async () => {
render(
<ChatMessageList
messages={[
{
id: 'assistant-1',
role: 'assistant',
name: 'YINIAN',
time: '10:03',
content: '已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx',
toolStatuses: [
{
id: 'tool-3',
toolCallId: 'skills.install:run-3',
name: 'skills.install',
status: 'completed',
summary: '已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx',
durationMs: 980,
updatedAt: Date.now(),
input: {
kind: 'github-url',
url: 'https://github.com/MiniMax-AI/skills/blob/main/skills/minimax-xlsx/SKILL.md',
},
result: {
slug: 'minimax-xlsx',
source: 'github-url',
baseDir: '/tmp/minimax-xlsx',
},
},
],
},
]}
/>,
);
expect(screen.getAllByText('已安装并启用 skill minimax-xlsxgithub-url。位置/tmp/minimax-xlsx')).toHaveLength(1);
expect(screen.getByText('Skill')).toBeTruthy();
expect(screen.getByText('/tmp/minimax-xlsx')).toBeTruthy();
expect(screen.getByText('后续动作')).toBeTruthy();
fireEvent.click(screen.getByText('打开目录'));
await waitFor(() => {
expect(mocks.apiOpenSkillPath).toHaveBeenCalledWith(
'minimax-xlsx',
'minimax-xlsx',
'/tmp/minimax-xlsx',
);
});
fireEvent.click(screen.getByText('复制路径'));
await waitFor(() => {
expect(mocks.writeText).toHaveBeenCalledWith('/tmp/minimax-xlsx');
});
expect(screen.getByText('路径已复制')).toBeTruthy();
});
});