修复扩展列表初次加载空态

增加客户端扩展列表 loading、ready 和 error 状态

初次异步读取期间显示加载提示,读取成功为空时才显示空列表提示

补充运行时设置界面回归测试并同步技术方案
This commit is contained in:
2026-09-01 08:28:02 +00:00
parent c250764b59
commit fa9d4579c8
3 changed files with 79 additions and 2 deletions
@@ -100,6 +100,8 @@ type RuntimeConfigToast = {
message: string;
};
type ClientExtensionsLoadState = 'loading' | 'ready' | 'error';
const runtimeSettingsSections = [
{
id: 'general',
@@ -399,6 +401,8 @@ export function RuntimeConfigDialog({
ClientExtensionItem[]
>([]);
const [clientExtensionsBusy, setClientExtensionsBusy] = useState(false);
const [clientExtensionsLoadState, setClientExtensionsLoadState] =
useState<ClientExtensionsLoadState>('loading');
const [clientExtensionsStatus, setClientExtensionsStatus] = useState('');
const [editingExtensionId, setEditingExtensionId] = useState<string | null>(
null,
@@ -430,17 +434,21 @@ export function RuntimeConfigDialog({
async function readClientExtensions() {
const invoke = resolveTauriInvoke();
if (!invoke) {
setClientExtensionsLoadState('error');
setClientExtensionsStatus('需要在 Tauri App 内运行');
return;
}
setClientExtensionsLoadState('loading');
setClientExtensionsBusy(true);
try {
const result = await invoke<ClientExtensionItem[]>(
'list_client_extensions',
);
setClientExtensions(result);
setClientExtensionsLoadState('ready');
setClientExtensionsStatus('');
} catch (error) {
setClientExtensionsLoadState('error');
setClientExtensionsStatus(
error instanceof Error ? error.message : String(error),
);
@@ -477,6 +485,7 @@ export function RuntimeConfigDialog({
'list_client_extensions',
);
setClientExtensions(refreshed);
setClientExtensionsLoadState('ready');
const importedCount = result.imported.length;
setClientExtensionsStatus(
importedCount > 0
@@ -1478,12 +1487,25 @@ export function RuntimeConfigDialog({
className="runtime-settings-section runtime-settings-extensions"
aria-label="客户端扩展"
>
{clientExtensionsStatus ? (
{clientExtensionsStatus &&
clientExtensionsLoadState !== 'error' ? (
<p className="runtime-settings-inline-status" role="status">
{clientExtensionsStatus}
</p>
) : null}
{clientExtensions.length > 0 ? (
{clientExtensionsLoadState === 'loading' ? (
<div className="runtime-settings-empty-state" role="status">
<strong></strong>
<span> Skill MCP</span>
</div>
) : clientExtensionsLoadState === 'error' ? (
<div className="runtime-settings-empty-state" role="alert">
<strong></strong>
<span>
{clientExtensionsStatus || '暂时无法读取扩展列表。'}
</span>
</div>
) : clientExtensions.length > 0 ? (
<div className="runtime-settings-extension-list">
{clientExtensions.map((item) => {
const editing = editingExtensionId === item.id;
@@ -255,6 +255,59 @@ export function registerAgentStatusDerivationTests() {
}
export function registerRuntimeSettingsTests() {
it('distinguishes loading the client extension list from an empty list', async () => {
let resolveExtensions!: (items: unknown[]) => void;
const extensionsRead = new Promise<unknown[]>((resolve) => {
resolveExtensions = resolve;
});
const invoke = vi.fn((command: string) => {
if (command === 'read_game_creator_app_config') {
return Promise.resolve({
path: '/home/test/AppData/game-creator.config.json',
config: {
agentMode: 'codex_app_server',
llm: {
apiKey: '',
baseUrl: 'https://llm.example.test/v1',
model: 'gpt-settings-extensions',
apiKind: 'openai_responses',
reasoningEffort: 'high',
stream: false,
webSearchEnabled: false,
requestTimeoutMs: 180000,
maxRetries: 0,
retryBackoffMs: 500,
},
editorApi: {
baseUrl: 'http://127.0.0.1:8082',
apiKey: '',
},
},
});
}
if (command === 'list_client_extensions') {
return extensionsRead;
}
throw new Error(`unexpected invoke ${command}`);
});
window.__TAURI__ = { core: { invoke } };
renderLauncherAt('/?launcher');
fireEvent.click(screen.getByRole('button', { name: '配置' }));
await screen.findByRole('dialog', { name: '运行时配置' });
fireEvent.click(screen.getByRole('button', { name: /^扩展/ }));
expect(await screen.findByText('正在加载扩展')).not.toBeNull();
expect(screen.queryByText('还没有导入扩展')).toBeNull();
await act(async () => {
resolveExtensions([]);
});
expect(await screen.findByText('还没有导入扩展')).not.toBeNull();
expect(screen.queryByText('正在加载扩展')).toBeNull();
});
it('shows the client version on the About settings page', async () => {
const invoke = vi.fn(async (command: string) => {
if (command === 'read_game_creator_app_config') {
@@ -280,6 +280,8 @@ MCP · 来自 my-plugin.zip 启动失败
- 当前不可用;
- 启动失败。
扩展列表首次读取完成前显示加载态;只有读取成功且结果为空时才显示“还没有导入扩展”,读取失败时显示加载错误,不把未完成的异步读取当作空列表。
“完整列表”指所有用户导入的独立扩展项,不指 zip 内每个普通文件或每个 MCP tool。
### 7.3 导入后的默认状态