feat: add credential priority controls and OAuth defaults

This commit is contained in:
lzt404 committed 2026-07-30 01:21:48 +08:00
1 parent 0f3e5093e8
commit bbe22a96eb
13 files changed
+582 -416

No files matched your search

+18 -17
View File
@@ -60,6 +60,7 @@ export const jaOverrides = {
'quota.confirm.warning': '「確認」を押した場合のみ実行されます。「キャンセル」ではリセット回数を消費しません。',
'oauth.title': 'OAuth ログイン',
'oauth.loginSuccess': '{provider} へのログインに成功しました',
'oauth.priorityApplyFailed': 'OAuth ログインは成功しましたが、新しい認証情報の優先度を 0 に設定できませんでした:{error}',
'oauth.authFailed': '認証に失敗しました',
'oauth.loginFailed': '{provider} へのログインに失敗しました{detail}',
'oauth.missingState': 'コアから OAuth state が返されませんでした',
@@ -204,18 +205,10 @@ export const jaOverrides = {
'aliases.created': '{alias} を作成し、推論レベルを {effort} に固定しました',
'aliases.deleteConfirm': 'エイリアスモデル「{alias}」を削除しますか?\n対応する推論レベルの上書きルールも削除されます。',
'aliases.deleted': '{alias} を削除しました',
'aliases.title': '推論エイリアス',
'aliases.intro.title': '元のモデルに固定推論レベルのカスタムエイリアスを追加',
'aliases.intro.description': 'エイリアス名は自由に指定できます。クライアントがエイリアスを使用した場合のみ CPA が選択した推論レベルを注入し、元のモデルを直接使用した場合は名前、設定、動作を変更しません。',
'aliases.intro.badge': '元のモデルには影響しません',
'aliases.flow.aria': '推論エイリアスのワークフロー',
'aliases.flow.original': '元のモデル',
'aliases.flow.direct': '直接呼び出し',
'aliases.flow.directValue': 'deepseek-v4-pro · 変更なし',
'aliases.flow.alias': 'エイリアス呼び出し',
'aliases.flow.aliasValue': 'deepseek-v4-pro-max · max に固定',
'aliases.create.title': '推論エイリアスを作成',
'aliases.create.description': 'Codex OAuth、Codex API、OpenAI 互換接続に対応',
'aliases.modelAlias.title': 'モデルエイリアス',
'aliases.modelAlias.description': 'クライアントが使用する名前を入力し、最終マッピングを確認します',
'aliases.originalModel': '元のモデル',
'aliases.loadingModels': 'モデルを読み込んでいます',
'aliases.searchModel': '元のモデルを検索して選択',
@@ -336,6 +329,13 @@ export const jaOverrides = {
'authFiles.quota.expiry': '最も早い有効期限 {time}',
'authFiles.models.noneForProvider': 'このプロバイダーには設定可能なモデル定義がありません',
'authFiles.models.updated': '{provider} の公開モデルを更新しました',
'authFiles.priority.button': '優先度 {priority}',
'authFiles.priority.title': '認証情報の優先度を変更',
'authFiles.priority.label': '優先度',
'authFiles.priority.placeholder': '例:10 または -1',
'authFiles.priority.hint': '整数のみ指定できます。値が大きいほど優先度が高くなります。空欄または 0 で既定値に戻ります。',
'authFiles.priority.invalid': '有効な整数の優先度を入力してください',
'authFiles.priority.updated': '{name} の優先度を更新しました',
'authFiles.uploaded': '認証ファイルを {count} 件アップロードしました',
'authFiles.uploadFailed': '{count} 件のファイルをアップロードできませんでした:{errors}',
'authFiles.notice.enabled': '認証ファイルを有効にしました',
@@ -345,8 +345,6 @@ export const jaOverrides = {
'authFiles.deleted': '認証ファイルを削除しました',
'authFiles.deleteAllConfirm': 'ディスク上の認証ファイルをすべて削除しますか?実行時認証は削除されません。',
'authFiles.deletedAll': 'ディスク上の認証ファイルをすべて削除しました',
'authFiles.runtimeDownloadError': '実行時認証ファイルにはダウンロード可能なディスクファイルがありません',
'authFiles.downloaded': '認証ファイルをダウンロードしました',
'common.copyFailed': 'コピーに失敗しました',
'authFiles.title': '認証ファイル',
'authFiles.summary': 'ファイル {files} 件 · 無効 {disabled} 件',
@@ -372,7 +370,7 @@ export const jaOverrides = {
'authFiles.models.settings': '公開モデルを設定',
'authFiles.models.button': 'モデル',
'authFiles.copyName': 'ファイル名をコピー',
'common.download': 'ダウンロード',
'authFiles.openDirectory': '認証情報フォルダーを開く',
'common.enable': '有効化',
'common.disable': '無効化',
'authFiles.runtimeFootnote': '{count} 件の認証情報は実行時ストレージ由来です。対応するプロバイダーまたは認証元でのみ管理できます。',
@@ -409,10 +407,6 @@ export const jaOverrides = {
'agents.model.search': 'モデル名またはエイリアスを検索',
'agents.model.clearSearch': '検索を消去',
'agents.model.refresh': 'モデルを更新',
'agents.catalog.update': 'モデル設定情報を更新',
'agents.catalog.updating': 'モデル設定を更新しています',
'agents.catalog.updated': 'GitHub からモデル設定を更新しました',
'agents.catalog.unchanged': 'モデル設定は最新です',
'agents.model.fetching': 'モデルを取得しています',
'agents.model.noMatch': '一致するモデルがありません',
'agents.model.unavailable': '現在利用可能なモデルはありません',
@@ -710,6 +704,13 @@ export const jaOverrides = {
'appUpdate.available': 'バージョン {version} を利用できます',
'appUpdate.check': '更新を確認',
'appUpdate.checking': '確認中',
'appUpdate.catalog.label': 'Codex モデルカタログ',
'appUpdate.catalog.description': '起動時に GitHub から通知なしで自動同期します',
'appUpdate.catalog.update': 'モデルカタログを同期',
'appUpdate.catalog.updateHint': 'GitHub を確認して Codex モデルカタログを更新します',
'appUpdate.catalog.updating': 'モデルカタログを同期しています',
'appUpdate.catalog.updated': 'GitHub からモデルカタログを同期しました',
'appUpdate.catalog.unchanged': 'モデルカタログは最新です',
'appUpdate.installNow': '今すぐ更新',
'appUpdate.openRelease': 'ダウンロードページを開く',
'appUpdate.confirmTitle': 'アプリ更新をインストール',
+36 -34
View File
@@ -63,6 +63,7 @@ export const zhCN = {
'quota.confirm.warning': '只有点击“确定”后才会执行;点击“取消”不会消耗重置机会。',
'oauth.title': 'OAuth 登录',
'oauth.loginSuccess': '{provider} 登录成功',
'oauth.priorityApplyFailed': 'OAuth 登录成功,但无法将新凭证的优先级设为 0:{error}',
'oauth.authFailed': '认证失败',
'oauth.loginFailed': '{provider} 登录失败{detail}',
'oauth.missingState': '内核未返回 OAuth state',
@@ -207,18 +208,10 @@ export const zhCN = {
'aliases.created': '已创建 {alias},思考强度固定为 {effort}',
'aliases.deleteConfirm': '确定删除别名模型“{alias}”吗?\n将同时移除对应的思考强度覆盖规则。',
'aliases.deleted': '已删除 {alias}',
'aliases.title': '思考别名',
'aliases.intro.title': '为原模型新增一个固定思考强度的自定义别名',
'aliases.intro.description': '别名名称由你填写,客户端调用它时 CPA 才会注入所选思考强度;直接调用原模型时,原有名称、配置和行为全部保持不变。',
'aliases.intro.badge': '原模型不受影响',
'aliases.flow.aria': '思考别名工作流程',
'aliases.flow.original': '原模型',
'aliases.flow.direct': '直接调用',
'aliases.flow.directValue': 'deepseek-v4-pro · 保持不变',
'aliases.flow.alias': '别名调用',
'aliases.flow.aliasValue': 'deepseek-v4-pro-max · 固定 max 强度',
'aliases.create.title': '创建思考别名',
'aliases.create.description': '支持 Codex OAuth、Codex API 和 OpenAI 兼容接入',
'aliases.modelAlias.title': '模型别名',
'aliases.modelAlias.description': '填写客户端调用名称并确认最终映射',
'aliases.originalModel': '原模型',
'aliases.loadingModels': '正在读取模型',
'aliases.searchModel': '搜索并选择原模型',
@@ -339,6 +332,13 @@ export const zhCN = {
'authFiles.quota.expiry': '最早过期 {time}',
'authFiles.models.noneForProvider': '该提供商没有可配置的模型定义',
'authFiles.models.updated': '{provider} 开放模型已更新',
'authFiles.priority.button': '优先级 {priority}',
'authFiles.priority.title': '修改凭证优先级',
'authFiles.priority.label': '优先级',
'authFiles.priority.placeholder': '例如 10 或 -1',
'authFiles.priority.hint': '仅支持整数;数值越大优先级越高。留空或填写 0 恢复默认。',
'authFiles.priority.invalid': '请输入有效的整数优先级',
'authFiles.priority.updated': '{name} 的优先级已更新',
'authFiles.uploaded': '已上传 {count} 个认证文件',
'authFiles.uploadFailed': '{count} 个文件上传失败:{errors}',
'authFiles.notice.enabled': '认证文件已启用',
@@ -348,8 +348,6 @@ export const zhCN = {
'authFiles.deleted': '认证文件已删除',
'authFiles.deleteAllConfirm': '确定删除所有磁盘认证文件吗?运行时认证不会被删除。',
'authFiles.deletedAll': '磁盘认证文件已全部删除',
'authFiles.runtimeDownloadError': '运行时认证文件没有可下载的磁盘文件',
'authFiles.downloaded': '认证文件已下载',
'common.copyFailed': '复制失败',
'authFiles.title': '认证文件',
'authFiles.summary': '{files} 个文件 · {disabled} 个停用',
@@ -375,7 +373,7 @@ export const zhCN = {
'authFiles.models.settings': '设置开放模型',
'authFiles.models.button': '模型',
'authFiles.copyName': '复制文件名',
'common.download': '下载',
'authFiles.openDirectory': '打开凭证所在文件夹',
'common.enable': '启用',
'common.disable': '停用',
'authFiles.runtimeFootnote': '{count} 个凭据来自运行时存储,只能在对应提供商或认证源中管理。',
@@ -412,10 +410,6 @@ export const zhCN = {
'agents.model.search': '搜索模型名称或别名',
'agents.model.clearSearch': '清空搜索',
'agents.model.refresh': '刷新模型',
'agents.catalog.update': '更新模型配置信息',
'agents.catalog.updating': '正在更新模型配置',
'agents.catalog.updated': '模型配置已从 GitHub 更新',
'agents.catalog.unchanged': '模型配置已是最新版本',
'agents.model.fetching': '正在获取模型',
'agents.model.noMatch': '没有匹配的模型',
'agents.model.unavailable': '暂时没有可用模型',
@@ -713,6 +707,13 @@ export const zhCN = {
'appUpdate.available': '发现新版本 {version}',
'appUpdate.check': '检查更新',
'appUpdate.checking': '正在检查',
'appUpdate.catalog.label': 'Codex 模型目录',
'appUpdate.catalog.description': '启动时自动从 GitHub 静默同步',
'appUpdate.catalog.update': '同步模型目录',
'appUpdate.catalog.updateHint': '从 GitHub 检查并更新 Codex 模型目录',
'appUpdate.catalog.updating': '正在同步模型目录',
'appUpdate.catalog.updated': '模型目录已从 GitHub 同步',
'appUpdate.catalog.unchanged': '模型目录已是最新版本',
'appUpdate.installNow': '立即升级',
'appUpdate.openRelease': '打开下载页面',
'appUpdate.confirmTitle': '安装软件更新',
@@ -911,6 +912,7 @@ export const en: Record<MessageKey, string> = {
'quota.confirm.warning': 'The reset runs only after you confirm. Canceling will not consume a reset credit.',
'oauth.title': 'OAuth Sign-In',
'oauth.loginSuccess': '{provider} sign-in succeeded',
'oauth.priorityApplyFailed': 'OAuth sign-in succeeded, but the new credential priority could not be set to 0: {error}',
'oauth.authFailed': 'Authentication failed',
'oauth.loginFailed': '{provider} sign-in failed{detail}',
'oauth.missingState': 'The core did not return an OAuth state',
@@ -1055,18 +1057,10 @@ export const en: Record<MessageKey, string> = {
'aliases.created': 'Created {alias} with reasoning effort fixed to {effort}',
'aliases.deleteConfirm': 'Delete the model alias “{alias}”?\nIts reasoning-effort override will also be removed.',
'aliases.deleted': 'Deleted {alias}',
'aliases.title': 'Reasoning Aliases',
'aliases.intro.title': 'Add a custom alias with a fixed reasoning effort to an original model',
'aliases.intro.description': 'Choose the alias name. CPA injects the selected reasoning effort only when a client calls that alias. Calls to the original model keep their existing name, configuration, and behavior.',
'aliases.intro.badge': 'Original model is unchanged',
'aliases.flow.aria': 'Reasoning alias workflow',
'aliases.flow.original': 'Original model',
'aliases.flow.direct': 'Direct call',
'aliases.flow.directValue': 'deepseek-v4-pro · unchanged',
'aliases.flow.alias': 'Alias call',
'aliases.flow.aliasValue': 'deepseek-v4-pro-max · fixed max effort',
'aliases.create.title': 'Create Reasoning Alias',
'aliases.create.description': 'Supports Codex OAuth, Codex API, and OpenAI-compatible connections',
'aliases.modelAlias.title': 'Model Alias',
'aliases.modelAlias.description': 'Enter the client-facing name and confirm the final mapping',
'aliases.originalModel': 'Original Model',
'aliases.loadingModels': 'Loading models',
'aliases.searchModel': 'Search and select an original model',
@@ -1187,6 +1181,13 @@ export const en: Record<MessageKey, string> = {
'authFiles.quota.expiry': 'Earliest expiry {time}',
'authFiles.models.noneForProvider': 'This provider has no configurable model definitions',
'authFiles.models.updated': 'Open models updated for {provider}',
'authFiles.priority.button': 'Priority {priority}',
'authFiles.priority.title': 'Edit Credential Priority',
'authFiles.priority.label': 'Priority',
'authFiles.priority.placeholder': 'For example, 10 or -1',
'authFiles.priority.hint': 'Integers only. Larger values have higher priority. Leave blank or enter 0 to restore the default.',
'authFiles.priority.invalid': 'Enter a valid integer priority',
'authFiles.priority.updated': 'Priority updated for {name}',
'authFiles.uploaded': 'Uploaded {count} authentication files',
'authFiles.uploadFailed': '{count} files failed to upload: {errors}',
'authFiles.notice.enabled': 'Authentication file enabled',
@@ -1196,8 +1197,6 @@ export const en: Record<MessageKey, string> = {
'authFiles.deleted': 'Authentication file deleted',
'authFiles.deleteAllConfirm': 'Delete all on-disk authentication files? Runtime credentials will not be deleted.',
'authFiles.deletedAll': 'All on-disk authentication files deleted',
'authFiles.runtimeDownloadError': 'This runtime authentication file has no downloadable disk file',
'authFiles.downloaded': 'Authentication file downloaded',
'common.copyFailed': 'Copy failed',
'authFiles.title': 'Authentication Files',
'authFiles.summary': '{files} files · {disabled} disabled',
@@ -1223,7 +1222,7 @@ export const en: Record<MessageKey, string> = {
'authFiles.models.settings': 'Configure open models',
'authFiles.models.button': 'Models',
'authFiles.copyName': 'Copy file name',
'common.download': 'Download',
'authFiles.openDirectory': 'Open credential folder',
'common.enable': 'Enable',
'common.disable': 'Disable',
'authFiles.runtimeFootnote': '{count} credentials come from runtime storage and can only be managed through their provider or authentication source.',
@@ -1260,10 +1259,6 @@ export const en: Record<MessageKey, string> = {
'agents.model.search': 'Search model names or aliases',
'agents.model.clearSearch': 'Clear search',
'agents.model.refresh': 'Refresh models',
'agents.catalog.update': 'Update model configuration',
'agents.catalog.updating': 'Updating model configuration',
'agents.catalog.updated': 'Model configuration updated from GitHub',
'agents.catalog.unchanged': 'Model configuration is already up to date',
'agents.model.fetching': 'Fetching models',
'agents.model.noMatch': 'No matching models',
'agents.model.unavailable': 'No models are currently available',
@@ -1561,6 +1556,13 @@ export const en: Record<MessageKey, string> = {
'appUpdate.available': 'Version {version} is available',
'appUpdate.check': 'Check for Updates',
'appUpdate.checking': 'Checking',
'appUpdate.catalog.label': 'Codex Model Catalog',
'appUpdate.catalog.description': 'Silently syncs from GitHub when the app starts',
'appUpdate.catalog.update': 'Sync Model Catalog',
'appUpdate.catalog.updateHint': 'Check GitHub and update the Codex model catalog',
'appUpdate.catalog.updating': 'Syncing Model Catalog',
'appUpdate.catalog.updated': 'Model catalog synced from GitHub',
'appUpdate.catalog.unchanged': 'Model catalog is already up to date',
'appUpdate.installNow': 'Update Now',
'appUpdate.openRelease': 'Open Download Page',
'appUpdate.confirmTitle': 'Install Application Update',
+1 -51
View File
@@ -85,10 +85,6 @@ type AgentConfigActionResult = {
conflictFiles: string[];
};
type CodexModelCatalogUpdateResult = {
outcome: 'updated' | 'unchanged';
};
type ChatGptCloseResult = {
wasRunning: boolean;
closedProcesses: number;
@@ -488,13 +484,11 @@ export function AgentsPage() {
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' | 'catalog' | null>(null);
const [busyAction, setBusyAction] = useState<'apply' | 'default' | 'clear' | 'launch' | 'close-app' | null>(null);
const busy = busyAction !== null;
const [detectionError, setDetectionError] = useState('');
const [modelError, setModelError] = useState('');
const [modelSelectionError, setModelSelectionError] = useState('');
const [catalogUpdateError, setCatalogUpdateError] = useState('');
const [catalogUpdateNotice, setCatalogUpdateNotice] = useState('');
const [configurationError, setConfigurationError] = useState('');
const [launchError, setLaunchError] = useState('');
const [defaultError, setDefaultError] = useState('');
@@ -592,8 +586,6 @@ export function AgentsPage() {
useEffect(() => {
setActiveSubpage(DEFAULT_AGENT_SUBPAGE);
setModelSelectionError('');
setCatalogUpdateError('');
setCatalogUpdateNotice('');
setConfigurationError('');
setLaunchError('');
setDefaultError('');
@@ -688,23 +680,6 @@ export function AgentsPage() {
void loadModels(selected);
};
const updateCodexModelCatalog = async () => {
setCatalogUpdateError('');
setCatalogUpdateNotice('');
setBusyAction('catalog');
try {
const result = await invoke<CodexModelCatalogUpdateResult>('update_codex_model_catalog');
setCatalogUpdateNotice(result.outcome === 'updated'
? t('agents.catalog.updated')
: t('agents.catalog.unchanged'));
await loadModels('codex');
} catch (requestError) {
setCatalogUpdateError(String(requestError));
} finally {
setBusyAction(null);
}
};
const reloadStatusesAfterAction = async () => {
setDetectionError('');
try {
@@ -989,31 +964,6 @@ export function AgentsPage() {
{modelHint}
</span>
) : null}
{selected === 'codex' ? (
<div className="agent-catalog-update-control">
<button
type="button"
className="secondary-button compact-button"
onClick={() => void updateCodexModelCatalog()}
disabled={busy}
>
<RefreshCw size={15} className={busyAction === 'catalog' ? 'spin' : ''} />
{busyAction === 'catalog'
? t('agents.catalog.updating')
: t('agents.catalog.update')}
</button>
{catalogUpdateError ? (
<span className="agent-inline-message error" role="alert" aria-live="polite">
{catalogUpdateError}
</span>
) : null}
{catalogUpdateNotice ? (
<span className="agent-inline-message" role="status" aria-live="polite">
{catalogUpdateNotice}
</span>
) : null}
</div>
) : null}
</section>
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
+107 -16
View File
@@ -2,9 +2,10 @@ import { ChangeEvent, useCallback, useEffect, useMemo, useRef, useState } from '
import {
Check,
Copy,
Download,
FileDown,
FolderOpen,
LoaderCircle,
Pencil,
RefreshCw,
Search,
Trash2,
@@ -45,6 +46,8 @@ import {
authFileName,
dedupeAuthFiles,
isRuntimeOnlyAuthFile,
normalizeAuthFilePriorityInput,
parseAuthFilePriority,
} from '../services/authFiles';
import {
exclusionsForOpenOAuthModels,
@@ -57,6 +60,13 @@ import { getCurrentLocale, translate, useI18n } from '../i18n';
type AuthFile = Record<string, unknown>;
type PriorityEditor = {
fileName: string;
originalPriority: number;
value: string;
error: string;
};
const providerIcons: Record<string, string> = {
antigravity: antigravityIcon,
claude: claudeIcon,
@@ -144,6 +154,7 @@ export function AuthFileManagementPage() {
const [error, setError] = useState('');
const [notice, setNotice] = useState('');
const [copied, setCopied] = useState('');
const [priorityEditor, setPriorityEditor] = useState<PriorityEditor | null>(null);
const [oauthModelProvider, setOauthModelProvider] = useState('');
const [oauthModelProviderLabel, setOauthModelProviderLabel] = useState('');
const [oauthModels, setOauthModels] = useState<OAuthModelDefinition[]>([]);
@@ -361,6 +372,51 @@ export function AuthFileManagementPage() {
}
};
const openPriorityEditor = (file: AuthFile) => {
const priority = parseAuthFilePriority(file.priority);
setPriorityEditor({
fileName: fileName(file),
originalPriority: priority ?? 0,
value: priority === undefined || priority === 0 ? '' : String(priority),
error: '',
});
};
const closePriorityEditor = () => {
if (!busy) setPriorityEditor(null);
};
const savePriority = async () => {
if (!priorityEditor || busy) return;
const priority = normalizeAuthFilePriorityInput(priorityEditor.value);
if (priority === null) {
setPriorityEditor((current) => current
? { ...current, error: t('authFiles.priority.invalid') }
: current);
return;
}
if (priority === priorityEditor.originalPriority) {
setPriorityEditor(null);
return;
}
const name = priorityEditor.fileName;
setBusy(true);
setError('');
try {
await managementApi.patch('/auth-files/fields', { name, priority });
setPriorityEditor(null);
setNotice(t('authFiles.priority.updated', { name }));
await loadFiles();
} catch (requestError) {
setPriorityEditor((current) => current
? { ...current, error: String(requestError) }
: current);
} finally {
setBusy(false);
}
};
const deleteFile = async (file: AuthFile) => {
const name = fileName(file);
if (isRuntimeOnly(file)) {
@@ -396,23 +452,11 @@ export function AuthFileManagementPage() {
}
};
const downloadFile = async (file: AuthFile) => {
const name = fileName(file);
if (isRuntimeOnly(file)) {
setError(t('authFiles.runtimeDownloadError'));
return;
}
const openAuthFilesDirectory = async () => {
setBusy(true);
setError('');
try {
const content = await managementApi.downloadAuthFile(name);
const url = URL.createObjectURL(new Blob([content], { type: 'application/json' }));
const anchor = document.createElement('a');
anchor.href = url;
anchor.download = name;
anchor.click();
URL.revokeObjectURL(url);
setNotice(t('authFiles.downloaded'));
await managementApi.openAuthFilesDirectory();
} catch (requestError) {
setError(String(requestError));
} finally {
@@ -485,6 +529,7 @@ export function AuthFileManagementPage() {
const name = fileName(file);
const icon = providerIcons[providerKey(file)] ?? geminiIcon;
const disabled = readBoolean(file, 'disabled');
const priority = parseAuthFilePriority(file.priority) ?? 0;
return (
<article className={`real-auth-file-row ${disabled ? 'disabled' : ''}`} key={`${name}-${readString(file, 'auth_index', 'authIndex')}`}>
<img src={icon} alt="" className="provider-logo" />
@@ -500,8 +545,9 @@ export function AuthFileManagementPage() {
<div className="auth-file-actions">
{quotaProviderForFile(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void refreshQuota(file)} disabled={busy || disabled || quotas[quotaKey(file)]?.status === 'loading'}>{disabled ? t('authFiles.status.disabled') : quotas[quotaKey(file)]?.status === 'loading' ? t('authFiles.quota.querying') : quotas[quotaKey(file)]?.status === 'success' ? t('authFiles.quota.refresh') : t('authFiles.quota.fetch')}</button> : null}
{providerKey(file) ? <button type="button" className="secondary-button compact-button" onClick={() => void openOauthModels(file)} disabled={busy} title={t('authFiles.models.settings')}>{t('authFiles.models.button')}</button> : null}
<button type="button" className="secondary-button compact-button auth-file-priority-button" onClick={() => openPriorityEditor(file)} disabled={busy} title={t('authFiles.priority.hint')}><Pencil size={14} />{t('authFiles.priority.button', { priority })}</button>
<button type="button" className="icon-button quiet" onClick={() => void copyName(name)} disabled={busy} title={t('authFiles.copyName')}>{copied === name ? <Check size={16} /> : <Copy size={16} />}</button>
<button type="button" className="icon-button quiet" onClick={() => void downloadFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.download')}><Download size={16} /></button>
<button type="button" className="icon-button quiet" onClick={() => void openAuthFilesDirectory()} disabled={busy} title={t('authFiles.openDirectory')}><FolderOpen size={16} /></button>
<button type="button" className="secondary-button compact-button" onClick={() => void toggleStatus(file)} disabled={busy}>{disabled ? t('common.enable') : t('common.disable')}</button>
<button type="button" className="icon-button danger" onClick={() => void deleteFile(file)} disabled={busy || isRuntimeOnly(file)} title={t('common.delete')}><Trash2 size={16} /></button>
</div>
@@ -514,6 +560,51 @@ export function AuthFileManagementPage() {
</section>
{runtimeCount > 0 ? <p className="page-footnote">{t('authFiles.runtimeFootnote', { count: runtimeCount })}</p> : null}
{priorityEditor ? (
<div className="config-dialog-backdrop" onMouseDown={(event) => event.currentTarget === event.target && closePriorityEditor()}>
<form
className="config-dialog auth-priority-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="auth-priority-title"
onSubmit={(event) => {
event.preventDefault();
void savePriority();
}}
>
<div className="config-dialog-heading">
<div><Pencil size={19} /><h2 id="auth-priority-title">{t('authFiles.priority.title')}</h2></div>
<button type="button" className="icon-button quiet" onClick={closePriorityEditor} disabled={busy} title={t('common.close')}><X size={18} /></button>
</div>
<div className="auth-priority-target" title={priorityEditor.fileName}>{priorityEditor.fileName}</div>
<label className="auth-priority-field" htmlFor="auth-file-priority-input">
<span>{t('authFiles.priority.label')}</span>
<input
id="auth-file-priority-input"
autoFocus
type="text"
inputMode="numeric"
value={priorityEditor.value}
placeholder={t('authFiles.priority.placeholder')}
disabled={busy}
aria-invalid={Boolean(priorityEditor.error)}
onChange={(event) => setPriorityEditor((current) => current
? { ...current, value: event.currentTarget.value, error: '' }
: current)}
/>
<small>{t('authFiles.priority.hint')}</small>
</label>
<div className={`config-form-message ${priorityEditor.error ? 'error' : ''}`} role={priorityEditor.error ? 'alert' : undefined}>
{priorityEditor.error || ' '}
</div>
<div className="config-dialog-actions two-actions">
<button type="button" className="secondary-button" onClick={closePriorityEditor} disabled={busy}>{t('common.cancel')}</button>
<button type="submit" className="primary-button" disabled={busy}>{busy ? <LoaderCircle size={16} className="spin" /> : <Check size={16} />}{busy ? t('common.saving') : t('common.save')}</button>
</div>
</form>
</div>
) : null}
{oauthModelProvider ? (
<div className="model-discovery-backdrop" onMouseDown={(event) => event.currentTarget === event.target && !oauthModelSaving && closeOauthModels()}>
<section className="model-discovery-dialog" role="dialog" aria-modal="true" aria-labelledby="oauth-model-title">
+46
View File
@@ -48,6 +48,10 @@ type CoreInstallTask = {
result: CoreInstallResult | null;
};
type CodexModelCatalogUpdateResult = {
outcome: 'updated' | 'unchanged';
};
type MessageType = 'info' | 'success' | 'error';
type CoreProcessCommand = 'start_core_process' | 'stop_core_process' | 'restart_core_process';
@@ -131,6 +135,9 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
} | null>(null);
const [message, setMessage] = useState('');
const [messageType, setMessageType] = useState<MessageType>('info');
const [catalogUpdating, setCatalogUpdating] = useState(false);
const [catalogUpdateError, setCatalogUpdateError] = useState('');
const [catalogUpdateNotice, setCatalogUpdateNotice] = useState('');
const [progress, setProgress] = useState<CoreInstallTask | null>(null);
const [installDialogOpen, setInstallDialogOpen] = useState(false);
const [cancellingInstall, setCancellingInstall] = useState(false);
@@ -551,6 +558,22 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
}
};
const updateCodexModelCatalog = async () => {
setCatalogUpdating(true);
setCatalogUpdateError('');
setCatalogUpdateNotice('');
try {
const result = await invoke<CodexModelCatalogUpdateResult>('update_codex_model_catalog');
setCatalogUpdateNotice(result.outcome === 'updated'
? t('appUpdate.catalog.updated')
: t('appUpdate.catalog.unchanged'));
} catch (error) {
setCatalogUpdateError(String(error));
} finally {
setCatalogUpdating(false);
}
};
const latestVersion = latest?.version ?? '';
const currentVersion = coreStatus?.currentVersion ?? '';
const coreInstalled = Boolean(coreStatus?.installed);
@@ -779,6 +802,18 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
: t('appUpdate.phase.checking')}
</dd>
</div>
<div className="panel-detail-row">
<dt>{t('appUpdate.catalog.label')}</dt>
<dd
className={catalogUpdateError ? 'error' : catalogUpdateNotice ? 'success' : ''}
role={catalogUpdateError ? 'alert' : undefined}
aria-live="polite"
>
{catalogUpdateError
|| catalogUpdateNotice
|| t('appUpdate.catalog.description')}
</dd>
</div>
</dl>
<div className="button-row panel-action-row software-update-actions">
@@ -804,6 +839,17 @@ export function KernelPage({ view = 'home' }: { view?: KernelView }) {
{t('appUpdate.openRelease')} <ExternalLink size={14} aria-hidden="true" />
</button>
)}
<button
type="button"
className="secondary-button"
disabled={catalogUpdating || appUpdateTask.running}
onClick={() => void updateCodexModelCatalog()}
title={t('appUpdate.catalog.updateHint')}
>
{catalogUpdating
? t('appUpdate.catalog.updating')
: t('appUpdate.catalog.update')}
</button>
</div>
</div>
) : null}
+41 -2
View File
@@ -13,6 +13,12 @@ import codexIcon from '../assets/icons/codex.svg';
import grokIcon from '../assets/icons/grok.svg';
import kimiIcon from '../assets/icons/kimi-light.svg';
import { useI18n } from '../i18n';
import {
changedOAuthAuthFileNames,
snapshotAuthFiles,
type AuthFileSnapshot,
} from '../services/authFiles';
import { managementApi, responseList } from '../services/managementApi';
type OAuthProviderId = 'codex' | 'claude' | 'antigravity' | 'kimi' | 'xai';
type OAuthFlowStatus = 'idle' | 'waiting' | 'success' | 'error';
@@ -69,6 +75,7 @@ export function OAuthLoginPage() {
const pollingTimers = useRef<Partial<Record<OAuthProviderId, number>>>({});
const pollingRequests = useRef<Partial<Record<OAuthProviderId, boolean>>>({});
const successResetTimers = useRef<Partial<Record<OAuthProviderId, number>>>({});
const credentialSnapshots = useRef<Partial<Record<OAuthProviderId, AuthFileSnapshot>>>({});
const noticeTimerRef = useRef<number | null>(null);
const showNotice = useCallback((message: string, tone: 'success' | 'error' | 'info') => {
@@ -118,6 +125,7 @@ export function OAuthLoginPage() {
const resetProviderAttempt = useCallback(
(provider: OAuthProviderId) => {
clearProviderTimers(provider);
delete credentialSnapshots.current[provider];
setStates((current) => ({ ...current, [provider]: { status: 'idle' } }));
},
[clearProviderTimers],
@@ -144,6 +152,24 @@ export function OAuthLoginPage() {
[clearProviderTimers, resetProviderAttempt, updateProviderState],
);
const captureCredentialSnapshot = useCallback(async (provider: OAuthProviderId) => {
const payload = await managementApi.get('/auth-files');
credentialSnapshots.current[provider] = snapshotAuthFiles(responseList(payload, 'files'));
}, []);
const applyDefaultCredentialPriority = useCallback(async (provider: OAuthProviderId) => {
const before = credentialSnapshots.current[provider];
delete credentialSnapshots.current[provider];
if (!before) return;
const payload = await managementApi.get('/auth-files');
const names = changedOAuthAuthFileNames(before, responseList(payload, 'files'), provider);
await Promise.all(names.map((name) => managementApi.patch('/auth-files/fields', {
name,
priority: 0,
})));
}, []);
const startPolling = useCallback(
(provider: OAuthProviderId, state: string) => {
clearPollingTimer(provider);
@@ -154,8 +180,19 @@ export function OAuthLoginPage() {
const result = await invoke<OAuthStatusResult>('get_oauth_status', { state });
const status = (result.status || '').toLowerCase();
if (status === 'ok') {
let priorityError = '';
try {
await applyDefaultCredentialPriority(provider);
} catch (error) {
priorityError = String(error);
}
completeProviderAuth(provider);
showNotice(t('oauth.loginSuccess', { provider: providerLabel(provider) }), 'success');
showNotice(
priorityError
? t('oauth.priorityApplyFailed', { error: priorityError })
: t('oauth.loginSuccess', { provider: providerLabel(provider) }),
priorityError ? 'error' : 'success',
);
} else if (status === 'error') {
updateProviderState(provider, {
status: 'error',
@@ -188,7 +225,7 @@ export function OAuthLoginPage() {
OAUTH_POLL_INTERVAL_MS,
);
},
[clearPollingTimer, completeProviderAuth, showNotice, t, updateProviderState],
[applyDefaultCredentialPriority, clearPollingTimer, completeProviderAuth, showNotice, t, updateProviderState],
);
useEffect(() => {
@@ -217,6 +254,7 @@ export function OAuthLoginPage() {
});
try {
await captureCredentialSnapshot(provider);
const result = await invoke<OAuthStartResult>('start_oauth_login', { provider });
if (!result.state) {
updateProviderState(provider, {
@@ -247,6 +285,7 @@ export function OAuthLoginPage() {
);
}
} catch (error) {
delete credentialSnapshots.current[provider];
updateProviderState(provider, {
status: 'error',
error: String(error),
+55 -67
View File
@@ -248,46 +248,23 @@ export function ThinkingAliasesPage() {
return (
<section className="page management-page thinking-alias-page">
<header className="management-header">
<div>
<span>Model Alias</span>
<h1>{t('aliases.title')}</h1>
</div>
</header>
<div className="thinking-alias-feedback" 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="thinking-alias-guide">
<BrainCircuit size={20} />
<div className="thinking-alias-guide-copy">
<strong>{t('aliases.intro.title')}</strong>
<p>{t('aliases.intro.description')}</p>
<span>{t('aliases.intro.badge')}</span>
</div>
<div className="thinking-alias-flow" aria-label={t('aliases.flow.aria')}>
<span><small>{t('aliases.flow.original')}</small><code>deepseek-v4-pro</code></span>
<ArrowRight size={14} />
<div>
<span><small>{t('aliases.flow.direct')}</small><code>{t('aliases.flow.directValue')}</code></span>
<span><small>{t('aliases.flow.alias')}</small><code>{t('aliases.flow.aliasValue')}</code></span>
</div>
</div>
</div>
<div className="thinking-alias-workbench">
<section className="panel thinking-alias-builder">
<div className="thinking-alias-panel-heading">
<span><GitFork size={18} /></span>
<div>
<h2>{t('aliases.create.title')}</h2>
<p>{t('aliases.create.description')}</p>
<div className="thinking-alias-left-stack">
<section className="panel thinking-alias-builder">
<div className="thinking-alias-panel-heading">
<span><GitFork size={18} /></span>
<div>
<h2>{t('aliases.create.title')}</h2>
<p>{t('aliases.create.description')}</p>
</div>
</div>
</div>
<div className="thinking-alias-field thinking-model-field">
<div className="thinking-alias-field thinking-model-field">
<label htmlFor="thinking-model-search">{t('aliases.originalModel')}</label>
<div className="thinking-model-picker" ref={modelPickerRef}>
<div className="thinking-model-search">
@@ -379,9 +356,9 @@ export function ThinkingAliasesPage() {
) : (
<small className="thinking-model-hint">{t('aliases.sourceHint')}</small>
)}
</div>
</div>
<div className="thinking-alias-field">
<div className="thinking-alias-field">
<div className="thinking-field-heading">
<strong>{t('aliases.effort.title')}</strong>
<span>{t('aliases.effort.description')}</span>
@@ -439,43 +416,54 @@ export function ThinkingAliasesPage() {
</button>
<small>{t('aliases.customHelp')}</small>
</div> : null}
</div>
<div className="thinking-alias-field">
<div className="thinking-field-heading">
<strong>{t('aliases.aliasName.title')}</strong>
<span>{t('aliases.aliasName.description')}</span>
</div>
<input
id="thinking-alias-name"
className="thinking-alias-input"
value={alias}
onChange={(event) => setAlias(event.currentTarget.value)}
placeholder={selectedSource
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
: t('aliases.aliasName.selectFirst')}
disabled={Boolean(busyAlias)}
/>
</div>
</section>
<div className="thinking-alias-preview">
<BrainCircuit size={18} />
<div>
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
<code>{selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = {normalizedEffort || t('aliases.notSet')}</code>
<section className="panel thinking-alias-name-panel">
<div className="thinking-alias-panel-heading">
<span><BrainCircuit size={18} /></span>
<div>
<h2>{t('aliases.modelAlias.title')}</h2>
<p>{t('aliases.modelAlias.description')}</p>
</div>
</div>
</div>
<button
type="button"
className="primary-button thinking-alias-create"
onClick={() => void createAlias()}
disabled={loading || Boolean(busyAlias) || !selectedSource || !effort.trim() || !alias.trim()}
>
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <GitFork size={16} />}
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
</button>
</section>
<div className="thinking-alias-field">
<div className="thinking-field-heading">
<strong>{t('aliases.aliasName.title')}</strong>
<span>{t('aliases.aliasName.description')}</span>
</div>
<input
id="thinking-alias-name"
className="thinking-alias-input"
value={alias}
onChange={(event) => setAlias(event.currentTarget.value)}
placeholder={selectedSource
? t('aliases.aliasName.example', { model: selectedSource.model, effort: normalizedEffort || 'high' })
: t('aliases.aliasName.selectFirst')}
disabled={Boolean(busyAlias)}
/>
</div>
<div className="thinking-alias-preview">
<BrainCircuit size={18} />
<div>
<span>{selectedSource?.model || t('aliases.notSelected')} <ArrowRight size={13} /> {alias || t('aliases.enterAlias')}</span>
<code>{selectedSource?.protocol === 'openai' ? 'reasoning_effort' : 'reasoning.effort'} = {normalizedEffort || t('aliases.notSet')}</code>
</div>
</div>
<button
type="button"
className="primary-button thinking-alias-create"
onClick={() => void createAlias()}
disabled={loading || Boolean(busyAlias) || !selectedSource || !effort.trim() || !alias.trim()}
>
{busyAction === 'create' ? <LoaderCircle size={16} className="spin" /> : <GitFork size={16} />}
{busyAction === 'create' ? t('aliases.creating') : t('aliases.create')}
</button>
</section>
</div>
<section className="panel thinking-alias-list-panel">
<div className="thinking-alias-list-heading">
+61
View File
@@ -2,6 +2,7 @@ import { readBoolean, readString } from './managementApi';
import { getCurrentLocale, translate } from '../i18n';
export type AuthFileRecord = Record<string, unknown>;
export type AuthFileSnapshot = Map<string, string>;
export const authFileName = (file: AuthFileRecord) =>
readString(file, 'name') || translate(getCurrentLocale(), 'authFiles.unnamed');
@@ -9,6 +10,66 @@ export const authFileName = (file: AuthFileRecord) =>
export const isRuntimeOnlyAuthFile = (file: AuthFileRecord) =>
readBoolean(file, 'runtime_only', 'runtimeOnly');
export const parseAuthFilePriority = (value: unknown): number | undefined => {
if (typeof value === 'number') {
return Number.isSafeInteger(value) ? value : undefined;
}
if (typeof value !== 'string') return undefined;
const normalized = value.trim();
if (!/^-?\d+$/.test(normalized)) return undefined;
const priority = Number(normalized);
return Number.isSafeInteger(priority) ? priority : undefined;
};
export const normalizeAuthFilePriorityInput = (value: string): number | null => {
const normalized = value.trim();
if (!normalized) return 0;
return parseAuthFilePriority(normalized) ?? null;
};
const normalizeOAuthProvider = (value: string) => {
const provider = value.trim().toLowerCase();
if (provider === 'anthropic') return 'claude';
if (provider === 'openai') return 'codex';
return provider;
};
const authFileProvider = (file: AuthFileRecord) =>
normalizeOAuthProvider(readString(file, 'provider', 'type'));
export const snapshotAuthFiles = (files: AuthFileRecord[]): AuthFileSnapshot => {
const grouped = new Map<string, string[]>();
files.forEach((file) => {
const name = readString(file, 'name');
if (!name) return;
const fingerprints = grouped.get(name) ?? [];
fingerprints.push(JSON.stringify(file));
grouped.set(name, fingerprints);
});
return new Map(Array.from(grouped, ([name, fingerprints]) => [
name,
fingerprints.sort().join('\n'),
]));
};
export const changedOAuthAuthFileNames = (
before: AuthFileSnapshot,
files: AuthFileRecord[],
provider: string,
) => {
const expectedProvider = normalizeOAuthProvider(provider);
const after = snapshotAuthFiles(files);
const names = new Set<string>();
files.forEach((file) => {
const name = readString(file, 'name');
if (!name || authFileProvider(file) !== expectedProvider) return;
const priority = parseAuthFilePriority(file.priority);
if (priority !== undefined && priority !== 0) return;
if (before.get(name) !== after.get(name)) names.add(name);
});
return Array.from(names);
};
const hasMeaningfulValue = (value: unknown) => {
if (value === null || value === undefined) return false;
if (typeof value === 'string') return value.trim().length > 0;
+1 -1
View File
@@ -58,7 +58,7 @@ export const managementApi = {
data,
});
},
downloadAuthFile: (name: string) => invoke<string>('download_auth_file', { name }),
openAuthFilesDirectory: () => invoke<void>('open_auth_files_directory'),
};
export function isRecord(value: unknown): value is Record<string, unknown> {
+6 -41
View File
@@ -540,24 +540,8 @@ export const quotaRowsFor = (provider: QuotaProvider, payload: unknown): QuotaRo
});
};
const resolveProjectId = async (file: AuthFile): Promise<string> => {
const direct = readString(file, 'project_id', 'projectId');
if (direct) return direct;
try {
const content = await managementApi.downloadAuthFile(fileName(file));
const parsed = JSON.parse(content) as Record<string, unknown>;
const direct = readString(parsed, 'project_id', 'projectId');
if (direct) return direct;
for (const container of [parsed.installed, parsed.web]) {
if (!isRecord(container)) continue;
const nested = readString(container, 'project_id', 'projectId');
if (nested) return nested;
}
return '';
} catch {
return '';
}
};
const resolveProjectId = (file: AuthFile): string =>
readString(file, 'project_id', 'projectId');
const decodeJwtPayload = (value: unknown): Record<string, unknown> | null => {
if (isRecord(value)) return value;
@@ -614,18 +598,8 @@ const codexAccountIdFromRecord = (record: Record<string, unknown>): string => {
return '';
};
const resolveCodexAccountId = async (file: AuthFile): Promise<string> => {
const direct = codexAccountIdFromRecord(file);
if (direct) return direct;
try {
const content = await managementApi.downloadAuthFile(fileName(file));
const parsed = JSON.parse(content);
return isRecord(parsed) ? codexAccountIdFromRecord(parsed) : '';
} catch {
return '';
}
};
const resolveCodexAccountId = (file: AuthFile): string =>
codexAccountIdFromRecord(file);
const xaiUserIdFromRecord = (record: Record<string, unknown>): string => {
const nestedRecords = [record, record.metadata, record.attributes]
@@ -642,17 +616,8 @@ const xaiUserIdFromRecord = (record: Record<string, unknown>): string => {
return '';
};
const resolveXaiUserId = async (file: AuthFile): Promise<string> => {
const direct = xaiUserIdFromRecord(file);
if (direct) return direct;
try {
const content = await managementApi.downloadAuthFile(fileName(file));
const parsed = JSON.parse(content);
return isRecord(parsed) ? xaiUserIdFromRecord(parsed) : '';
} catch {
return '';
}
};
const resolveXaiUserId = (file: AuthFile): string =>
xaiUserIdFromRecord(file);
const requestQuotaPayload = async (
authIndex: string,
+144 -166
View File
@@ -858,7 +858,7 @@ input:focus-visible {
grid-template-columns: minmax(0, 1.35fr) minmax(280px, 0.65fr);
align-items: stretch;
gap: 18px;
height: 430px;
height: 350px;
}
.control-panel {
@@ -1493,20 +1493,20 @@ input:focus-visible {
.config-keys-panel {
grid-template-rows: 42px minmax(0, 1fr);
gap: 16px;
height: 430px;
height: 350px;
}
.config-side-stack {
display: grid;
grid-template-rows: repeat(2, 206px);
grid-template-rows: repeat(2, 166px);
gap: 18px;
height: 430px;
height: 350px;
}
.config-setting-panel {
grid-template-rows: 42px minmax(0, 1fr);
gap: 16px;
height: 206px;
height: 166px;
}
.config-network-panel {
@@ -3453,110 +3453,34 @@ input:focus-visible {
min-height: 42px;
}
.thinking-alias-guide {
display: grid;
grid-template-columns: 24px minmax(270px, 1fr) minmax(280px, auto);
align-items: center;
gap: 12px;
min-height: 86px;
padding: 12px 14px;
border: 1px solid var(--theme-d9d4c9);
border-radius: 8px;
background: var(--theme-f8f6f1);
color: var(--theme-655f57);
}
.thinking-alias-guide > svg {
color: var(--theme-654f91);
}
.thinking-alias-guide-copy {
display: grid;
min-width: 0;
gap: 4px;
}
.thinking-alias-guide strong {
color: var(--theme-342f2a);
font-size: 12px;
}
.thinking-alias-guide p {
margin: 0;
color: var(--theme-777067);
font-size: 10px;
line-height: 1.5;
}
.thinking-alias-guide-copy > span {
width: fit-content;
padding: 2px 7px;
border: 1px solid var(--theme-b9d5bf);
border-radius: 999px;
background: var(--theme-eef7f0);
color: var(--theme-2f6b3f);
font-size: 9px;
font-weight: 800;
}
.thinking-alias-guide code {
padding: 6px 9px;
border: 1px solid var(--theme-d8d1eb);
border-radius: 6px;
background: var(--theme-f5f1ff);
color: var(--theme-654f91);
font-size: 10px;
white-space: nowrap;
}
.thinking-alias-flow {
display: grid;
grid-template-columns: auto 14px minmax(0, auto);
align-items: center;
gap: 6px;
}
.thinking-alias-flow > span,
.thinking-alias-flow > div > span {
display: grid;
gap: 3px;
}
.thinking-alias-flow > div {
display: grid;
gap: 5px;
}
.thinking-alias-flow > div > span {
grid-template-columns: 54px minmax(0, auto);
align-items: center;
}
.thinking-alias-flow small {
color: var(--theme-8a847c);
font-size: 8px;
font-weight: 700;
text-align: center;
}
.thinking-alias-flow > svg {
color: var(--theme-aaa49b);
}
.thinking-alias-workbench {
display: grid;
grid-template-columns: minmax(330px, 390px) minmax(0, 1fr);
grid-template-columns: minmax(480px, 1.4fr) minmax(320px, 0.6fr);
gap: 16px;
align-items: stretch;
align-items: start;
}
.thinking-alias-builder,
.thinking-alias-list-panel {
height: clamp(420px, calc(100vh - 270px), 580px);
min-height: 420px;
.thinking-alias-left-stack {
display: grid;
min-width: 0;
gap: 16px;
}
.thinking-alias-builder {
min-height: 330px;
}
.thinking-alias-name-panel {
min-height: 250px;
}
.thinking-alias-list-panel {
height: clamp(360px, calc(100vh - 330px), 460px);
min-height: 360px;
}
.thinking-alias-builder,
.thinking-alias-name-panel {
position: relative;
z-index: 2;
display: flex;
@@ -3596,14 +3520,15 @@ input:focus-visible {
.thinking-alias-list-heading h2 {
margin: 0;
color: var(--theme-2d2a26);
font-size: 15px;
font-size: 17px;
}
.thinking-alias-panel-heading p,
.thinking-alias-list-heading span {
margin: 0;
color: var(--theme-8a847c);
font-size: 10px;
font-size: 14px;
line-height: 1.45;
}
.thinking-alias-field {
@@ -3619,7 +3544,7 @@ input:focus-visible {
.thinking-alias-field > label {
color: var(--theme-6f6961);
font-size: 11px;
font-size: 14px;
font-weight: 700;
}
@@ -3632,13 +3557,13 @@ input:focus-visible {
.thinking-field-heading strong {
color: var(--theme-6f6961);
font-size: 11px;
font-size: 14px;
}
.thinking-field-heading span {
overflow: hidden;
color: var(--theme-918a81);
font-size: 9px;
font-size: 13px;
text-overflow: ellipsis;
white-space: nowrap;
}
@@ -3669,7 +3594,7 @@ input:focus-visible {
outline: 0;
background: transparent;
color: var(--theme-2d2a26);
font-size: 12px;
font-size: 14px;
}
.thinking-source-kind {
@@ -3680,7 +3605,7 @@ input:focus-visible {
border-radius: 999px;
background: var(--theme-f5f1ff);
color: var(--theme-654f91);
font-size: 8px;
font-size: 11px;
font-weight: 800;
text-overflow: ellipsis;
white-space: nowrap;
@@ -3749,12 +3674,12 @@ input:focus-visible {
}
.thinking-model-option-copy strong {
font-size: 11px;
font-size: 15px;
}
.thinking-model-option-copy > span:first-child small {
color: var(--theme-8a847c);
font-size: 9px;
font-size: 13px;
}
.thinking-model-source {
@@ -3770,14 +3695,14 @@ input:focus-visible {
border-radius: 4px;
background: var(--theme-eeece6);
color: var(--theme-5f5a53);
font-size: 8px;
font-size: 11px;
font-style: normal;
font-weight: 800;
}
.thinking-model-source small {
color: var(--theme-8a847c);
font-size: 9px;
font-size: 13px;
}
.thinking-model-list > button > svg {
@@ -3791,7 +3716,7 @@ input:focus-visible {
justify-content: center;
gap: 7px;
color: var(--theme-8a847c);
font-size: 11px;
font-size: 13px;
}
.thinking-model-selection {
@@ -3800,7 +3725,7 @@ input:focus-visible {
align-items: center;
gap: 6px;
color: var(--theme-777067);
font-size: 9px;
font-size: 13px;
}
.thinking-model-selection > span {
@@ -3822,8 +3747,8 @@ input:focus-visible {
.thinking-model-hint {
color: var(--theme-918a81);
font-size: 9px;
line-height: 1.4;
font-size: 13px;
line-height: 1.5;
}
.thinking-effort-options {
@@ -3839,7 +3764,7 @@ input:focus-visible {
border-radius: 6px;
background: var(--theme-fff);
color: var(--theme-7a746c);
font-size: 10px;
font-size: 13px;
font-weight: 700;
}
@@ -3876,7 +3801,7 @@ input:focus-visible {
outline: 0;
background: var(--theme-fff);
color: var(--theme-2d2a26);
font-size: 11px;
font-size: 14px;
}
.thinking-effort-custom input:focus {
@@ -3892,7 +3817,8 @@ input:focus-visible {
.thinking-effort-custom small {
grid-column: 1 / -1;
color: var(--theme-918a81);
font-size: 9px;
font-size: 12px;
line-height: 1.45;
}
.thinking-alias-input {
@@ -3904,7 +3830,7 @@ input:focus-visible {
outline: 0;
background: var(--theme-fff);
color: var(--theme-2d2a26);
font-size: 12px;
font-size: 14px;
}
.thinking-alias-input:focus,
@@ -3939,7 +3865,7 @@ input:focus-visible {
gap: 5px;
overflow: hidden;
color: var(--theme-3f3550);
font-size: 11px;
font-size: 15px;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
@@ -3947,7 +3873,7 @@ input:focus-visible {
.thinking-alias-preview code {
color: var(--theme-8069ad);
font-size: 10px;
font-size: 13px;
}
.thinking-alias-create {
@@ -3957,7 +3883,7 @@ input:focus-visible {
.thinking-alias-list-panel {
display: grid;
grid-template-rows: 48px minmax(0, 1fr);
grid-template-rows: 64px minmax(0, 1fr);
gap: 12px;
padding: 16px;
}
@@ -3985,7 +3911,7 @@ input:focus-visible {
border-radius: 999px;
background: var(--theme-eeece6);
color: var(--theme-5f5a53);
font-size: 11px;
font-size: 13px;
}
.thinking-alias-list {
@@ -3998,8 +3924,8 @@ input:focus-visible {
grid-template-columns: minmax(0, 1fr) auto 34px;
align-items: center;
gap: 10px;
min-height: 58px;
padding: 8px 7px 8px 10px;
min-height: 66px;
padding: 9px 7px 9px 10px;
border-bottom: 1px solid var(--theme-eeeae2);
}
@@ -4024,7 +3950,7 @@ input:focus-visible {
.thinking-alias-route-source > span {
color: var(--theme-7a746c);
font-size: 11px;
font-size: 14px;
}
.thinking-alias-route-source {
@@ -4046,19 +3972,19 @@ input:focus-visible {
border-radius: 3px;
background: var(--theme-eeece6);
color: var(--theme-6f6961);
font-size: 7px;
font-size: 10px;
font-style: normal;
font-weight: 800;
}
.thinking-alias-route-source > small span {
color: var(--theme-9a938a);
font-size: 8px;
font-size: 12px;
}
.thinking-alias-route strong {
color: var(--theme-2d2a26);
font-size: 12px;
font-size: 15px;
}
.thinking-alias-route svg {
@@ -4072,7 +3998,7 @@ input:focus-visible {
border-radius: 999px;
background: var(--theme-f5f1ff);
color: var(--theme-654f91);
font-size: 10px;
font-size: 12px;
font-weight: 800;
text-align: center;
text-transform: uppercase;
@@ -4085,6 +4011,25 @@ input:focus-visible {
text-transform: none;
}
@media (min-width: 1081px) {
.thinking-alias-list-panel .thinking-alias-row {
grid-template-columns: minmax(0, 1fr) auto 34px;
min-height: 94px;
}
.thinking-alias-list-panel .thinking-alias-route {
grid-column: 1 / -1;
}
.thinking-alias-list-panel .thinking-effort-badge {
grid-column: 2;
}
.thinking-alias-list-panel .thinking-alias-row > .icon-button {
grid-column: 3;
}
}
.agent-workbench {
display: grid;
grid-template-columns: 230px minmax(0, 1fr);
@@ -4934,18 +4879,6 @@ input:focus-visible {
overflow-wrap: anywhere;
}
.agent-catalog-update-control {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
}
.agent-catalog-update-control .agent-inline-message {
min-width: 0;
overflow-wrap: anywhere;
}
.agent-pending-badge {
flex: none;
padding: 4px 8px;
@@ -6125,6 +6058,68 @@ input:focus-visible {
min-width: 52px;
}
.real-auth-file-row .auth-file-priority-button {
min-width: 92px;
white-space: nowrap;
}
.auth-priority-dialog {
grid-template-rows: 40px auto auto 20px 40px;
width: min(460px, 100%);
height: auto;
min-height: 0;
}
.auth-priority-target {
overflow: hidden;
padding: 9px 11px;
border: 1px solid var(--theme-e3e1db);
border-radius: 6px;
background: var(--theme-f4f1ea);
color: var(--theme-5f5a53);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
font-size: 12px;
text-overflow: ellipsis;
white-space: nowrap;
}
.auth-priority-field {
display: grid;
gap: 8px;
color: var(--theme-5f5a53);
font-size: 13px;
font-weight: 700;
}
.auth-priority-field input {
width: 100%;
height: 42px;
min-width: 0;
padding: 0 12px;
border: 1px solid var(--theme-d5d2cb);
border-radius: 6px;
outline: none;
background: var(--theme-ffffff);
color: var(--theme-2d2a26);
font: inherit;
}
.auth-priority-field input:focus {
border-color: var(--theme-777067);
box-shadow: 0 0 0 3px rgba(119, 112, 103, 0.12);
}
.auth-priority-field input[aria-invalid='true'] {
border-color: var(--theme-a83f32);
}
.auth-priority-field small {
color: var(--theme-7a746c);
font-size: 12px;
font-weight: 400;
line-height: 1.5;
}
.page-footnote {
margin: -6px 0 0;
color: var(--theme-7a746c);
@@ -6733,8 +6728,8 @@ input:focus-visible {
.config-side-stack {
grid-template-columns: repeat(2, minmax(0, 1fr));
grid-template-rows: 206px;
height: 206px;
grid-template-rows: 166px;
height: 166px;
}
.control-panel,
@@ -6884,16 +6879,8 @@ input:focus-visible {
grid-template-columns: 1fr;
}
.thinking-alias-guide {
grid-template-columns: 24px minmax(0, 1fr);
}
.thinking-alias-flow {
grid-column: 2;
justify-self: start;
}
.thinking-alias-builder,
.thinking-alias-name-panel,
.thinking-alias-list-panel {
height: auto;
min-height: 0;
@@ -7006,8 +6993,8 @@ input:focus-visible {
.config-side-stack {
grid-template-columns: 1fr;
grid-template-rows: repeat(2, 206px);
height: 430px;
grid-template-rows: repeat(2, 166px);
height: 350px;
}
}
@@ -7148,11 +7135,6 @@ input:focus-visible {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.thinking-alias-flow {
width: 100%;
grid-template-columns: auto 14px minmax(0, 1fr);
}
.thinking-alias-row {
grid-template-columns: minmax(0, 1fr) auto;
}
@@ -7176,11 +7158,6 @@ input:focus-visible {
flex-direction: column;
}
.agent-catalog-update-control {
align-items: stretch;
flex-direction: column;
}
.codex-auto-restore-toggle {
width: 100%;
min-width: 0;
@@ -7272,6 +7249,7 @@ input:focus-visible {
.auth-file-actions {
grid-column: 2;
flex-wrap: wrap;
justify-content: flex-start;
}