feat: redesign agent configuration UI and add Codex session tab

- add localized core configuration and session management tabs
- display action errors inline and streamline status feedback
- refine responsive layouts, dialogs, action buttons, and default window size
This commit is contained in:
lzt404 committed 2026-07-25 20:47:57 +08:00
1 parent 82758d6681
commit 50b35ae6bd
6 files changed
+528 -318

No files matched your search

+2 -2
View File
@@ -14,8 +14,8 @@
{
"label": "main",
"title": "EasyCLIProxyAPI",
"width": 1200,
"height": 810,
"width": 1359,
"height": 865,
"minWidth": 640,
"minHeight": 600
}
+5 -8
View File
@@ -360,15 +360,7 @@ export const jaOverrides = {
'agents.error.modelsLoading': 'モデル一覧を読み込み中です。しばらくしてから再試行してください',
'agents.error.noModels': '選択可能なモデルがないため、設定変更を適用できません',
'agents.error.selectionGone': '選択中のモデルは利用可能一覧にありません。更新後に選び直してください',
'agents.notice.enabled': '{name} の設定変更を適用しました',
'agents.notice.applied': '{name} の設定変更を適用しました',
'agents.notice.defaultWritten': '{name} のデフォルト設定を書き込みました',
'agents.notice.updated': '{name} の設定変更を更新しました',
'agents.error.applyFirst': '選択したモデルはまだ適用されていません。先に「設定変更を適用」をクリックしてください',
'agents.notice.appliedLaunch': '{name} にモデルを適用して起動しました',
'agents.notice.syncedLaunch': '{name} の管理対象設定を同期して起動しました',
'agents.notice.managedLaunch': '{name} を設定変更が適用された状態で起動しました',
'agents.notice.launched': '{name} を起動しました',
'agents.status.detecting': '検出中',
'agents.status.unsupported': 'プラットフォーム非対応',
'agents.status.notInstalled': '未インストール',
@@ -381,6 +373,11 @@ export const jaOverrides = {
'agents.status.unconfigured': '未設定',
'agents.title': 'エージェント設定',
'agents.redetect': '再検出',
'agents.tabs.label': 'エージェント設定ページ',
'agents.tabs.core': 'コア設定',
'agents.tabs.sessions': 'セッション管理',
'agents.sessions.title': 'Codex セッション管理',
'agents.sessions.description': 'セッションの一覧と管理機能はここに表示されます。',
'agents.localClients': 'ローカルクライアント',
'agents.selectClient': '管理するエージェントを選択',
'agents.installStatus': 'インストール状態',
+10 -16
View File
@@ -363,15 +363,7 @@ export const zhCN = {
'agents.error.modelsLoading': '模型列表仍在加载,请稍后再试',
'agents.error.noModels': '当前没有可选模型,无法应用配置修改',
'agents.error.selectionGone': '当前选择已不在可用模型列表中,请刷新模型后重新选择',
'agents.notice.enabled': '{name} 的配置修改已应用',
'agents.notice.applied': '{name} 的配置修改已应用',
'agents.notice.defaultWritten': '{name} 的默认配置已写入',
'agents.notice.updated': '{name} 的配置修改已更新',
'agents.error.applyFirst': '模型选择尚未应用,请先点击“应用配置修改”',
'agents.notice.appliedLaunch': '{name} 已应用模型并启动',
'agents.notice.syncedLaunch': '{name} 已同步受管配置并启动',
'agents.notice.managedLaunch': '{name} 已启动,配置修改仍然生效',
'agents.notice.launched': '{name} 已启动',
'agents.status.detecting': '检测中',
'agents.status.unsupported': '平台不支持',
'agents.status.notInstalled': '未安装',
@@ -384,6 +376,11 @@ export const zhCN = {
'agents.status.unconfigured': '尚未配置',
'agents.title': '智能体配置',
'agents.redetect': '重新检测',
'agents.tabs.label': '智能体配置子页面',
'agents.tabs.core': '核心配置',
'agents.tabs.sessions': '会话管理',
'agents.sessions.title': 'Codex 会话管理',
'agents.sessions.description': '会话列表与管理功能将在这里显示。',
'agents.localClients': '本机客户端',
'agents.selectClient': '选择需要管理的智能体',
'agents.installStatus': '安装状态',
@@ -1078,15 +1075,7 @@ export const en: Record<MessageKey, string> = {
'agents.error.modelsLoading': 'The model list is still loading. Try again shortly.',
'agents.error.noModels': 'No models are available, so configuration changes cannot be applied',
'agents.error.selectionGone': 'The selected model is no longer available. Refresh and select it again.',
'agents.notice.enabled': 'Configuration changes for {name} were applied.',
'agents.notice.applied': 'Configuration changes for {name} were applied.',
'agents.notice.defaultWritten': 'The default configuration for {name} was written.',
'agents.notice.updated': 'Configuration changes for {name} were updated.',
'agents.error.applyFirst': 'The selected model has not been applied. Click “Apply Configuration Changes” first.',
'agents.notice.appliedLaunch': '{name} applied the model and started.',
'agents.notice.syncedLaunch': '{name} synchronized the managed configuration and started.',
'agents.notice.managedLaunch': '{name} started with the configuration changes still applied.',
'agents.notice.launched': '{name} started',
'agents.status.detecting': 'Detecting',
'agents.status.unsupported': 'Unsupported Platform',
'agents.status.notInstalled': 'Not Installed',
@@ -1099,6 +1088,11 @@ export const en: Record<MessageKey, string> = {
'agents.status.unconfigured': 'Not Configured',
'agents.title': 'Agent Configuration',
'agents.redetect': 'Detect Again',
'agents.tabs.label': 'Agent configuration sections',
'agents.tabs.core': 'Core Configuration',
'agents.tabs.sessions': 'Session Management',
'agents.sessions.title': 'Codex Session Management',
'agents.sessions.description': 'Session browsing and management will appear here.',
'agents.localClients': 'Local Clients',
'agents.selectClient': 'Select an agent to manage',
'agents.installStatus': 'Installation Status',
+1
View File
@@ -2,6 +2,7 @@ const phraseReplacements: ReadonlyArray<readonly [string, string]> = [
['繁体中文', '繁體中文'],
['简体中文', '簡體中文'],
['智能体', '智慧代理'],
['会话', '工作階段'],
['使用记录', '使用記錄'],
['认证文件', '認證檔案'],
['配置文件', '設定檔'],
+300 -195
View File
@@ -17,6 +17,7 @@ import {
Check,
ChevronDown,
LoaderCircle,
MessageSquare,
Play,
RefreshCw,
Search,
@@ -88,6 +89,14 @@ type AgentDefinition = {
descriptionKey: 'agents.description.claudeCode' | 'agents.description.claudeDesktop' | 'agents.description.codex' | 'agents.description.opencode' | 'agents.description.openclaw' | 'agents.description.hermes';
};
type AgentSubpageId = 'core' | 'sessions';
type AgentSubpageDefinition = {
id: AgentSubpageId;
labelKey: 'agents.tabs.core' | 'agents.tabs.sessions';
clients?: readonly AgentClientId[];
};
const agentDefinitions: AgentDefinition[] = [
{
id: 'claude-code',
@@ -127,6 +136,20 @@ const agentDefinitions: AgentDefinition[] = [
},
];
const agentSubpages: AgentSubpageDefinition[] = [
{
id: 'core',
labelKey: 'agents.tabs.core',
},
{
id: 'sessions',
labelKey: 'agents.tabs.sessions',
clients: ['codex'],
},
];
const DEFAULT_AGENT_SUBPAGE: AgentSubpageId = 'core';
const AGENT_MODEL_SELECTIONS_KEY = 'cpa-gui.agent-model-selections.v1';
const AGENT_SELECTED_CLIENT_KEY = 'cpa-gui.agent-selected-client.v1';
@@ -455,6 +478,7 @@ function AgentModelPicker({
export function AgentsPage() {
const { t } = useI18n();
const [selected, setSelected] = useState<AgentClientId>(readSelectedAgentClient);
const [activeSubpage, setActiveSubpage] = useState<AgentSubpageId>(DEFAULT_AGENT_SUBPAGE);
const [statuses, setStatuses] = useState<AgentConfigStatus[]>([]);
const [models, setModels] = useState<ModelOption[]>([]);
const [modelByClient, setModelByClient] = useState<Partial<Record<AgentClientId, string>>>(
@@ -465,9 +489,12 @@ export function AgentsPage() {
const [modelLoading, setModelLoading] = useState(false);
const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'launch' | null>(null);
const busy = busyAction !== null;
const [error, setError] = useState('');
const [detectionError, setDetectionError] = useState('');
const [modelError, setModelError] = useState('');
const [notice, setNotice] = useState('');
const [modelSelectionError, setModelSelectionError] = useState('');
const [configurationError, setConfigurationError] = useState('');
const [launchError, setLaunchError] = useState('');
const [defaultError, setDefaultError] = useState('');
const [defaultConfirmOpen, setDefaultConfirmOpen] = useState(false);
const loadStatuses = useCallback(async (forceRefresh = false) => {
@@ -484,6 +511,7 @@ export function AgentsPage() {
try {
const nextModels = await invoke<ModelOption[]>('get_agent_models');
setModels(nextModels);
setModelSelectionError('');
setModelByClient((current) => {
const next = reconcileAgentModelSelections(current, nextModels);
writeAgentModelSelections(next);
@@ -498,11 +526,11 @@ export function AgentsPage() {
const refresh = useCallback(async () => {
setLoading(true);
setError('');
setDetectionError('');
try {
await Promise.all([loadStatuses(true), loadModels()]);
} catch (requestError) {
setError(String(requestError));
setDetectionError(String(requestError));
} finally {
setLoading(false);
}
@@ -510,9 +538,9 @@ export function AgentsPage() {
useEffect(() => {
setLoading(true);
setError('');
setDetectionError('');
void Promise.all([loadStatuses(), loadModels()])
.catch((requestError) => setError(String(requestError)))
.catch((requestError) => setDetectionError(String(requestError)))
.finally(() => setLoading(false));
}, [loadModels, loadStatuses]);
@@ -521,8 +549,9 @@ export function AgentsPage() {
let stop: (() => void) | null = null;
void listen('config-files-changed', () => {
if (disposed) return;
setDetectionError('');
void Promise.all([loadStatuses(true), loadModels()]).catch((requestError) => {
if (!disposed) setError(String(requestError));
if (!disposed) setDetectionError(String(requestError));
});
}).then((unlisten) => {
if (disposed) unlisten();
@@ -538,6 +567,15 @@ export function AgentsPage() {
writeSelectedAgentClient(selected);
}, [selected]);
useEffect(() => {
setActiveSubpage(DEFAULT_AGENT_SUBPAGE);
setModelSelectionError('');
setConfigurationError('');
setLaunchError('');
setDefaultError('');
setDefaultConfirmOpen(false);
}, [selected]);
useEffect(() => {
setLaunchTargetByClient((current) => agentDefinitions.reduce<Partial<Record<AgentClientId, string>>>(
(next, definition) => {
@@ -587,9 +625,19 @@ export function AgentsPage() {
void loadModels();
};
const reloadStatusesAfterAction = async () => {
setDetectionError('');
try {
await loadStatuses(true);
} catch (requestError) {
setDetectionError(String(requestError));
}
};
const selectModel = (value: string) => {
const model = findAgentModel(models, value);
if (!model) return;
setModelSelectionError('');
setModelByClient((current) => {
const next = { ...current, [selected]: model.name };
writeAgentModelSelections(next);
@@ -599,36 +647,35 @@ export function AgentsPage() {
const requireSelectedModel = () => {
if (modelLoading) {
setError(t('agents.error.modelsLoading'));
setModelSelectionError(t('agents.error.modelsLoading'));
return null;
}
if (models.length === 0) {
setError(modelError || t('agents.error.noModels'));
setModelSelectionError(modelError || t('agents.error.noModels'));
return null;
}
const model = findAgentModel(models, selectedModel);
if (!model) {
setError(t('agents.error.selectionGone'));
setModelSelectionError(t('agents.error.selectionGone'));
return null;
}
setModelSelectionError('');
return model.name;
};
const applyConfigurationChanges = async () => {
setConfigurationError('');
const model = requireSelectedModel();
if (!model) return;
setBusyAction('apply');
setError('');
setNotice('');
try {
await invoke<AgentConfigActionResult>('apply_agent_config', {
client: selected,
model,
});
setNotice(t('agents.notice.applied', { name: activeDefinition.name }));
await loadStatuses(true);
await reloadStatusesAfterAction();
} catch (requestError) {
setError(String(requestError));
setConfigurationError(String(requestError));
} finally {
setBusyAction(null);
}
@@ -638,18 +685,16 @@ export function AgentsPage() {
const model = requireSelectedModel();
if (!model) return;
setBusyAction('default');
setError('');
setNotice('');
setDefaultError('');
try {
await invoke<AgentConfigActionResult>('reset_agent_config_to_default', {
client: selected,
model,
});
setDefaultConfirmOpen(false);
setNotice(t('agents.notice.defaultWritten', { name: activeDefinition.name }));
await loadStatuses(true);
await reloadStatusesAfterAction();
} catch (requestError) {
setError(String(requestError));
setDefaultError(String(requestError));
} finally {
setBusyAction(null);
}
@@ -657,41 +702,32 @@ export function AgentsPage() {
const launchAgent = async () => {
setBusyAction('launch');
setError('');
setNotice('');
setLaunchError('');
try {
if (draftChanged) {
throw new Error(t('agents.error.applyFirst'));
}
await invoke('launch_agent', { client: selected, target: selectedLaunchTarget?.id });
setNotice(t('agents.notice.managedLaunch', { name: activeDefinition.name }));
} catch (requestError) {
setError(String(requestError));
setLaunchError(String(requestError));
} finally {
setBusyAction(null);
}
};
const statusLabel = loading
? t('agents.status.detecting')
: !activeStatus?.supportedPlatform
? t('agents.status.unsupported')
: !activeStatus.installed
? t('agents.status.notInstalled')
: activeStatus.modificationState === 'external-changed'
? t('agents.status.externalChanged')
: activeStatus.modificationState === 'invalid'
? t('agents.status.invalid')
: activeStatus.modificationState === 'applied'
? t('agents.status.applied')
: t('agents.status.unconfigured');
const statusTone = activeStatus?.modificationState === 'external-changed'
|| activeStatus?.modificationState === 'invalid'
|| !activeStatus?.supportedPlatform
? 'error'
: activeStatus?.modificationState === 'applied'
? 'success'
: '';
const openDefaultConfirmation = () => {
setDefaultError('');
setDefaultConfirmOpen(true);
};
const closeDefaultConfirmation = () => {
setDefaultError('');
setDefaultConfirmOpen(false);
};
const availableSubpages = agentSubpages.filter(
(subpage) => !subpage.clients || subpage.clients.includes(selected),
);
return (
<section className="page management-page agents-page">
@@ -700,17 +736,19 @@ export function AgentsPage() {
<span>Agent Clients</span>
<h1>{t('agents.title')}</h1>
</div>
<button type="button" className="secondary-button compact-button" onClick={() => void refresh()} disabled={loading || busy}>
<RefreshCw size={16} className={loading ? 'spin' : ''} />
{t('agents.redetect')}
</button>
<div className="agent-header-actions">
{detectionError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{detectionError}
</span>
) : null}
<button type="button" className="secondary-button compact-button" onClick={() => void refresh()} disabled={loading || busy}>
<RefreshCw size={16} className={loading ? 'spin' : ''} />
{t('agents.redetect')}
</button>
</div>
</header>
<div className="agent-feedback-slot" aria-live="polite">
{error ? <div className="management-alert error">{error}</div> : null}
{!error && notice ? <div className="management-alert success">{notice}</div> : null}
</div>
<div className="agent-workbench">
<aside className="panel agent-client-list">
<div className="agent-list-heading">
@@ -725,7 +763,10 @@ export function AgentsPage() {
type="button"
className={selected === agent.id ? 'active' : ''}
key={agent.id}
onClick={() => setSelected(agent.id)}
onClick={() => {
setActiveSubpage(DEFAULT_AGENT_SUBPAGE);
setSelected(agent.id);
}}
disabled={busy}
>
<span className="agent-client-icon"><AgentMark definition={agent} /></span>
@@ -741,154 +782,213 @@ export function AgentsPage() {
</aside>
<section className="panel agent-config-panel">
<div className="agent-config-heading">
<div className="agent-config-title">
<span className="agent-logo"><AgentMark definition={activeDefinition} size={24} /></span>
<div><h2>{activeDefinition.name}</h2><span>{t(activeDefinition.descriptionKey)}</span></div>
</div>
<span className={`state-pill ${statusTone}`}>{statusLabel}</span>
</div>
<div className="agent-config-message-slot">
{activeStatus?.error ? <div className="management-alert error">{activeStatus.error}</div> : null}
{activeStatus?.warnings.length && !activeStatus.error ? (
<div className="agent-warning-line">{activeStatus.warnings.join(';')}</div>
) : null}
</div>
<div className="agent-status-grid">
<div>
<span><BadgeCheck size={14} />{t('agents.installStatus')}</span>
<strong>{activeStatus?.installed ? t('agents.clientDetected') : t('agents.clientNotDetected')}</strong>
</div>
<div>
<span>{t('agents.clientVersion')}</span>
<strong title={activeStatus?.version ?? undefined}>{activeStatus?.version ?? t('agents.notFetched')}</strong>
</div>
</div>
<section className="agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong><span>{t('agents.useModelDescription')}</span></div>
{draftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
</div>
<AgentModelPicker
models={models}
value={selectedModel}
loading={modelLoading}
error={modelError}
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
onChange={selectModel}
onRefresh={refreshModels}
/>
<span className="agent-model-hint">
{modelLoading
? t('agents.model.readingAvailable')
: modelError && models.length === 0
? modelError
: models.length === 0
? t('agents.model.cannotConfigure')
: activeStatus?.modificationState === 'applied'
? t('agents.model.current', { model: appliedModel || '—' })
: t('agents.model.firstSelection', { count: models.length })}
</span>
</section>
<section className={`agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
<div className="agent-modification-copy">
<strong>{t('agents.modify.title')}</strong>
<span>{activeStatus?.modificationState === 'external-changed'
? t('agents.modify.externalChanged')
: activeStatus?.modificationState === 'invalid'
? t('agents.modify.invalid')
: activeStatus?.modificationState === 'applied'
? t('agents.modify.applied')
: t('agents.modify.unconfigured')}</span>
</div>
<div className="agent-modification-buttons">
<div className="agent-subpage-tabs" role="tablist" aria-label={t('agents.tabs.label')}>
{availableSubpages.map((subpage) => (
<button
type="button"
className="primary-button"
onClick={() => void applyConfigurationChanges()}
disabled={
busy
|| !canEnable
}
id={`agent-subpage-tab-${subpage.id}`}
role="tab"
className={activeSubpage === subpage.id ? 'active' : ''}
aria-selected={activeSubpage === subpage.id}
aria-controls={`agent-subpage-panel-${subpage.id}`}
tabIndex={activeSubpage === subpage.id ? 0 : -1}
key={subpage.id}
onClick={() => setActiveSubpage(subpage.id)}
>
{busyAction === 'apply' ? <LoaderCircle size={16} className="spin" /> : <Sparkles size={16} />}
{t('agents.modify.apply')}
{t(subpage.labelKey)}
</button>
<button
type="button"
className="secondary-button"
onClick={() => setDefaultConfirmOpen(true)}
disabled={busy || !canEnable}
>
{busyAction === 'default' ? <LoaderCircle size={16} className="spin" /> : <RefreshCw size={16} />}
{t('agents.modify.default')}
</button>
</div>
</section>
))}
</div>
<div className="agent-config-footer">
<div>
{activeStatus?.modificationState === 'applied' ? <Check size={16} /> : <Sparkles size={16} />}
<span>{activeStatus?.modificationState === 'applied'
? draftChanged
? t('agents.footer.changed')
: t('agents.footer.applied')
: activeStatus?.modificationState === 'external-changed'
? t('agents.footer.externalChanged')
: 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')
: t('agents.footer.enableFirst')}</span>
</div>
<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>
))}
{activeSubpage === 'core' ? (
<div
className="agent-core-config"
id="agent-subpage-panel-core"
role="tabpanel"
aria-labelledby="agent-subpage-tab-core"
>
<div className="agent-status-grid">
<div>
<span><BadgeCheck size={14} />{t('agents.installStatus')}</span>
<strong>{activeStatus?.installed ? t('agents.clientDetected') : t('agents.clientNotDetected')}</strong>
</div>
<div>
<span>{t('agents.clientVersion')}</span>
<strong title={activeStatus?.version ?? undefined}>{activeStatus?.version ?? t('agents.notFetched')}</strong>
</div>
</div>
{activeStatus?.error || activeStatus?.warnings.length ? (
<div className="agent-status-messages" aria-live="polite">
{activeStatus.error ? (
<span className="agent-inline-message error" role="alert">{activeStatus.error}</span>
) : (
<span className="agent-inline-message warning">{activeStatus.warnings.join(';')}</span>
)}
</div>
) : null}
<button
type="button"
className="primary-button"
onClick={() => void launchAgent()}
disabled={
busy
|| !canLaunch
|| draftChanged
}
title={draftChanged
? t('agents.launch.applyFirst')
: activeStatus?.modificationState === 'applied'
? selectedLaunchTarget?.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>
<section className="agent-model-section">
<div className="agent-section-heading">
<div><strong>{t('agents.useModel')}</strong><span>{t('agents.useModelDescription')}</span></div>
{draftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
</div>
<AgentModelPicker
models={models}
value={selectedModel}
loading={modelLoading}
error={modelError}
disabled={busy || !activeStatus?.installed || !activeStatus.supportedPlatform}
onChange={selectModel}
onRefresh={refreshModels}
/>
<span
className={`agent-model-hint ${modelSelectionError || modelError ? 'error' : ''}`}
role={modelSelectionError || modelError ? 'alert' : undefined}
aria-live="polite"
>
{modelSelectionError
|| modelError
|| (modelLoading
? t('agents.model.readingAvailable')
: models.length === 0
? t('agents.model.cannotConfigure')
: activeStatus?.modificationState === 'applied'
? t('agents.model.current', { model: appliedModel || '—' })
: t('agents.model.firstSelection', { count: models.length }))}
</span>
</section>
<section className={`agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
<div className="agent-modification-copy">
<strong>{t('agents.modify.title')}</strong>
<span>{activeStatus?.modificationState === 'external-changed'
? t('agents.modify.externalChanged')
: activeStatus?.modificationState === 'invalid'
? t('agents.modify.invalid')
: activeStatus?.modificationState === 'applied'
? t('agents.modify.applied')
: t('agents.modify.unconfigured')}</span>
</div>
<div className="agent-modification-control">
<div className="agent-modification-buttons">
<button
type="button"
className="primary-button"
onClick={() => void applyConfigurationChanges()}
disabled={
busy
|| !canEnable
}
>
{busyAction === 'apply' ? <LoaderCircle size={16} className="spin" /> : <Sparkles size={16} />}
{t('agents.modify.apply')}
</button>
<button
type="button"
className="secondary-button"
onClick={openDefaultConfirmation}
disabled={busy || !canEnable}
>
{busyAction === 'default' ? <LoaderCircle size={16} className="spin" /> : <RefreshCw size={16} />}
{t('agents.modify.default')}
</button>
</div>
{configurationError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{configurationError}
</span>
) : null}
</div>
</section>
<div className="agent-config-footer">
<div className="agent-config-summary">
{activeStatus?.modificationState === 'applied' ? <Check size={16} /> : <Sparkles size={16} />}
<span>{activeStatus?.modificationState === 'applied'
? draftChanged
? t('agents.footer.changed')
: t('agents.footer.applied')
: activeStatus?.modificationState === 'external-changed'
? t('agents.footer.externalChanged')
: 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')
: t('agents.footer.enableFirst')}</span>
</div>
<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
|| draftChanged
}
title={draftChanged
? t('agents.launch.applyFirst')
: activeStatus?.modificationState === 'applied'
? selectedLaunchTarget?.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>
</div>
{launchError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{launchError}
</span>
) : null}
</div>
</div>
</div>
</div>
) : null}
{selected === 'codex' && activeSubpage === 'sessions' ? (
<div
className="agent-sessions-page"
id="agent-subpage-panel-sessions"
role="tabpanel"
aria-labelledby="agent-subpage-tab-sessions"
>
<div className="agent-sessions-empty">
<span className="agent-sessions-icon" aria-hidden="true">
<MessageSquare size={22} />
</span>
<div>
<h3>{t('agents.sessions.title')}</h3>
<p>{t('agents.sessions.description')}</p>
</div>
</div>
</div>
) : null}
</section>
</div>
@@ -901,8 +1001,13 @@ export function AgentsPage() {
<p>
{t('agents.default.description', { name: activeDefinition.name })}
</p>
{defaultError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{defaultError}
</span>
) : null}
<div className="config-dialog-actions two-actions">
<button type="button" className="secondary-button" onClick={() => setDefaultConfirmOpen(false)} disabled={busy}>{t('common.cancel')}</button>
<button type="button" className="secondary-button" onClick={closeDefaultConfirmation} disabled={busy}>{t('common.cancel')}</button>
<button type="button" className="danger-button" onClick={() => void resetConfigurationToDefault()} disabled={busy}>
{busyAction === 'default' ? <LoaderCircle size={16} className="spin" /> : null}
{t('agents.default.confirm')}
+210 -97
View File
@@ -908,8 +908,7 @@ input:focus-visible {
background: #f4f1ea;
}
.button-row.control-action-row,
.button-row.version-action-row {
.button-row.control-action-row {
--kernel-action-button-min: clamp(112px, 12ch, 156px);
grid-template-columns: repeat(
@@ -923,11 +922,15 @@ input:focus-visible {
}
.button-row.version-action-row {
--kernel-action-button-min: clamp(120px, 15ch, 176px);
grid-template-columns: repeat(4, minmax(0, 1fr));
grid-auto-flow: row;
grid-auto-columns: unset;
grid-auto-rows: 38px;
align-items: stretch;
gap: 10px;
}
.control-action-row > button,
.version-action-row > button {
.control-action-row > button {
width: 100%;
min-height: 42px;
height: auto;
@@ -948,6 +951,20 @@ input:focus-visible {
.software-update-actions {
grid-template-columns: repeat(auto-fit, minmax(min(120px, 100%), 1fr));
grid-auto-flow: row;
grid-auto-rows: 38px;
align-items: stretch;
}
.software-update-actions > button,
.version-action-row > button {
width: 100%;
height: 38px;
min-height: 38px;
padding: 0 10px;
line-height: normal;
overflow-wrap: normal;
text-wrap: nowrap;
white-space: nowrap;
}
.software-update-panel .panel-heading p {
@@ -2796,7 +2813,7 @@ input:focus-visible {
.real-provider-workbench + .config-dialog-backdrop .management-dialog {
grid-template-rows: auto repeat(5, minmax(52px, auto)) auto;
height: auto;
max-height: min(600px, calc(100vh - 32px));
max-height: min(720px, calc(100vh - 48px));
overflow: auto;
}
@@ -3710,24 +3727,47 @@ input:focus-visible {
.agent-client-list,
.agent-config-panel {
height: 540px;
min-height: 540px;
}
.agents-page {
grid-template-rows: 54px 42px auto;
grid-template-rows: minmax(54px, auto) auto;
gap: 14px;
}
.agent-feedback-slot {
min-height: 42px;
overflow: hidden;
.agent-header-actions {
display: flex;
min-width: 0;
align-items: center;
justify-content: flex-end;
gap: 10px;
}
.agent-feedback-slot .management-alert {
min-height: 42px;
max-height: 42px;
overflow: hidden;
.agent-header-actions .agent-inline-message {
max-width: 360px;
text-align: right;
}
.agent-header-actions .compact-button {
flex: none;
}
.agent-inline-message {
display: block;
min-width: 0;
color: #7a746c;
font-size: 11px;
line-height: 1.45;
overflow-wrap: anywhere;
}
.agent-inline-message.error,
.agent-model-hint.error {
color: #9f2d20;
}
.agent-inline-message.warning {
color: #805a24;
}
.agent-client-list {
@@ -3831,31 +3871,106 @@ input:focus-visible {
.agent-config-panel {
display: grid;
grid-template-rows: 44px 40px 70px 138px minmax(72px, auto) minmax(48px, 1fr);
align-content: stretch;
grid-template-rows: auto minmax(0, 1fr);
align-content: start;
gap: 14px;
}
.agent-config-message-slot {
min-height: 40px;
overflow: hidden;
.agent-subpage-tabs {
display: inline-grid;
grid-auto-flow: column;
justify-self: start;
max-width: 100%;
gap: 3px;
padding: 3px;
overflow-x: auto;
border: 1px solid #d5d2cb;
border-radius: 8px;
background: #eeece6;
}
.agent-config-message-slot .management-alert,
.agent-config-message-slot .agent-warning-line {
min-height: 40px;
max-height: 40px;
overflow: hidden;
.agent-subpage-tabs button {
display: inline-flex;
min-width: 104px;
height: 34px;
align-items: center;
justify-content: center;
padding: 0 12px;
border: 0;
border-radius: 6px;
background: transparent;
color: #7a746c;
font-size: 11px;
font-weight: 700;
white-space: nowrap;
}
.agent-config-message-slot .agent-warning-line {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
.agent-subpage-tabs button:hover,
.agent-subpage-tabs button.active {
background: #fffdf8;
color: #2d2a26;
}
.agent-subpage-tabs button.active {
box-shadow: 0 1px 4px rgba(45, 42, 38, 0.14);
}
.agent-core-config {
display: grid;
min-width: 0;
align-content: start;
gap: 14px;
}
.agent-sessions-page {
display: grid;
min-width: 0;
min-height: 360px;
place-items: center;
padding: 32px;
border: 1px dashed #d5d2cb;
border-radius: 8px;
background: #faf9f5;
}
.agent-sessions-empty {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
align-items: center;
gap: 12px;
width: min(100%, 420px);
}
.agent-sessions-icon {
display: grid;
width: 44px;
height: 44px;
place-items: center;
border: 1px solid #e3e1db;
border-radius: 9px;
background: #fffdf8;
color: #5f5a53;
}
.agent-sessions-empty h3 {
margin: 0 0 5px;
color: #2d2a26;
font-size: 14px;
}
.agent-sessions-empty p {
margin: 0;
color: #7a746c;
font-size: 11px;
line-height: 1.6;
}
.agent-status-messages {
display: grid;
gap: 4px;
padding: 0 2px;
}
.agent-config-heading,
.agent-config-title,
.agent-config-footer,
.agent-config-footer > div,
.agent-status-grid span {
@@ -3863,7 +3978,6 @@ input:focus-visible {
align-items: center;
}
.agent-config-heading,
.agent-config-footer {
justify-content: space-between;
gap: 14px;
@@ -3904,67 +4018,6 @@ input:focus-visible {
box-shadow: 0 1px 3px rgba(45, 42, 38, 0.14);
}
.agent-config-heading .state-pill {
min-width: 92px;
justify-content: center;
text-align: center;
}
.agent-config-title {
min-width: 0;
gap: 11px;
}
.agent-logo {
display: grid;
width: 42px;
height: 42px;
flex: none;
place-items: center;
border: 1px solid #e3e1db;
border-radius: 8px;
background: #fffdf8;
}
.agent-logo img {
width: 24px;
height: 24px;
}
.agent-logo svg {
color: #5f5a53;
}
.agent-config-title > div {
display: grid;
min-width: 0;
gap: 4px;
}
.agent-config-title h2 {
margin: 0;
color: #2d2a26;
font-size: 17px;
}
.agent-config-title span {
overflow: hidden;
color: #7a746c;
font-size: 11px;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-warning-line {
padding: 9px 11px;
border: 1px solid #e4d0af;
border-radius: 7px;
background: #fff8eb;
color: #805a24;
font-size: 11px;
line-height: 1.5;
}
.agent-status-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4002,11 +4055,11 @@ input:focus-visible {
display: grid;
gap: 9px;
min-width: 0;
min-height: 138px;
padding: 15px;
border: 1px solid #e3e1db;
border-radius: 8px;
background: #faf9f5;
height: 138px;
}
.agent-section-heading {
@@ -4034,6 +4087,11 @@ input:focus-visible {
font-size: 11px;
}
.agent-model-hint {
line-height: 1.45;
overflow-wrap: anywhere;
}
.agent-pending-badge {
flex: none;
padding: 4px 8px;
@@ -4277,6 +4335,16 @@ input:focus-visible {
line-height: 1.45;
}
.agent-modification-control {
display: grid;
min-width: 0;
gap: 6px;
}
.agent-modification-control > .agent-inline-message {
text-align: right;
}
.agent-modification-buttons {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -4295,7 +4363,7 @@ input:focus-visible {
}
.agent-restore-dialog {
grid-template-rows: 40px minmax(58px, auto) 40px;
grid-template-rows: auto;
height: auto;
min-height: 210px;
}
@@ -4313,6 +4381,10 @@ input:focus-visible {
line-height: 1.65;
}
.agent-restore-dialog > .agent-inline-message {
margin-top: -6px;
}
.agent-config-footer {
min-height: 48px;
padding-top: 14px;
@@ -4326,12 +4398,23 @@ input:focus-visible {
font-size: 11px;
}
.agent-config-footer > div span {
.agent-config-summary > span {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.agent-config-footer > .agent-launch-control {
display: grid;
justify-items: end;
gap: 5px;
}
.agent-launch-control > .agent-inline-message {
max-width: 360px;
text-align: right;
}
.agent-config-footer .primary-button {
min-width: 120px;
}
@@ -5602,6 +5685,22 @@ input:focus-visible {
flex-direction: column;
}
.agent-header-actions {
width: 100%;
align-items: stretch;
flex-direction: column;
}
.agent-header-actions .agent-inline-message {
max-width: none;
text-align: left;
}
.agent-subpage-tabs {
width: 100%;
grid-auto-columns: minmax(104px, 1fr);
}
.usage-tabs {
width: 100%;
grid-auto-columns: 1fr;
@@ -5647,7 +5746,10 @@ input:focus-visible {
grid-template-columns: 1fr;
}
.agent-config-heading,
.agent-status-grid {
height: auto;
}
.agent-config-footer,
.agent-section-heading {
align-items: stretch;
@@ -5658,6 +5760,17 @@ input:focus-visible {
width: 100%;
}
.agent-modification-control > .agent-inline-message,
.agent-launch-control > .agent-inline-message {
max-width: none;
text-align: left;
}
.agent-config-footer > .agent-launch-control {
width: 100%;
justify-items: stretch;
}
.agent-launch-actions {
width: 100%;
align-items: stretch;