update ui

This commit is contained in:
lzt404 committed 2026-07-31 01:55:16 +08:00
1 parent d499edabbc
commit b66afcf2c5
8 files changed
+567 -585

No files matched your search

+1 -1
View File
@@ -12,7 +12,7 @@
: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
document.documentElement.style.backgroundColor = theme === 'dark' ? '#0b1220' : '#f4f7fb';
document.documentElement.style.backgroundColor = theme === 'dark' ? '#111412' : '#f6f7f5';
} catch {
// The app applies the theme again during initialization.
}
+6 -12
View File
@@ -442,7 +442,7 @@ export const jaOverrides = {
'agents.sessions.databaseMissing': 'セッションデータベースが見つかりません',
'agents.sessions.repairTitle': '過去のセッションを復元',
'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。',
'agents.sessions.autoRepair': 'Codex 起動前に過去のセッションを自動復元',
'agents.sessions.autoRepair': '過去のセッションを自動復元',
'agents.sessions.settingLoading': '読み込み中',
'agents.sessions.settingOn': 'オン',
'agents.sessions.settingOff': 'オフ',
@@ -517,17 +517,11 @@ export const jaOverrides = {
'agents.modify.clear': '設定を消去',
'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。',
'agents.modify.applied': 'アプリが管理するフィールドは適用済みで、それ以外のユーザー設定は保持されています。',
'agents.footer.changed': 'モデル選択が変更されました。設定変更を適用すると新しいモデルが使用されます。',
'agents.footer.applied': '設定変更は適用済みです。アプリは関連フィールドだけを管理します。',
'agents.footer.invalidManaged': '設定が無効です。変更を適用して修復するか、デフォルト設定にリセットしてください。',
'agents.footer.unsupported': '現在のシステムではこのクライアントを設定できません',
'agents.footer.installFirst': 'クライアントをインストールしてから再検出してください',
'agents.footer.noCommand': '設定ファイルは検出しましたが、クライアントコマンドが見つかりません',
'agents.footer.invalidConfig': '元の設定形式が無効なため、安全に変更できません',
'agents.launchMethods': 'Codex の起動方法',
'agents.launch.enableFirst': '先に設定変更を適用してください',
'agents.launch.applyFirst': '先に選択中のモデルを適用してください',
'agents.launch.starting': '起動中',
'agents.launch.startCli': 'CLI を起動',
'agents.launch.startApp': 'App を起動',
'agents.launch.start': '{target} を起動',
'agents.launch.unavailable': '起動できません',
'agents.launch.closeChatgpt': 'ChatGPT App を終了',
@@ -791,9 +785,9 @@ export const jaOverrides = {
'kernel.dialog.install': 'コアのインストール',
'kernel.dialog.phase': '現在の段階',
'kernel.dialog.unknownProgress': '進捗不明',
'kernel.access.openaiDescription': 'OpenAI SDK、OpenCode などの互換クライアント向け',
'kernel.access.claudeDescription': 'Claude SDK および Claude Code 向け',
'kernel.access.geminiDescription': 'Gemini ネイティブ互換クライアント向け',
'kernel.access.openaiDescription': 'OpenAI互換形式',
'kernel.access.claudeDescription': 'Anthropic互換形式',
'kernel.access.geminiDescription': 'Gemini互換形式',
'authFiles.unnamed': '無名の認証ファイル',
'management.notConfigured': '未設定',
'management.error.upstream': '上流リクエストに失敗しました',
+12 -24
View File
@@ -445,7 +445,7 @@ export const zhCN = {
'agents.sessions.databaseMissing': '未发现会话数据库',
'agents.sessions.repairTitle': '恢复历史会话',
'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话',
'agents.sessions.autoRepair': '启动 Codex 前自动恢复历史会话',
'agents.sessions.autoRepair': '自动恢复历史会话',
'agents.sessions.settingLoading': '读取中',
'agents.sessions.settingOn': '已开启',
'agents.sessions.settingOff': '已关闭',
@@ -520,17 +520,11 @@ export const zhCN = {
'agents.modify.clear': '清空配置',
'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。',
'agents.modify.applied': '软件负责的配置字段已应用,其他用户内容保持不变。',
'agents.footer.changed': '模型选择已变化;应用配置修改后将使用新模型。',
'agents.footer.applied': '配置修改已应用;软件只管理相关字段。',
'agents.footer.invalidManaged': '配置当前无效,请应用修改进行修复,或重置为默认配置。',
'agents.footer.unsupported': '当前系统无法配置该客户端',
'agents.footer.installFirst': '请先安装客户端并重新检测',
'agents.footer.noCommand': '检测到配置文件,但没有找到客户端命令',
'agents.footer.invalidConfig': '原配置格式异常,无法安全修改',
'agents.launchMethods': 'Codex 启动方式',
'agents.launch.enableFirst': '请先应用配置修改',
'agents.launch.applyFirst': '请先应用当前模型选择',
'agents.launch.starting': '启动中',
'agents.launch.startCli': '启动 CLI',
'agents.launch.startApp': '启动 App',
'agents.launch.start': '启动 {target}',
'agents.launch.unavailable': '无法启动',
'agents.launch.closeChatgpt': '关闭 ChatGPT App',
@@ -794,9 +788,9 @@ export const zhCN = {
'kernel.dialog.install': '内核安装',
'kernel.dialog.phase': '当前阶段',
'kernel.dialog.unknownProgress': '进度未知',
'kernel.access.openaiDescription': '适用于 OpenAI SDK、OpenCode 等兼容客户端',
'kernel.access.claudeDescription': '适用于 Claude SDK 与 Claude Code',
'kernel.access.geminiDescription': '适用于 Gemini 原生兼容客户端',
'kernel.access.openaiDescription': 'OpenAI兼容格式',
'kernel.access.claudeDescription': 'Anthropic兼容格式',
'kernel.access.geminiDescription': 'Gemini兼容格式',
'authFiles.unnamed': '未命名认证文件',
'management.notConfigured': '未配置',
'management.error.upstream': '上游请求失败',
@@ -1293,7 +1287,7 @@ export const en: Record<MessageKey, string> = {
'agents.sessions.databaseMissing': 'No session database found',
'agents.sessions.repairTitle': 'Restore Historical Sessions',
'agents.sessions.repairDescription': 'Sync sessions from other providers to the current session provider.',
'agents.sessions.autoRepair': 'Restore historical sessions before launching Codex',
'agents.sessions.autoRepair': 'Automatically restore historical sessions',
'agents.sessions.settingLoading': 'Loading',
'agents.sessions.settingOn': 'On',
'agents.sessions.settingOff': 'Off',
@@ -1368,17 +1362,11 @@ export const en: Record<MessageKey, string> = {
'agents.modify.clear': 'Clear Configuration',
'agents.modify.invalid': 'The current configuration cannot be parsed. Apply Configuration Changes rebuilds a minimal valid configuration; Default Configuration resets the whole file.',
'agents.modify.applied': 'The fields managed by the app are applied; all other user content remains unchanged.',
'agents.footer.changed': 'The model selection changed. Apply the configuration to use the new model.',
'agents.footer.applied': 'Configuration changes are applied; only the relevant fields are managed by the app.',
'agents.footer.invalidManaged': 'The configuration is invalid. Apply changes to repair it or reset it to the default configuration.',
'agents.footer.unsupported': 'This client cannot be configured on the current system',
'agents.footer.installFirst': 'Install the client, then detect again',
'agents.footer.noCommand': 'A configuration file was found, but no client command is available',
'agents.footer.invalidConfig': 'The original configuration is invalid and cannot be modified safely',
'agents.launchMethods': 'Codex launch methods',
'agents.launch.enableFirst': 'Apply the configuration changes first',
'agents.launch.applyFirst': 'Apply the selected model first',
'agents.launch.starting': 'Starting',
'agents.launch.startCli': 'Start CLI',
'agents.launch.startApp': 'Start App',
'agents.launch.start': 'Start {target}',
'agents.launch.unavailable': 'Cannot Start',
'agents.launch.closeChatgpt': 'Close ChatGPT App',
@@ -1642,9 +1630,9 @@ export const en: Record<MessageKey, string> = {
'kernel.dialog.install': 'Core Installation',
'kernel.dialog.phase': 'Current Phase',
'kernel.dialog.unknownProgress': 'Progress Unknown',
'kernel.access.openaiDescription': 'For OpenAI SDK, OpenCode, and other compatible clients',
'kernel.access.claudeDescription': 'For Claude SDK and Claude Code',
'kernel.access.geminiDescription': 'For native Gemini-compatible clients',
'kernel.access.openaiDescription': 'OpenAI-compatible format',
'kernel.access.claudeDescription': 'Anthropic-compatible format',
'kernel.access.geminiDescription': 'Gemini-compatible format',
'authFiles.unnamed': 'Unnamed credential file',
'management.notConfigured': 'Not configured',
'management.error.upstream': 'Upstream request failed',
+2
View File
@@ -1,4 +1,6 @@
const phraseReplacements: ReadonlyArray<readonly [string, string]> = [
['兼容格式', '相容格式'],
['历史', '歷史'],
['高级设置', '進階設定'],
['繁体中文', '繁體中文'],
['简体中文', '簡體中文'],
+73 -100
View File
@@ -12,6 +12,7 @@ import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import {
AlertTriangle,
AppWindow,
BadgeCheck,
Bot,
Check,
@@ -22,6 +23,7 @@ import {
RefreshCw,
Search,
Sparkles,
Terminal,
Trash2,
X,
} from 'lucide-react';
@@ -481,10 +483,11 @@ export function AgentsPage() {
const [modelByClient, setModelByClient] = useState<Partial<Record<AgentClientId, string>>>(
readAgentModelSelections,
);
const [launchTargetByClient, setLaunchTargetByClient] = useState<Partial<Record<AgentClientId, string>>>({});
const [loading, setLoading] = useState(true);
const [modelLoading, setModelLoading] = useState(false);
const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | 'close-app' | null>(null);
const [busyAction, setBusyAction] = useState<
'apply' | 'default' | 'clear' | 'launch' | 'launch-cli' | 'launch-app' | 'close-app' | null
>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
const [modelError, setModelError] = useState('');
@@ -598,20 +601,6 @@ export function AgentsPage() {
setCloseAppConfirmOpen(false);
}, [selected]);
useEffect(() => {
setLaunchTargetByClient((current) => agentDefinitions.reduce<Partial<Record<AgentClientId, string>>>(
(next, definition) => {
const targets = statuses.find((status) => status.id === definition.id)?.launchTargets ?? [];
const previous = current[definition.id] ?? '';
next[definition.id] = targets.some((target) => target.id === previous)
? previous
: targets[0]?.id ?? '';
return next;
},
{},
));
}, [statuses]);
const activeDefinition = agentDefinitions.find((agent) => agent.id === selected)
?? agentDefinitions[0];
const activeStatus = statuses.find((status) => status.id === selected) ?? null;
@@ -622,10 +611,9 @@ export function AgentsPage() {
const selectedModelOption = findAgentModel(models, savedSelectedModel);
const selectedModel = selectedModelOption?.name ?? '';
const activeLaunchTargets = activeStatus?.launchTargets ?? [];
const selectedLaunchTargetId = launchTargetByClient[selected] ?? activeLaunchTargets[0]?.id ?? '';
const selectedLaunchTarget = activeLaunchTargets.find(
(target) => target.id === selectedLaunchTargetId,
) ?? activeLaunchTargets[0] ?? null;
const defaultLaunchTarget = activeLaunchTargets[0] ?? null;
const cliLaunchTarget = activeLaunchTargets.find((target) => target.id === 'cli') ?? null;
const appLaunchTarget = activeLaunchTargets.find((target) => target.id === 'app') ?? null;
const appliedModel = activeStatus?.appliedModel ?? activeStatus?.currentModel ?? '';
const modelDraftChanged = Boolean(
selectedModel.trim()
@@ -641,13 +629,13 @@ export function AgentsPage() {
&& !modelLoading
&& selectedModelOption,
);
const canLaunch = Boolean(
const launchEnabled = Boolean(
activeStatus?.supportedPlatform
&& activeStatus.installed
&& selectedLaunchTarget
&& (selected === 'codex'
|| (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')),
);
const canLaunchTarget = (target: AgentLaunchTarget | null) => launchEnabled && Boolean(target);
const modelHint = modelSelectionError
|| modelError
|| (modelLoading
@@ -662,20 +650,6 @@ export function AgentsPage() {
: activeStatus?.modificationState === 'applied'
? t('agents.modify.applied')
: '';
const footerMessage = activeStatus?.modificationState === 'applied'
? draftChanged
? t('agents.footer.changed')
: t('agents.footer.applied')
: activeStatus?.modificationState === 'invalid'
? t('agents.footer.invalidManaged')
: !activeStatus?.supportedPlatform
? t('agents.footer.unsupported')
: !activeStatus.installed
? t('agents.footer.installFirst')
: activeStatus.launchTargets.length === 0
? t('agents.footer.noCommand')
: '';
const refreshModels = () => {
void loadModels(selected);
};
@@ -776,14 +750,18 @@ export function AgentsPage() {
}
};
const launchAgent = async () => {
setBusyAction('launch');
const launchAgent = async (target: AgentLaunchTarget | null) => {
if (!target) return;
const launchAction = selected === 'codex'
? target.id === 'cli' ? 'launch-cli' : 'launch-app'
: 'launch';
setBusyAction(launchAction);
setLaunchError('');
try {
if (selected !== 'codex' && draftChanged) {
throw new Error(t('agents.error.applyFirst'));
}
await invoke('launch_agent', { client: selected, target: selectedLaunchTarget?.id });
await invoke('launch_agent', { client: selected, target: target.id });
} catch (requestError) {
setLaunchError(String(requestError));
} finally {
@@ -975,23 +953,26 @@ export function AgentsPage() {
</div>
<div className="agent-modification-control">
{selected === 'codex' ? (
<label
className="agent-oauth-configuration"
title={t('agents.modify.oauthConfiguration')}
>
<span>{t('agents.modify.oauthConfiguration')}</span>
<span className="switch-control">
<input
type="checkbox"
role="switch"
checked={oauthConfiguration}
onChange={(event) => setOauthConfigurationDraft(event.currentTarget.checked)}
disabled={busy}
aria-label={t('agents.modify.oauthConfiguration')}
/>
<span className="switch-track" />
</span>
</label>
<div className="agent-codex-options">
<CodexSessionAutoRestoreCard />
<label
className="agent-oauth-configuration"
title={t('agents.modify.oauthConfiguration')}
>
<span>{t('agents.modify.oauthConfiguration')}</span>
<span className="switch-control">
<input
type="checkbox"
role="switch"
checked={oauthConfiguration}
onChange={(event) => setOauthConfigurationDraft(event.currentTarget.checked)}
disabled={busy}
aria-label={t('agents.modify.oauthConfiguration')}
/>
<span className="switch-track" />
</span>
</label>
</div>
) : null}
<div className={`agent-modification-buttons ${selected === 'codex' ? 'codex' : ''}`}>
<button
@@ -1040,56 +1021,48 @@ export function AgentsPage() {
</div>
</section>
{selected === 'codex' ? <CodexSessionAutoRestoreCard /> : null}
<div className="agent-config-footer">
{footerMessage ? (
<div className="agent-config-summary">
{activeStatus?.modificationState === 'applied' ? <Check size={16} /> : <Sparkles size={16} />}
<span>{footerMessage}</span>
</div>
) : null}
<div className="agent-launch-control">
<div className="agent-launch-actions">
{activeLaunchTargets.length > 1 ? (
<div className="agent-launch-targets" aria-label={t('agents.launchMethods')}>
{activeLaunchTargets.map((target) => (
<button
type="button"
className={target.id === selectedLaunchTarget?.id ? 'active' : ''}
key={target.id}
onClick={() => setLaunchTargetByClient((current) => ({
...current,
[selected]: target.id,
}))}
disabled={busy}
title={target.detail}
>
{target.label.replace('Codex ', '')}
</button>
))}
</div>
) : null}
<button
type="button"
className="primary-button"
onClick={() => void launchAgent()}
disabled={
busy
|| !canLaunch
|| (selected !== 'codex' && draftChanged)
}
title={selected === 'codex'
? selectedLaunchTarget?.detail
: draftChanged
{selected === 'codex' ? (
<>
<button
type="button"
className="secondary-button agent-launch-button"
onClick={() => void launchAgent(cliLaunchTarget)}
disabled={busy || !canLaunchTarget(cliLaunchTarget)}
title={cliLaunchTarget?.detail ?? t('agents.launch.unavailable')}
>
{busyAction === 'launch-cli' ? <LoaderCircle size={16} className="spin" /> : <Terminal size={16} />}
{busyAction === 'launch-cli' ? t('agents.launch.starting') : t('agents.launch.startCli')}
</button>
<button
type="button"
className="primary-button agent-launch-button"
onClick={() => void launchAgent(appLaunchTarget)}
disabled={busy || !canLaunchTarget(appLaunchTarget)}
title={appLaunchTarget?.detail ?? t('agents.launch.unavailable')}
>
{busyAction === 'launch-app' ? <LoaderCircle size={16} className="spin" /> : <AppWindow size={16} />}
{busyAction === 'launch-app' ? t('agents.launch.starting') : t('agents.launch.startApp')}
</button>
</>
) : (
<button
type="button"
className="primary-button agent-launch-button"
onClick={() => void launchAgent(defaultLaunchTarget)}
disabled={busy || !canLaunchTarget(defaultLaunchTarget) || draftChanged}
title={draftChanged
? t('agents.launch.applyFirst')
: activeStatus?.modificationState === 'applied'
? selectedLaunchTarget?.detail
? defaultLaunchTarget?.detail
: t('agents.launch.enableFirst')}
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : <Play size={16} />}
{busyAction === 'launch' ? t('agents.launch.starting') : selectedLaunchTarget ? t('agents.launch.start', { target: selectedLaunchTarget.label }) : t('agents.launch.unavailable')}
</button>
>
{busyAction === 'launch' ? <LoaderCircle size={16} className="spin" /> : <Play size={16} />}
{busyAction === 'launch' ? t('agents.launch.starting') : defaultLaunchTarget ? t('agents.launch.start', { target: defaultLaunchTarget.label }) : t('agents.launch.unavailable')}
</button>
)}
{selected === 'codex' ? (
<button
type="button"
+450 -446
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -5,8 +5,8 @@ export type AppTheme = 'light' | 'dark';
const STORAGE_KEY = 'easy-cli-proxy-api.theme';
const WINDOW_BACKGROUND: Record<AppTheme, string> = {
light: '#f4f7fb',
dark: '#0b1220',
light: '#f6f7f5',
dark: '#111412',
};
async function applyNativeTheme(theme: AppTheme): Promise<void> {
+21
View File
@@ -25,6 +25,27 @@ describe('i18n', () => {
.toBe('1.2.3 をインストールしています');
});
it('translates client API compatibility formats consistently', () => {
expect(translate('zh-CN', 'kernel.access.openaiDescription')).toBe('OpenAI兼容格式');
expect(translate('zh-TW', 'kernel.access.claudeDescription')).toBe('Anthropic相容格式');
expect(translate('ja', 'kernel.access.geminiDescription')).toBe('Gemini互換形式');
expect(translate('en', 'kernel.access.openaiDescription')).toBe('OpenAI-compatible format');
});
it('translates direct Codex launch actions', () => {
expect(translate('zh-CN', 'agents.launch.startCli')).toBe('启动 CLI');
expect(translate('zh-TW', 'agents.launch.startApp')).toBe('啟動 App');
expect(translate('ja', 'agents.launch.startCli')).toBe('CLI を起動');
expect(translate('en', 'agents.launch.startApp')).toBe('Start App');
});
it('uses the concise automatic session restoration label', () => {
expect(translate('zh-CN', 'agents.sessions.autoRepair')).toBe('自动恢复历史会话');
expect(translate('zh-TW', 'agents.sessions.autoRepair')).toBe('自動恢復歷史工作階段');
expect(translate('ja', 'agents.sessions.autoRepair')).toBe('過去のセッションを自動復元');
expect(translate('en', 'agents.sessions.autoRepair')).toBe('Automatically restore historical sessions');
});
it('uses each language native name independently of the active locale', () => {
expect(languageOptions).toEqual([
{ value: 'zh-CN', nativeLabel: '简体中文' },