Add coverage prettification and sorting functionality

- Introduced `prettify.css` for syntax highlighting in coverage reports.
- Added `prettify.js` to handle code formatting and highlighting.
- Included `sort-arrow-sprite.png` for sort indicators in the coverage table.
- Implemented `sorter.js` to enable sorting and filtering of coverage summary tables.
- Added a search box for filtering table rows based on user input.
This commit is contained in:
catlog22
2026-02-07 22:21:05 +08:00
parent 6073627ff2
commit ece02ab32a
40 changed files with 2828 additions and 728 deletions

View File

@@ -26,14 +26,21 @@
},
"cliTools": {
"title": "CLI Tools",
"description": "Configure CLI tool settings",
"description": "Configure CLI tool settings. Current default: ",
"enabled": "Enabled",
"disabled": "Disabled",
"default": "Default",
"setDefault": "Set as Default",
"primaryModel": "Primary Model",
"secondaryModel": "Secondary Model",
"expand": "Expand for details"
"expand": "Expand for details",
"envFile": "Environment File (.env)",
"envFilePlaceholder": "e.g., ~/.gemini/.env",
"envFileHint": "Path to .env file loaded before CLI execution for API keys and environment variables",
"saveToConfig": "Save to Config",
"saving": "Saving...",
"configSaved": "Configuration saved to ~/.claude/cli-tools.json",
"configSaveError": "Failed to save configuration"
},
"display": {
"title": "Display Settings",

View File

@@ -26,14 +26,21 @@
},
"cliTools": {
"title": "CLI 工具",
"description": "配置 CLI 工具设置",
"description": "配置 CLI 工具设置,当前默认工具:",
"enabled": "已启用",
"disabled": "已禁用",
"default": "默认",
"setDefault": "设为默认",
"primaryModel": "主模型",
"secondaryModel": "辅助模型",
"expand": "展开详情"
"expand": "展开详情",
"envFile": "环境变量文件 (.env)",
"envFilePlaceholder": "例如:~/.gemini/.env",
"envFileHint": "CLI 执行前加载的 .env 文件路径,用于设置 API Key 等环境变量",
"saveToConfig": "保存到配置文件",
"saving": "保存中...",
"configSaved": "配置已保存到 ~/.claude/cli-tools.json",
"configSaveError": "保存配置失败"
},
"display": {
"title": "显示设置",

View File

@@ -71,8 +71,7 @@ describe('EndpointsPage', () => {
beforeEach(() => {
vi.clearAllMocks();
// confirm() used for delete
// @ts-expect-error - test override
global.confirm = vi.fn(() => true);
vi.stubGlobal('confirm', vi.fn(() => true));
});
it('should render page title', () => {
@@ -128,4 +127,3 @@ describe('EndpointsPage', () => {
});
});
});

View File

@@ -3,7 +3,7 @@
// ========================================
// Application settings and configuration with CLI tools management
import { useState } from 'react';
import { useState, useCallback } from 'react';
import { useIntl } from 'react-intl';
import {
Settings,
@@ -28,6 +28,7 @@ import {
Calendar,
File,
ArrowUpCircle,
Save,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -53,6 +54,21 @@ import {
useUpgradeCcwInstallation,
} from '@/hooks/useSystemSettings';
// ========== Tool Config File Helpers ==========
/** Tools that use .env file for environment variables */
const ENV_FILE_TOOLS = new Set(['gemini', 'qwen', 'opencode']);
/** Tools that use --settings for Claude CLI settings file */
const SETTINGS_FILE_TOOLS = new Set(['claude']);
/** Tools that don't need any config file */
const NO_CONFIG_FILE_TOOLS = new Set(['codex']);
function getConfigFileType(toolId: string): 'envFile' | 'settingsFile' | 'none' {
if (ENV_FILE_TOOLS.has(toolId)) return 'envFile';
if (SETTINGS_FILE_TOOLS.has(toolId)) return 'settingsFile';
return 'none';
}
// ========== CLI Tool Card Component ==========
interface CliToolCardProps {
@@ -61,13 +77,16 @@ interface CliToolCardProps {
isDefault: boolean;
isExpanded: boolean;
toolAvailable?: boolean;
isSaving?: boolean;
onToggleExpand: () => void;
onToggleEnabled: () => void;
onSetDefault: () => void;
onUpdateModel: (field: 'primaryModel' | 'secondaryModel', value: string) => void;
onUpdateTags: (tags: string[]) => void;
onUpdateAvailableModels: (models: string[]) => void;
onUpdateEnvFile: (envFile: string | undefined) => void;
onUpdateSettingsFile: (settingsFile: string | undefined) => void;
onSaveToBackend: () => void;
}
function CliToolCard({
@@ -76,13 +95,16 @@ function CliToolCard({
isDefault,
isExpanded,
toolAvailable,
isSaving,
onToggleExpand,
onToggleEnabled,
onSetDefault,
onUpdateModel,
onUpdateTags,
onUpdateAvailableModels,
onUpdateEnvFile,
onUpdateSettingsFile,
onSaveToBackend,
}: CliToolCardProps) {
const { formatMessage } = useIntl();
@@ -123,6 +145,8 @@ function CliToolCard({
// Predefined tags
const predefinedTags = ['分析', 'Debug', 'implementation', 'refactoring', 'testing'];
const configFileType = getConfigFileType(toolId);
return (
<Card className={cn('overflow-hidden', !config.enabled && 'opacity-60')}>
{/* Header */}
@@ -350,26 +374,59 @@ function CliToolCard({
</p>
</div>
{/* Settings File */}
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
</label>
<Input
value={config.settingsFile || ''}
onChange={(e) => onUpdateSettingsFile(e.target.value || undefined)}
placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
</p>
</div>
{!isDefault && config.enabled && (
<Button variant="outline" size="sm" onClick={onSetDefault}>
{formatMessage({ id: 'settings.cliTools.setDefault' })}
</Button>
{/* Env File - for gemini/qwen/opencode */}
{configFileType === 'envFile' && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'settings.cliTools.envFile' })}
</label>
<Input
value={config.envFile || ''}
onChange={(e) => onUpdateEnvFile(e.target.value || undefined)}
placeholder={formatMessage({ id: 'settings.cliTools.envFilePlaceholder' })}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.cliTools.envFileHint' })}
</p>
</div>
)}
{/* Settings File - for claude only */}
{configFileType === 'settingsFile' && (
<div className="space-y-2">
<label className="text-sm font-medium text-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFile' })}
</label>
<Input
value={config.settingsFile || ''}
onChange={(e) => onUpdateSettingsFile(e.target.value || undefined)}
placeholder={formatMessage({ id: 'apiSettings.cliSettings.settingsFilePlaceholder' })}
/>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'apiSettings.cliSettings.settingsFileHint' })}
</p>
</div>
)}
{/* Action Buttons */}
<div className="flex items-center gap-2">
{!isDefault && config.enabled && (
<Button variant="outline" size="sm" onClick={onSetDefault}>
{formatMessage({ id: 'settings.cliTools.setDefault' })}
</Button>
)}
<Button
variant="default"
size="sm"
onClick={onSaveToBackend}
disabled={isSaving}
>
<Save className="w-4 h-4 mr-1" />
{isSaving
? formatMessage({ id: 'settings.cliTools.saving' })
: formatMessage({ id: 'settings.cliTools.saveToConfig' })}
</Button>
</div>
</div>
)}
</Card>
@@ -666,13 +723,16 @@ interface CliToolsWithStatusProps {
cliTools: Record<string, CliToolConfig>;
defaultCliTool: string;
expandedTools: Set<string>;
savingTools: Set<string>;
onToggleExpand: (toolId: string) => void;
onToggleEnabled: (toolId: string) => void;
onSetDefault: (toolId: string) => void;
onUpdateModel: (toolId: string, field: 'primaryModel' | 'secondaryModel', value: string) => void;
onUpdateTags: (toolId: string, tags: string[]) => void;
onUpdateAvailableModels: (toolId: string, models: string[]) => void;
onUpdateEnvFile: (toolId: string, envFile: string | undefined) => void;
onUpdateSettingsFile: (toolId: string, settingsFile: string | undefined) => void;
onSaveToBackend: (toolId: string) => void;
formatMessage: ReturnType<typeof useIntl>['formatMessage'];
}
@@ -680,13 +740,16 @@ function CliToolsWithStatus({
cliTools,
defaultCliTool,
expandedTools,
savingTools,
onToggleExpand,
onToggleEnabled,
onSetDefault,
onUpdateModel,
onUpdateTags,
onUpdateAvailableModels,
onUpdateEnvFile,
onUpdateSettingsFile,
onSaveToBackend,
formatMessage,
}: CliToolsWithStatusProps) {
const { data: toolStatus } = useCliToolStatus();
@@ -707,13 +770,16 @@ function CliToolsWithStatus({
isDefault={toolId === defaultCliTool}
isExpanded={expandedTools.has(toolId)}
toolAvailable={status?.available}
isSaving={savingTools.has(toolId)}
onToggleExpand={() => onToggleExpand(toolId)}
onToggleEnabled={() => onToggleEnabled(toolId)}
onSetDefault={() => onSetDefault(toolId)}
onUpdateModel={(field, value) => onUpdateModel(toolId, field, value)}
onUpdateTags={(tags) => onUpdateTags(toolId, tags)}
onUpdateAvailableModels={(models) => onUpdateAvailableModels(toolId, models)}
onUpdateEnvFile={(envFile) => onUpdateEnvFile(toolId, envFile)}
onUpdateSettingsFile={(settingsFile) => onUpdateSettingsFile(toolId, settingsFile)}
onSaveToBackend={() => onSaveToBackend(toolId)}
/>
);
})}
@@ -733,6 +799,7 @@ export function SettingsPage() {
const { updateCliTool, setDefaultCliTool, setUserPreferences, resetUserPreferences } = useConfigStore();
const [expandedTools, setExpandedTools] = useState<Set<string>>(new Set());
const [savingTools, setSavingTools] = useState<Set<string>>(new Set());
const toggleToolExpand = (toolId: string) => {
setExpandedTools((prev) => {
@@ -766,10 +833,68 @@ export function SettingsPage() {
updateCliTool(toolId, { availableModels });
};
const handleUpdateEnvFile = (toolId: string, envFile: string | undefined) => {
updateCliTool(toolId, { envFile });
};
const handleUpdateSettingsFile = (toolId: string, settingsFile: string | undefined) => {
updateCliTool(toolId, { settingsFile });
};
// Save tool config to backend (~/.claude/cli-tools.json)
const handleSaveToBackend = useCallback(async (toolId: string) => {
const config = cliTools[toolId];
if (!config) return;
setSavingTools((prev) => new Set(prev).add(toolId));
try {
const body: Record<string, unknown> = {
enabled: config.enabled,
primaryModel: config.primaryModel,
secondaryModel: config.secondaryModel,
tags: config.tags,
availableModels: config.availableModels,
};
// Only include the relevant config file field
const configFileType = getConfigFileType(toolId);
if (configFileType === 'envFile') {
body.envFile = config.envFile || null;
} else if (configFileType === 'settingsFile') {
body.settingsFile = config.settingsFile || null;
}
const res = await fetch(`/api/cli/config/${toolId}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
throw new Error(`HTTP ${res.status}`);
}
// Show success notification via a brief visual indicator
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 z-50 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm animate-in fade-in slide-in-from-bottom-2';
toast.textContent = formatMessage({ id: 'settings.cliTools.configSaved' });
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 3000);
} catch {
const toast = document.createElement('div');
toast.className = 'fixed bottom-4 right-4 z-50 bg-red-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm animate-in fade-in slide-in-from-bottom-2';
toast.textContent = formatMessage({ id: 'settings.cliTools.configSaveError' });
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 4000);
} finally {
setSavingTools((prev) => {
const next = new Set(prev);
next.delete(toolId);
return next;
});
}
}, [cliTools, formatMessage]);
const handlePreferenceChange = (key: keyof UserPreferences, value: unknown) => {
setUserPreferences({ [key]: value });
};
@@ -859,13 +984,16 @@ export function SettingsPage() {
cliTools={cliTools}
defaultCliTool={defaultCliTool}
expandedTools={expandedTools}
savingTools={savingTools}
onToggleExpand={toggleToolExpand}
onToggleEnabled={handleToggleToolEnabled}
onSetDefault={handleSetDefaultTool}
onUpdateModel={handleUpdateModel}
onUpdateTags={handleUpdateTags}
onUpdateAvailableModels={handleUpdateAvailableModels}
onUpdateEnvFile={handleUpdateEnvFile}
onUpdateSettingsFile={handleUpdateSettingsFile}
onSaveToBackend={handleSaveToBackend}
formatMessage={formatMessage}
/>
</Card>

View File

@@ -321,6 +321,9 @@ export interface CliToolConfig {
secondaryModel: string;
tags: string[];
type: 'builtin' | 'cli-wrapper' | 'api-endpoint';
/** Path to .env file for environment variables (gemini/qwen/opencode) */
envFile?: string;
/** Path to Claude CLI settings.json, passed via --settings (claude only) */
settingsFile?: string;
availableModels?: string[];
}