feat: add CLI config preview API for Codex and Gemini

- Implemented `fetchCodexConfigPreview` and `fetchGeminiConfigPreview` functions in the API layer to retrieve masked configuration files.
- Added new interfaces `CodexConfigPreviewResponse` and `GeminiConfigPreviewResponse` to define the structure of the API responses.
- Created utility functions to read and mask sensitive values from `config.toml` and `auth.json` for Codex, and `settings.json` for Gemini.
- Updated CLI settings routes to handle new preview endpoints.
- Enhanced session content parser to support Claude JSONL format.
- Updated UI components to reflect changes in history page and navigation, including new tabs for observability.
- Localized changes for English and Chinese languages to reflect "CLI History" terminology.
This commit is contained in:
catlog22
2026-02-25 22:37:30 +08:00
parent c92754505a
commit b4d3426e6a
15 changed files with 1137 additions and 163 deletions

View File

@@ -5,7 +5,7 @@
import { useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { Check, Eye, EyeOff, X, Plus } from 'lucide-react';
import { Check, Eye, EyeOff, X, Plus, Loader2, Download } from 'lucide-react';
import {
Dialog,
DialogContent,
@@ -23,6 +23,7 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { useCreateCliSettings, useUpdateCliSettings, useProviders } from '@/hooks/useApiSettings';
import { useNotifications } from '@/hooks/useNotifications';
import { fetchCodexConfigPreview, fetchGeminiConfigPreview } from '@/lib/api';
import type { CliSettingsEndpoint, CliProvider } from '@/lib/api';
// ========== Types ==========
@@ -101,6 +102,8 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
// Gemini specific
const [geminiApiKey, setGeminiApiKey] = useState('');
const [showGeminiKey, setShowGeminiKey] = useState(false);
const [geminiSettingsJson, setGeminiSettingsJson] = useState('');
const [isLoadingGeminiConfig, setIsLoadingGeminiConfig] = useState(false);
// Shared
const [model, setModel] = useState('');
@@ -112,6 +115,9 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
const [showJsonInput, setShowJsonInput] = useState(false);
const [errors, setErrors] = useState<Record<string, string>>({});
// Codex config preview loading state
const [isLoadingCodexConfig, setIsLoadingCodexConfig] = useState(false);
// Initialize form
useEffect(() => {
if (cliSettings) {
@@ -171,6 +177,7 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
setWriteCommonConfig(false);
setGeminiApiKey('');
setShowGeminiKey(false);
setGeminiSettingsJson('');
setAvailableModels([]);
setModelInput('');
setTags([]);
@@ -224,6 +231,47 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
return Object.keys(newErrors).length === 0;
};
// Handle load Codex config preview
const handleLoadCodexConfig = async () => {
setIsLoadingCodexConfig(true);
try {
const result = await fetchCodexConfigPreview();
if (result.success) {
if (result.configToml) {
setConfigToml(result.configToml);
}
if (result.authJson) {
setAuthJson(result.authJson);
}
} else {
error(formatMessage({ id: 'apiSettings.cliSettings.loadConfigError' }) || 'Failed to load config');
}
} catch (err) {
error(formatMessage({ id: 'apiSettings.cliSettings.loadConfigError' }) || 'Failed to load config');
} finally {
setIsLoadingCodexConfig(false);
}
};
// Handle load Gemini config preview
const handleLoadGeminiConfig = async () => {
setIsLoadingGeminiConfig(true);
try {
const result = await fetchGeminiConfigPreview();
if (result.success) {
if (result.settingsJson) {
setGeminiSettingsJson(result.settingsJson);
}
} else {
error(formatMessage({ id: 'apiSettings.cliSettings.loadConfigError' }) || 'Failed to load config');
}
} catch (err) {
error(formatMessage({ id: 'apiSettings.cliSettings.loadConfigError' }) || 'Failed to load config');
} finally {
setIsLoadingGeminiConfig(false);
}
};
// Handle save
const handleSave = async () => {
if (!validateForm()) return;
@@ -521,6 +569,35 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
{/* ========== Codex Settings ========== */}
{cliProvider === 'codex' && (
<div className="space-y-4">
{/* Load Global Config Button */}
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div>
<p className="text-sm font-medium">Load Global Config</p>
<p className="text-xs text-muted-foreground">
Load config.toml and auth.json from global Codex config directory
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleLoadCodexConfig}
disabled={isLoadingCodexConfig}
>
{isLoadingCodexConfig ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Loading...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
Load Config
</>
)}
</Button>
</div>
{/* API Key */}
<div className="space-y-2">
<Label htmlFor="codex-apikey">API Key</Label>
@@ -645,6 +722,35 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
{/* ========== Gemini Settings ========== */}
{cliProvider === 'gemini' && (
<div className="space-y-4">
{/* Load Global Config Button */}
<div className="flex items-center justify-between p-3 bg-muted/30 rounded-lg">
<div>
<p className="text-sm font-medium">Load Global Config</p>
<p className="text-xs text-muted-foreground">
Load settings.json from global Gemini config directory
</p>
</div>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleLoadGeminiConfig}
disabled={isLoadingGeminiConfig}
>
{isLoadingGeminiConfig ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Loading...
</>
) : (
<>
<Download className="w-4 h-4 mr-2" />
Load Config
</>
)}
</Button>
</div>
<div className="space-y-2">
<Label htmlFor="gemini-apikey">API Key</Label>
<div className="relative">
@@ -675,6 +781,38 @@ export function CliSettingsModal({ open, onClose, cliSettings, defaultProvider }
placeholder="gemini-2.5-flash"
/>
</div>
{/* settings.json Editor */}
{geminiSettingsJson && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label htmlFor="gemini-settingsjson">settings.json (Preview)</Label>
<Button
type="button" variant="ghost" size="sm"
onClick={() => {
try {
const formatted = JSON.stringify(JSON.parse(geminiSettingsJson), null, 2);
setGeminiSettingsJson(formatted);
} catch { /* skip */ }
}}
>
Format
</Button>
</div>
<Textarea
id="gemini-settingsjson"
value={geminiSettingsJson}
onChange={(e) => setGeminiSettingsJson(e.target.value)}
placeholder='{"model": "gemini-2.5-flash", ...}'
className="font-mono text-sm"
rows={8}
readOnly
/>
<p className="text-xs text-muted-foreground">
Gemini settings.json content (read-only preview)
</p>
</div>
)}
</div>
)}

View File

@@ -4,9 +4,8 @@
// Dynamic header component for IssueHub
import { useIntl } from 'react-intl';
import { AlertCircle, Radar, ListTodo, LayoutGrid, Activity, Terminal } from 'lucide-react';
type IssueTab = 'issues' | 'board' | 'queue' | 'discovery' | 'observability' | 'executions';
import { AlertCircle, Radar, ListTodo, LayoutGrid, Play } from 'lucide-react';
import type { IssueTab } from './IssueHubTabs';
interface IssueHubHeaderProps {
currentTab: IssueTab;
@@ -37,19 +36,14 @@ export function IssueHubHeader({ currentTab }: IssueHubHeaderProps) {
title: formatMessage({ id: 'issues.discovery.pageTitle' }),
description: formatMessage({ id: 'issues.discovery.description' }),
},
observability: {
icon: <Activity className="w-6 h-6 text-primary" />,
title: formatMessage({ id: 'issues.observability.pageTitle' }),
description: formatMessage({ id: 'issues.observability.description' }),
},
executions: {
icon: <Terminal className="w-6 h-6 text-primary" />,
icon: <Play className="w-6 h-6 text-primary" />,
title: formatMessage({ id: 'issues.executions.pageTitle' }),
description: formatMessage({ id: 'issues.executions.description' }),
},
};
const config = tabConfig[currentTab];
const config = tabConfig[currentTab] || tabConfig.issues;
return (
<div className="flex items-center gap-2">

View File

@@ -8,7 +8,7 @@ import { Button } from '@/components/ui/Button';
import { cn } from '@/lib/utils';
// Keep in sync with IssueHubHeader/IssueHubPage
export type IssueTab = 'issues' | 'board' | 'queue' | 'discovery' | 'observability' | 'executions';
export type IssueTab = 'issues' | 'board' | 'queue' | 'discovery' | 'executions';
interface IssueHubTabsProps {
currentTab: IssueTab;
@@ -23,7 +23,6 @@ export function IssueHubTabs({ currentTab, onTabChange }: IssueHubTabsProps) {
{ value: 'board', label: formatMessage({ id: 'issues.hub.tabs.board' }) },
{ value: 'queue', label: formatMessage({ id: 'issues.hub.tabs.queue' }) },
{ value: 'discovery', label: formatMessage({ id: 'issues.hub.tabs.discovery' }) },
{ value: 'observability', label: formatMessage({ id: 'issues.hub.tabs.observability' }) },
{ value: 'executions', label: formatMessage({ id: 'issues.hub.tabs.executions' }) },
];

View File

@@ -17,7 +17,6 @@ import {
PanelLeftClose,
PanelLeftOpen,
LayoutDashboard,
Clock,
Zap,
GitFork,
Shield,
@@ -78,7 +77,6 @@ const navGroupDefinitions: NavGroupDef[] = [
{ path: '/sessions', labelKey: 'navigation.main.sessions', icon: FolderKanban },
{ path: '/lite-tasks', labelKey: 'navigation.main.liteTasks', icon: Zap },
{ path: '/orchestrator', labelKey: 'navigation.main.orchestrator', icon: Workflow },
{ path: '/history', labelKey: 'navigation.main.history', icon: Clock },
{ path: '/issues', labelKey: 'navigation.main.issues', icon: AlertCircle },
{ path: '/teams', labelKey: 'navigation.main.teams', icon: Users },
{ path: '/terminal-dashboard', labelKey: 'navigation.main.terminalDashboard', icon: Terminal },