feat: add codex skills support and enhance system status UI

- Introduced new query keys for codex skills and their list.
- Updated English and Chinese locale files for system status messages.
- Enhanced the SettingsPage to display installation details and upgrade options.
- Integrated CLI mode toggle in SkillsManagerPage for better skill management.
- Modified skills routes to handle CLI type for skill operations and configurations.
This commit is contained in:
catlog22
2026-02-07 21:45:12 +08:00
parent 2094c1085b
commit 678be8d41f
14 changed files with 519 additions and 168 deletions

View File

@@ -22,6 +22,12 @@ import {
Monitor,
Terminal,
AlertTriangle,
Package,
Home,
Folder,
Calendar,
File,
ArrowUpCircle,
} from 'lucide-react';
import { Card } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
@@ -43,6 +49,8 @@ import {
useRefreshCodexCliEnhancement,
useCcwInstallStatus,
useCliToolStatus,
useCcwInstallations,
useUpgradeCcwInstallation,
} from '@/hooks/useSystemSettings';
// ========== CLI Tool Card Component ==========
@@ -517,51 +525,137 @@ function ResponseLanguageSection() {
function SystemStatusSection() {
const { formatMessage } = useIntl();
const { data: ccwInstall, isLoading: installLoading } = useCcwInstallStatus();
// Don't show if installed or still loading
if (installLoading || ccwInstall?.installed) return null;
const { installations, isLoading, refetch } = useCcwInstallations();
const { upgrade, isPending: upgrading } = useUpgradeCcwInstallation();
const { data: ccwInstall } = useCcwInstallStatus();
return (
<Card className="p-6 border-yellow-500/50">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5 text-yellow-500" />
{formatMessage({ id: 'settings.systemStatus.title' })}
</h2>
{/* CCW Install Status */}
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium">{formatMessage({ id: 'settings.systemStatus.ccwInstall' })}</span>
<Badge variant="outline" className="text-yellow-500 border-yellow-500/50">
{formatMessage({ id: 'settings.systemStatus.incomplete' })}
</Badge>
<Card className="p-6">
{/* Header */}
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2">
<Package className="w-5 h-5" />
{formatMessage({ id: 'settings.systemStatus.title' })}
{!isLoading && (
<span className="text-sm font-normal text-muted-foreground">
{installations.length} {formatMessage({ id: 'settings.systemStatus.installations' })}
</span>
)}
</h2>
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
onClick={() => refetch()}
title={formatMessage({ id: 'settings.systemStatus.refresh' })}
>
<RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
</Button>
</div>
{ccwInstall && ccwInstall.missingFiles.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'settings.systemStatus.missingFiles' })}:
</p>
<ul className="text-xs text-muted-foreground/70 list-disc list-inside">
{ccwInstall.missingFiles.slice(0, 5).map((file) => (
<li key={file}>{file}</li>
))}
{ccwInstall.missingFiles.length > 5 && (
<li>+{ccwInstall.missingFiles.length - 5} more...</li>
)}
</ul>
<div className="bg-muted/50 rounded-md p-3 mt-2">
<p className="text-xs font-medium mb-1">
{formatMessage({ id: 'settings.systemStatus.runToFix' })}:
</p>
<code className="text-xs bg-background px-2 py-1 rounded block font-mono">
ccw install
</code>
</div>
</div>
)}
</div>
{/* Installation cards */}
{isLoading ? (
<div className="text-sm text-muted-foreground py-4 text-center">
{formatMessage({ id: 'settings.systemStatus.checking' })}
</div>
) : installations.length === 0 ? (
<div className="text-center py-6 space-y-2">
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'settings.systemStatus.noInstallations' })}
</p>
<div className="bg-muted/50 rounded-md p-3 inline-block">
<code className="text-xs font-mono">ccw install</code>
</div>
</div>
) : (
<div className="space-y-3">
{installations.map((inst) => {
const isGlobal = inst.installation_mode === 'Global';
const installDate = new Date(inst.installation_date).toLocaleDateString();
const version = inst.application_version !== 'unknown' ? inst.application_version : inst.installer_version;
return (
<div
key={inst.manifest_id}
className="rounded-lg border border-border p-4 space-y-2"
>
{/* Mode + Version + Upgrade */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className={cn(
'inline-flex items-center justify-center w-8 h-8 rounded-lg',
isGlobal ? 'bg-primary/10 text-primary' : 'bg-orange-500/10 text-orange-500'
)}>
{isGlobal ? <Home className="w-4 h-4" /> : <Folder className="w-4 h-4" />}
</span>
<span className="text-sm font-medium">
{isGlobal
? formatMessage({ id: 'settings.systemStatus.global' })
: formatMessage({ id: 'settings.systemStatus.path' })}
</span>
<Badge variant="secondary" className="text-xs font-mono">
v{version}
</Badge>
</div>
<Button
variant="ghost"
size="sm"
className="h-7"
disabled={upgrading}
onClick={() => upgrade(inst.installation_path)}
>
<ArrowUpCircle className={cn('w-3.5 h-3.5 mr-1', upgrading && 'animate-spin')} />
{upgrading
? formatMessage({ id: 'settings.systemStatus.upgrading' })
: formatMessage({ id: 'settings.systemStatus.upgrade' })}
</Button>
</div>
{/* Path */}
<div className="text-xs text-muted-foreground bg-muted/50 rounded px-2 py-1 font-mono truncate" title={inst.installation_path}>
{inst.installation_path}
</div>
{/* Date + Files */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="inline-flex items-center gap-1">
<Calendar className="w-3 h-3" />
{installDate}
</span>
<span className="inline-flex items-center gap-1">
<File className="w-3 h-3" />
{inst.files_count} {formatMessage({ id: 'settings.systemStatus.files' })}
</span>
</div>
</div>
);
})}
{/* Missing files warning */}
{ccwInstall && !ccwInstall.installed && ccwInstall.missingFiles.length > 0 && (
<div className="rounded-lg border border-yellow-500/50 bg-yellow-500/5 p-4 space-y-2">
<div className="flex items-center gap-2 text-sm font-medium text-yellow-600 dark:text-yellow-500">
<AlertTriangle className="w-4 h-4" />
{formatMessage({ id: 'settings.systemStatus.incomplete' })} &mdash; {ccwInstall.missingFiles.length} {formatMessage({ id: 'settings.systemStatus.missingFiles' }).toLowerCase()}
</div>
<ul className="text-xs text-muted-foreground list-disc list-inside">
{ccwInstall.missingFiles.slice(0, 4).map((f) => (
<li key={f}>{f}</li>
))}
{ccwInstall.missingFiles.length > 4 && (
<li>+{ccwInstall.missingFiles.length - 4} more...</li>
)}
</ul>
<div className="bg-muted/50 rounded-md p-2">
<p className="text-xs font-medium mb-1">{formatMessage({ id: 'settings.systemStatus.runToFix' })}:</p>
<code className="text-xs font-mono bg-background px-2 py-1 rounded block">ccw install</code>
</div>
</div>
)}
</div>
)}
</Card>
);
}
@@ -865,31 +959,6 @@ export function SettingsPage() {
</div>
</Card>
{/* Display Settings */}
<div className="py-4">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">
<Settings className="w-5 h-5" />
{formatMessage({ id: 'settings.sections.display' })}
</h2>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-foreground">{formatMessage({ id: 'settings.display.showCompletedTasks' })}</p>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'settings.display.showCompletedTasksDesc' })}
</p>
</div>
<Button
variant={userPreferences.showCompletedTasks ? 'default' : 'outline'}
size="sm"
onClick={() => handlePreferenceChange('showCompletedTasks', !userPreferences.showCompletedTasks)}
>
{userPreferences.showCompletedTasks ? formatMessage({ id: 'settings.display.show' }) : formatMessage({ id: 'settings.display.hide' })}
</Button>
</div>
</div>
</div>
{/* Reset Settings */}
<Card className="p-6 border-destructive/50">
<h2 className="text-lg font-semibold text-foreground flex items-center gap-2 mb-4">

View File

@@ -39,6 +39,7 @@ import {
AlertDialogCancel,
} from '@/components/ui';
import { SkillCard, SkillDetailPanel, SkillCreateDialog } from '@/components/shared';
import { CliModeToggle, type CliMode } from '@/components/mcp/CliModeToggle';
import { useSkills, useSkillMutations } from '@/hooks';
import { fetchSkillDetail } from '@/lib/api';
import { useWorkflowStore, selectProjectPath } from '@/stores/workflowStore';
@@ -109,6 +110,7 @@ export function SkillsManagerPage() {
const { formatMessage } = useIntl();
const projectPath = useWorkflowStore(selectProjectPath);
const [cliMode, setCliMode] = useState<CliMode>('claude');
const [searchQuery, setSearchQuery] = useState('');
const [categoryFilter, setCategoryFilter] = useState<string>('all');
const [sourceFilter, setSourceFilter] = useState<string>('all');
@@ -143,6 +145,7 @@ export function SkillsManagerPage() {
enabledOnly: enabledFilter === 'enabled',
location: locationFilter,
},
cliType: cliMode,
});
const { toggleSkill, isToggling } = useSkillMutations();
@@ -165,18 +168,19 @@ export function SkillsManagerPage() {
const location = skill.location || 'project';
// Use folderName for API calls (actual folder name), fallback to name if not available
const skillIdentifier = skill.folderName || skill.name;
// Debug logging
console.log('[SkillToggle] Toggling skill:', {
name: skill.name,
folderName: skill.folderName,
location,
enabled,
skillIdentifier
console.log('[SkillToggle] Toggling skill:', {
name: skill.name,
folderName: skill.folderName,
location,
enabled,
skillIdentifier,
cliMode
});
try {
await toggleSkill(skillIdentifier, enabled, location);
await toggleSkill(skillIdentifier, enabled, location, cliMode);
} catch (error) {
console.error('[SkillToggle] Toggle failed:', error);
throw error;
@@ -211,7 +215,8 @@ export function SkillsManagerPage() {
const data = await fetchSkillDetail(
skill.name,
skill.location || 'project',
projectPath
projectPath,
cliMode
);
setSelectedSkill(data.skill);
} catch (error) {
@@ -220,7 +225,7 @@ export function SkillsManagerPage() {
} finally {
setIsDetailLoading(false);
}
}, [projectPath]);
}, [projectPath, cliMode]);
const handleCloseDetailPanel = useCallback(() => {
setIsDetailPanelOpen(false);
@@ -232,14 +237,23 @@ export function SkillsManagerPage() {
{/* Page Header */}
<div className="flex flex-col gap-4">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Sparkles className="w-6 h-6 text-primary" />
{formatMessage({ id: 'skills.title' })}
</h1>
<p className="text-muted-foreground mt-1">
{formatMessage({ id: 'skills.description' })}
</p>
<div className="flex items-center gap-3">
<div>
<h1 className="text-2xl font-bold text-foreground flex items-center gap-2">
<Sparkles className="w-6 h-6 text-primary" />
{formatMessage({ id: 'skills.title' })}
</h1>
<p className="text-muted-foreground mt-1">
{formatMessage({ id: 'skills.description' })}
</p>
</div>
{/* CLI Mode Badge Switcher */}
<div className="ml-3 flex-shrink-0">
<CliModeToggle
currentMode={cliMode}
onModeChange={setCliMode}
/>
</div>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={() => refetch()} disabled={isFetching}>
@@ -479,6 +493,7 @@ export function SkillsManagerPage() {
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
onCreated={() => refetch()}
cliType={cliMode}
/>
</div>
);