feat: improve Codex launch controls and add ChatGPT app shutdown
This commit is contained in:
1 parent
7f19f4bbc8
commit
a8a0d54d90
7 files changed
+526
-155
No files matched your search
+161
-14
@@ -152,6 +152,13 @@ pub(crate) struct SessionIndexCleanupResult {
|
||||
backup_path: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ChatGptCloseResult {
|
||||
was_running: bool,
|
||||
closed_processes: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct SetRepairOnLaunchRequest {
|
||||
@@ -315,6 +322,13 @@ pub(crate) fn set_codex_session_repair_on_launch(
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn close_chatgpt_app() -> Result<ChatGptCloseResult, String> {
|
||||
tauri::async_runtime::spawn_blocking(close_chatgpt_app_processes)
|
||||
.await
|
||||
.map_err(|error| format!("关闭 ChatGPT App 任务失败: {error}"))?
|
||||
}
|
||||
|
||||
pub(crate) fn repair_before_codex_launch(
|
||||
user_home: &Path,
|
||||
) -> Result<CodexSessionRepairResult, String> {
|
||||
@@ -2099,10 +2113,20 @@ fn ensure_codex_apps_stopped() -> Result<(), String> {
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn codex_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
windows_app_process_ids(&["ChatGPT.exe", "Codex.exe"])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn chatgpt_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
windows_app_process_ids(&["ChatGPT.exe"])
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn windows_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let mut ids = Vec::new();
|
||||
for name in ["ChatGPT.exe", "Codex.exe"] {
|
||||
for name in names {
|
||||
let filter = format!("IMAGENAME eq {name}");
|
||||
let output = Command::new("tasklist")
|
||||
.args(["/FI", &filter, "/FO", "CSV", "/NH"])
|
||||
@@ -2115,28 +2139,42 @@ fn codex_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
String::from_utf8_lossy(&output.stderr).trim()
|
||||
));
|
||||
}
|
||||
ids.extend(
|
||||
String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let columns = line
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.split("\",\"")
|
||||
.collect::<Vec<_>>();
|
||||
columns.get(1)?.parse::<u32>().ok()
|
||||
}),
|
||||
);
|
||||
ids.extend(parse_windows_tasklist_process_ids(&output.stdout));
|
||||
}
|
||||
ids.sort_unstable();
|
||||
ids.dedup();
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn parse_windows_tasklist_process_ids(output: &[u8]) -> Vec<u32> {
|
||||
String::from_utf8_lossy(output)
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
let columns = line
|
||||
.trim()
|
||||
.trim_matches('"')
|
||||
.split("\",\"")
|
||||
.collect::<Vec<_>>();
|
||||
columns.get(1)?.parse::<u32>().ok()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn codex_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
unix_app_process_ids(&["ChatGPT", "Codex"])
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn chatgpt_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
unix_app_process_ids(&["ChatGPT"])
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn unix_app_process_ids(names: &[&str]) -> Result<Vec<u32>, String> {
|
||||
let mut ids = Vec::new();
|
||||
for name in ["ChatGPT", "Codex"] {
|
||||
for name in names {
|
||||
let output = Command::new("pgrep")
|
||||
.args(["-x", name])
|
||||
.output()
|
||||
@@ -2161,6 +2199,108 @@ fn codex_app_process_ids() -> Result<Vec<u32>, String> {
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn close_chatgpt_app_processes() -> Result<ChatGptCloseResult, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
let process_ids = chatgpt_app_process_ids()?;
|
||||
if process_ids.is_empty() {
|
||||
return Ok(ChatGptCloseResult {
|
||||
was_running: false,
|
||||
closed_processes: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let output = Command::new("taskkill")
|
||||
.args(["/IM", "ChatGPT.exe", "/T", "/F"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|error| format!("无法关闭 ChatGPT.exe: {error}"))?;
|
||||
let mut remaining = chatgpt_app_process_ids()?;
|
||||
for _ in 0..20 {
|
||||
if remaining.is_empty() {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
remaining = chatgpt_app_process_ids()?;
|
||||
}
|
||||
if !remaining.is_empty() {
|
||||
let detail = String::from_utf8_lossy(if output.stderr.is_empty() {
|
||||
&output.stdout
|
||||
} else {
|
||||
&output.stderr
|
||||
});
|
||||
return Err(format!(
|
||||
"ChatGPT App 未能完全关闭(剩余进程:{}){}{}",
|
||||
remaining
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("、"),
|
||||
if detail.trim().is_empty() { "" } else { ":" },
|
||||
detail.trim()
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ChatGptCloseResult {
|
||||
was_running: true,
|
||||
closed_processes: process_ids.len(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
fn close_chatgpt_app_processes() -> Result<ChatGptCloseResult, String> {
|
||||
let process_ids = chatgpt_app_process_ids()?;
|
||||
if process_ids.is_empty() {
|
||||
return Ok(ChatGptCloseResult {
|
||||
was_running: false,
|
||||
closed_processes: 0,
|
||||
});
|
||||
}
|
||||
|
||||
let mut terminate = Command::new("kill");
|
||||
terminate.arg("-TERM");
|
||||
for process_id in &process_ids {
|
||||
terminate.arg(process_id.to_string());
|
||||
}
|
||||
let _ = terminate.status();
|
||||
|
||||
for _ in 0..20 {
|
||||
if chatgpt_app_process_ids()?.is_empty() {
|
||||
return Ok(ChatGptCloseResult {
|
||||
was_running: true,
|
||||
closed_processes: process_ids.len(),
|
||||
});
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
}
|
||||
|
||||
let remaining = chatgpt_app_process_ids()?;
|
||||
let mut force = Command::new("kill");
|
||||
force.arg("-KILL");
|
||||
for process_id in &remaining {
|
||||
force.arg(process_id.to_string());
|
||||
}
|
||||
let _ = force.status();
|
||||
std::thread::sleep(Duration::from_millis(100));
|
||||
let remaining = chatgpt_app_process_ids()?;
|
||||
if !remaining.is_empty() {
|
||||
return Err(format!(
|
||||
"ChatGPT App 未能完全关闭(剩余进程:{})",
|
||||
remaining
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join("、")
|
||||
));
|
||||
}
|
||||
|
||||
Ok(ChatGptCloseResult {
|
||||
was_running: true,
|
||||
closed_processes: process_ids.len(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2168,6 +2308,13 @@ mod tests {
|
||||
|
||||
static TEST_SEQUENCE: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_tasklist_parser_accepts_csv_rows_and_ignores_status_messages() {
|
||||
let output = b"\"ChatGPT.exe\",\"1234\",\"Console\",\"1\",\"10,000 K\"\r\nINFO: No tasks are running which match the specified criteria.\r\n\"ChatGPT.exe\",\"5678\",\"Console\",\"1\",\"20,000 K\"\r\n";
|
||||
assert_eq!(parse_windows_tasklist_process_ids(output), vec![1234, 5678]);
|
||||
}
|
||||
|
||||
fn test_root(name: &str) -> PathBuf {
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"easy-cli-proxy-codex-sessions-{name}-{}-{}",
|
||||
|
||||
+13
-5
@@ -1773,7 +1773,7 @@ async fn launch_agent(
|
||||
status.modification_enabled,
|
||||
&status.modification_state,
|
||||
)?;
|
||||
if client == AgentClient::Codex && config.codex_session_repair_on_launch {
|
||||
if client == AgentClient::Codex && config.codex_session_repair_on_launch && status.configured {
|
||||
let repair_home = home.clone();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
codex_sessions::repair_before_codex_launch(&repair_home)
|
||||
@@ -1827,7 +1827,7 @@ fn validate_agent_launch_modification(
|
||||
enabled: bool,
|
||||
state: &str,
|
||||
) -> Result<(), String> {
|
||||
if enabled && state == "applied" {
|
||||
if client == AgentClient::Codex || (enabled && state == "applied") {
|
||||
return Ok(());
|
||||
}
|
||||
Err(format!(
|
||||
@@ -12869,7 +12869,8 @@ fn main() {
|
||||
codex_sessions::preview_codex_session_index_cleanup,
|
||||
codex_sessions::apply_codex_session_index_cleanup,
|
||||
codex_sessions::get_codex_session_repair_on_launch,
|
||||
codex_sessions::set_codex_session_repair_on_launch
|
||||
codex_sessions::set_codex_session_repair_on_launch,
|
||||
codex_sessions::close_chatgpt_app
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("failed to build app");
|
||||
@@ -15437,13 +15438,20 @@ custom_option = "keep-original"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_launch_requires_an_active_managed_configuration() {
|
||||
fn codex_launch_is_independent_from_managed_configuration_state() {
|
||||
assert!(validate_agent_launch_modification(AgentClient::Codex, true, "applied").is_ok());
|
||||
assert!(validate_agent_launch_modification(AgentClient::Codex, false, "applied").is_err());
|
||||
assert!(
|
||||
validate_agent_launch_modification(AgentClient::Codex, false, "unconfigured").is_ok()
|
||||
);
|
||||
assert!(
|
||||
validate_agent_launch_modification(AgentClient::Codex, true, "external-changed",)
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
validate_agent_launch_modification(AgentClient::OpenCode, false, "unconfigured")
|
||||
.is_err()
|
||||
);
|
||||
assert!(validate_agent_launch_modification(AgentClient::OpenCode, true, "applied").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+9
-7
@@ -387,7 +387,6 @@ export const jaOverrides = {
|
||||
'agents.sessions.repairTitle': '過去のセッションを復元',
|
||||
'agents.sessions.repairDescription': '他の Provider のセッションを現在のセッション Provider に同期します。',
|
||||
'agents.sessions.autoRepair': 'Codex 起動前に過去のセッションを自動復元',
|
||||
'agents.sessions.autoRestoreDescription': 'このアプリから Codex を起動するときにセッションを自動同期します。',
|
||||
'agents.sessions.autoRepairEnabled': '起動前の自動復元を有効にしました。',
|
||||
'agents.sessions.autoRepairDisabled': '起動前の自動復元を無効にしました。',
|
||||
'agents.sessions.settingLoading': '読み込み中',
|
||||
@@ -453,10 +452,8 @@ export const jaOverrides = {
|
||||
'agents.clientVersion': 'クライアントバージョン',
|
||||
'agents.notFetched': '未取得',
|
||||
'agents.useModel': '使用モデル',
|
||||
'agents.useModelDescription': 'コアで公開中のモデルを同期し、選択したモデルを既定に設定します',
|
||||
'agents.pending': '未適用',
|
||||
'agents.model.readingAvailable': '利用可能なモデルを読み込んでいます',
|
||||
'agents.model.cannotConfigure': '選択可能なモデルがないため、設定変更を適用できません',
|
||||
'agents.model.current': '現在適用中:{model}',
|
||||
'agents.model.firstSelection': '利用可能なモデル {count} 件。初回は先頭を選択します',
|
||||
'agents.modify.title': '設定変更',
|
||||
@@ -466,22 +463,22 @@ export const jaOverrides = {
|
||||
'agents.modify.externalChanged': '外部の設定変更を検出し、その内容を保持しています。「設定変更を適用」をクリックするまで管理対象フィールドは書き換えません。',
|
||||
'agents.modify.invalid': '現在の設定を解析できません。「設定変更を適用」は最小の有効な設定を再構築し、「デフォルト設定」はファイル全体をリセットします。',
|
||||
'agents.modify.applied': 'アプリが管理するフィールドは適用済みで、それ以外のユーザー設定は保持されています。',
|
||||
'agents.modify.unconfigured': 'モデルを選択して設定変更を適用してください。設定のコピーやバックアップは作成しません。',
|
||||
'agents.footer.changed': 'モデル選択が変更されました。先に「設定変更を適用」をクリックしてください',
|
||||
'agents.footer.changed': 'モデル選択が変更されました。設定変更を適用すると新しいモデルが使用されます。',
|
||||
'agents.footer.applied': '設定変更は適用済みです。アプリは関連フィールドだけを管理します。',
|
||||
'agents.footer.externalChanged': '外部変更を保持しています。再適用するまでこのクライアントは起動できません。',
|
||||
'agents.footer.externalChanged': '外部変更を保持しています。設定変更を適用すると管理対象フィールドが再度書き込まれます。',
|
||||
'agents.footer.invalidManaged': '設定が無効です。変更を適用して修復するか、デフォルト設定にリセットしてください。',
|
||||
'agents.footer.unsupported': '現在のシステムではこのクライアントを設定できません',
|
||||
'agents.footer.installFirst': 'クライアントをインストールしてから再検出してください',
|
||||
'agents.footer.noCommand': '設定ファイルは検出しましたが、クライアントコマンドが見つかりません',
|
||||
'agents.footer.invalidConfig': '元の設定形式が無効なため、安全に変更できません',
|
||||
'agents.footer.enableFirst': '先に「設定変更を適用」をクリックし、CPA モデル設定を反映してからクライアントを起動してください',
|
||||
'agents.launchMethods': 'Codex の起動方法',
|
||||
'agents.launch.enableFirst': '先に設定変更を適用してください',
|
||||
'agents.launch.applyFirst': '先に選択中のモデルを適用してください',
|
||||
'agents.launch.starting': '起動中',
|
||||
'agents.launch.start': '{target} を起動',
|
||||
'agents.launch.unavailable': '起動できません',
|
||||
'agents.launch.closeChatgpt': 'ChatGPT App を終了',
|
||||
'agents.launch.closingChatgpt': '終了中',
|
||||
'agents.default.title': 'デフォルト設定にリセット',
|
||||
'agents.default.description': '選択中のモデルを使って {name} の関連設定ファイルをすべて書き直し、その他のユーザー内容を削除します。バックアップは作成されず、元に戻せません。',
|
||||
'agents.default.confirm': 'デフォルト設定を書き込む',
|
||||
@@ -489,6 +486,11 @@ export const jaOverrides = {
|
||||
'agents.clear.description': 'Codex 設定ディレクトリの auth.json と config.toml を完全に削除し、このアプリが記録した適用状態をリセットします。バックアップは作成されず、元に戻せません。',
|
||||
'agents.clear.confirm': '設定を消去',
|
||||
'agents.clear.success': 'Codex 設定を消去しました。',
|
||||
'agents.closeApp.title': 'ChatGPT App を終了',
|
||||
'agents.closeApp.description': 'このコンピューターで実行中の ChatGPT App プロセスとその子プロセスをすべて終了します。生成中の応答や未保存の状態が失われる可能性があります。',
|
||||
'agents.closeApp.confirm': 'App を終了',
|
||||
'agents.closeApp.success': 'ChatGPT App を終了しました({count} プロセス)。',
|
||||
'agents.closeApp.notRunning': 'ChatGPT App は実行されていません。',
|
||||
'apiAccess.compatibleName': 'OpenAI 互換 {number}',
|
||||
'apiAccess.error.headerMissingColon': 'カスタムヘッダーの {number} 行目にコロンがありません',
|
||||
'apiAccess.error.headerInvalid': 'カスタムヘッダーの {number} 行目の形式が無効です',
|
||||
|
||||
+18
-14
@@ -390,7 +390,6 @@ export const zhCN = {
|
||||
'agents.sessions.repairTitle': '恢复历史会话',
|
||||
'agents.sessions.repairDescription': '将其他渠道的会话同步到当前会话',
|
||||
'agents.sessions.autoRepair': '启动 Codex 前自动恢复历史会话',
|
||||
'agents.sessions.autoRestoreDescription': '从本软件启动 Codex 时自动同步会话',
|
||||
'agents.sessions.autoRepairEnabled': '已开启启动前自动恢复历史会话。',
|
||||
'agents.sessions.autoRepairDisabled': '已关闭启动前自动恢复历史会话。',
|
||||
'agents.sessions.settingLoading': '读取中',
|
||||
@@ -456,10 +455,8 @@ export const zhCN = {
|
||||
'agents.clientVersion': '客户端版本',
|
||||
'agents.notFetched': '未获取',
|
||||
'agents.useModel': '使用模型',
|
||||
'agents.useModelDescription': '同步内核当前开放的模型,并将所选项设为默认模型',
|
||||
'agents.pending': '尚未应用',
|
||||
'agents.model.readingAvailable': '正在读取可用模型',
|
||||
'agents.model.cannotConfigure': '无可选模型,无法应用配置修改',
|
||||
'agents.model.current': '当前已应用:{model}',
|
||||
'agents.model.firstSelection': '{count} 个可用模型,首次默认选择第一项',
|
||||
'agents.modify.title': '配置修改',
|
||||
@@ -469,22 +466,22 @@ export const zhCN = {
|
||||
'agents.modify.externalChanged': '检测到外部配置变化。软件会保留这些内容;点击“应用配置修改”后才会重新写入受管字段。',
|
||||
'agents.modify.invalid': '当前配置无法解析。点击“应用配置修改”会重建最小合法配置;“默认配置”会重置整个文件。',
|
||||
'agents.modify.applied': '软件负责的配置字段已应用,其他用户内容保持不变。',
|
||||
'agents.modify.unconfigured': '选择模型后可应用配置修改;不会创建配置副本或备份。',
|
||||
'agents.footer.changed': '模型选择已变化,请先点击“应用配置修改”',
|
||||
'agents.footer.changed': '模型选择已变化;应用配置修改后将使用新模型。',
|
||||
'agents.footer.applied': '配置修改已应用;软件只管理相关字段。',
|
||||
'agents.footer.externalChanged': '外部配置已保留。重新应用前无法启动该客户端。',
|
||||
'agents.footer.externalChanged': '外部配置已保留;应用配置修改后才会重新写入受管字段。',
|
||||
'agents.footer.invalidManaged': '配置当前无效,请应用修改进行修复,或重置为默认配置。',
|
||||
'agents.footer.unsupported': '当前系统无法配置该客户端',
|
||||
'agents.footer.installFirst': '请先安装客户端并重新检测',
|
||||
'agents.footer.noCommand': '检测到配置文件,但没有找到客户端命令',
|
||||
'agents.footer.invalidConfig': '原配置格式异常,无法安全修改',
|
||||
'agents.footer.enableFirst': '请先点击“应用配置修改”,确保 CPA 模型配置生效后再启动客户端',
|
||||
'agents.launchMethods': 'Codex 启动方式',
|
||||
'agents.launch.enableFirst': '请先应用配置修改',
|
||||
'agents.launch.applyFirst': '请先应用当前模型选择',
|
||||
'agents.launch.starting': '启动中',
|
||||
'agents.launch.start': '启动 {target}',
|
||||
'agents.launch.unavailable': '无法启动',
|
||||
'agents.launch.closeChatgpt': '关闭 ChatGPT App',
|
||||
'agents.launch.closingChatgpt': '关闭中',
|
||||
'agents.default.title': '重置为默认配置',
|
||||
'agents.default.description': '这会使用当前选中模型重写 {name} 的全部相关配置文件,并删除其中其他用户内容。该操作没有备份且无法恢复。',
|
||||
'agents.default.confirm': '确认写入默认配置',
|
||||
@@ -492,6 +489,11 @@ export const zhCN = {
|
||||
'agents.clear.description': '这会永久删除 Codex 配置目录中的 auth.json 和 config.toml,并重置软件记录的应用状态。该操作没有备份且无法恢复。',
|
||||
'agents.clear.confirm': '确认清空配置',
|
||||
'agents.clear.success': 'Codex 配置已清空。',
|
||||
'agents.closeApp.title': '关闭 ChatGPT App',
|
||||
'agents.closeApp.description': '这会关闭本机所有正在运行的 ChatGPT App 进程及其子进程。正在生成的回复或尚未保存的状态可能丢失。',
|
||||
'agents.closeApp.confirm': '确认关闭 App',
|
||||
'agents.closeApp.success': 'ChatGPT App 已关闭({count} 个进程)。',
|
||||
'agents.closeApp.notRunning': 'ChatGPT App 当前没有运行。',
|
||||
'apiAccess.compatibleName': 'OpenAI 兼容 {number}',
|
||||
'apiAccess.error.headerMissingColon': '自定义请求头第 {number} 行缺少冒号',
|
||||
'apiAccess.error.headerInvalid': '自定义请求头第 {number} 行格式无效',
|
||||
@@ -1174,7 +1176,6 @@ export const en: Record<MessageKey, string> = {
|
||||
'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.autoRestoreDescription': 'Automatically sync sessions when Codex is launched from this app.',
|
||||
'agents.sessions.autoRepairEnabled': 'Automatic restoration before launch is enabled.',
|
||||
'agents.sessions.autoRepairDisabled': 'Automatic restoration before launch is disabled.',
|
||||
'agents.sessions.settingLoading': 'Loading',
|
||||
@@ -1240,10 +1241,8 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.clientVersion': 'Client Version',
|
||||
'agents.notFetched': 'Not available',
|
||||
'agents.useModel': 'Model',
|
||||
'agents.useModelDescription': 'Synchronize models currently exposed by the core and set the selection as the default',
|
||||
'agents.pending': 'Not Applied',
|
||||
'agents.model.readingAvailable': 'Loading available models',
|
||||
'agents.model.cannotConfigure': 'No models are available, so configuration changes cannot be applied',
|
||||
'agents.model.current': 'Currently applied: {model}',
|
||||
'agents.model.firstSelection': '{count} models available; the first is selected by default',
|
||||
'agents.modify.title': 'Configuration Changes',
|
||||
@@ -1253,22 +1252,22 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.modify.externalChanged': 'External configuration changes were detected and preserved. Managed fields are rewritten only after you click Apply Configuration Changes.',
|
||||
'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.modify.unconfigured': 'Select a model and apply the configuration. No configuration copy or backup is created.',
|
||||
'agents.footer.changed': 'The model selection changed. Click “Apply Configuration Changes” first.',
|
||||
'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.externalChanged': 'External changes were preserved. Apply them before launching this client.',
|
||||
'agents.footer.externalChanged': 'External changes were preserved. Managed fields are rewritten when you apply the configuration.',
|
||||
'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.footer.enableFirst': 'Click “Apply Configuration Changes” before launching so the CPA model configuration is active',
|
||||
'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.start': 'Start {target}',
|
||||
'agents.launch.unavailable': 'Cannot Start',
|
||||
'agents.launch.closeChatgpt': 'Close ChatGPT App',
|
||||
'agents.launch.closingChatgpt': 'Closing',
|
||||
'agents.default.title': 'Reset to Default Configuration',
|
||||
'agents.default.description': 'This rewrites every related {name} configuration file using the selected model and deletes all other user content in those files. No backup is created and the operation cannot be undone.',
|
||||
'agents.default.confirm': 'Write Default Configuration',
|
||||
@@ -1276,6 +1275,11 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.clear.description': 'This permanently deletes auth.json and config.toml from the Codex configuration directory and resets the application state tracked by this app. No backup is created and the operation cannot be undone.',
|
||||
'agents.clear.confirm': 'Clear Configuration',
|
||||
'agents.clear.success': 'The Codex configuration has been cleared.',
|
||||
'agents.closeApp.title': 'Close ChatGPT App',
|
||||
'agents.closeApp.description': 'This closes all running ChatGPT App processes and their child processes on this computer. In-progress responses or unsaved state may be lost.',
|
||||
'agents.closeApp.confirm': 'Close App',
|
||||
'agents.closeApp.success': 'ChatGPT App was closed ({count} processes).',
|
||||
'agents.closeApp.notRunning': 'ChatGPT App is not running.',
|
||||
'apiAccess.compatibleName': 'OpenAI Compatible {number}',
|
||||
'apiAccess.error.headerMissingColon': 'Custom header line {number} is missing a colon',
|
||||
'apiAccess.error.headerInvalid': 'Custom header line {number} is invalid',
|
||||
|
||||
+139
-52
@@ -18,6 +18,7 @@ import {
|
||||
ChevronDown,
|
||||
LoaderCircle,
|
||||
Play,
|
||||
Power,
|
||||
RefreshCw,
|
||||
Search,
|
||||
Sparkles,
|
||||
@@ -83,6 +84,11 @@ type AgentConfigActionResult = {
|
||||
conflictFiles: string[];
|
||||
};
|
||||
|
||||
type ChatGptCloseResult = {
|
||||
wasRunning: boolean;
|
||||
closedProcesses: number;
|
||||
};
|
||||
|
||||
type AgentDefinition = {
|
||||
id: AgentClientId;
|
||||
name: string;
|
||||
@@ -489,7 +495,7 @@ 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' | 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('');
|
||||
@@ -501,6 +507,9 @@ export function AgentsPage() {
|
||||
const [clearError, setClearError] = useState('');
|
||||
const [clearNotice, setClearNotice] = useState('');
|
||||
const [clearConfirmOpen, setClearConfirmOpen] = useState(false);
|
||||
const [closeAppError, setCloseAppError] = useState('');
|
||||
const [closeAppNotice, setCloseAppNotice] = useState('');
|
||||
const [closeAppConfirmOpen, setCloseAppConfirmOpen] = useState(false);
|
||||
|
||||
const loadStatuses = useCallback(async (forceRefresh = false) => {
|
||||
const command = forceRefresh
|
||||
@@ -582,6 +591,9 @@ export function AgentsPage() {
|
||||
setClearError('');
|
||||
setClearNotice('');
|
||||
setClearConfirmOpen(false);
|
||||
setCloseAppError('');
|
||||
setCloseAppNotice('');
|
||||
setCloseAppConfirmOpen(false);
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -624,10 +636,41 @@ export function AgentsPage() {
|
||||
const canLaunch = Boolean(
|
||||
activeStatus?.supportedPlatform
|
||||
&& activeStatus.installed
|
||||
&& activeStatus.modificationEnabled
|
||||
&& activeStatus.modificationState === 'applied'
|
||||
&& selectedLaunchTarget,
|
||||
&& selectedLaunchTarget
|
||||
&& (selected === 'codex'
|
||||
|| (activeStatus.modificationEnabled && activeStatus.modificationState === 'applied')),
|
||||
);
|
||||
const modelHint = modelSelectionError
|
||||
|| modelError
|
||||
|| (modelLoading
|
||||
? t('agents.model.readingAvailable')
|
||||
: models.length === 0
|
||||
? ''
|
||||
: activeStatus?.modificationState === 'applied'
|
||||
? t('agents.model.current', { model: appliedModel || '—' })
|
||||
: t('agents.model.firstSelection', { count: models.length }));
|
||||
const modificationDescription = activeStatus?.modificationState === 'external-changed'
|
||||
? t('agents.modify.externalChanged')
|
||||
: activeStatus?.modificationState === 'invalid'
|
||||
? t('agents.modify.invalid')
|
||||
: activeStatus?.modificationState === 'applied'
|
||||
? t('agents.modify.applied')
|
||||
: '';
|
||||
const footerMessage = 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')
|
||||
: '';
|
||||
|
||||
const refreshModels = () => {
|
||||
void loadModels();
|
||||
@@ -728,7 +771,7 @@ export function AgentsPage() {
|
||||
setBusyAction('launch');
|
||||
setLaunchError('');
|
||||
try {
|
||||
if (draftChanged) {
|
||||
if (selected !== 'codex' && draftChanged) {
|
||||
throw new Error(t('agents.error.applyFirst'));
|
||||
}
|
||||
await invoke('launch_agent', { client: selected, target: selectedLaunchTarget?.id });
|
||||
@@ -739,6 +782,23 @@ export function AgentsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const closeChatGptApp = async () => {
|
||||
setBusyAction('close-app');
|
||||
setCloseAppError('');
|
||||
setCloseAppNotice('');
|
||||
try {
|
||||
const result = await invoke<ChatGptCloseResult>('close_chatgpt_app');
|
||||
setCloseAppConfirmOpen(false);
|
||||
setCloseAppNotice(result.wasRunning
|
||||
? t('agents.closeApp.success', { count: result.closedProcesses })
|
||||
: t('agents.closeApp.notRunning'));
|
||||
} catch (requestError) {
|
||||
setCloseAppError(String(requestError));
|
||||
} finally {
|
||||
setBusyAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const openDefaultConfirmation = () => {
|
||||
setDefaultError('');
|
||||
setDefaultConfirmOpen(true);
|
||||
@@ -759,6 +819,16 @@ export function AgentsPage() {
|
||||
setClearConfirmOpen(false);
|
||||
};
|
||||
|
||||
const openCloseAppConfirmation = () => {
|
||||
setCloseAppError('');
|
||||
setCloseAppConfirmOpen(true);
|
||||
};
|
||||
|
||||
const closeCloseAppConfirmation = () => {
|
||||
setCloseAppError('');
|
||||
setCloseAppConfirmOpen(false);
|
||||
};
|
||||
|
||||
const availableSubpages = agentSubpages.filter(
|
||||
(subpage) => !subpage.clients || subpage.clients.includes(selected),
|
||||
);
|
||||
@@ -864,7 +934,7 @@ export function AgentsPage() {
|
||||
|
||||
<section className="agent-core-setting-section agent-model-section">
|
||||
<div className="agent-section-heading">
|
||||
<div><strong>{t('agents.useModel')}</strong><span>{t('agents.useModelDescription')}</span></div>
|
||||
<div><strong>{t('agents.useModel')}</strong></div>
|
||||
{draftChanged ? <span className="agent-pending-badge">{t('agents.pending')}</span> : null}
|
||||
</div>
|
||||
<AgentModelPicker
|
||||
@@ -876,34 +946,22 @@ export function AgentsPage() {
|
||||
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>
|
||||
{modelHint ? (
|
||||
<span
|
||||
className={`agent-model-hint ${modelSelectionError || modelError ? 'error' : ''}`}
|
||||
role={modelSelectionError || modelError ? 'alert' : undefined}
|
||||
aria-live="polite"
|
||||
>
|
||||
{modelHint}
|
||||
</span>
|
||||
) : null}
|
||||
</section>
|
||||
|
||||
<section className={`agent-core-setting-section agent-modification-actions ${activeStatus?.modificationState === 'applied' ? 'enabled' : ''}`}>
|
||||
<div className="agent-section-heading">
|
||||
<div>
|
||||
<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>
|
||||
{modificationDescription ? <span>{modificationDescription}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="agent-modification-control">
|
||||
@@ -957,24 +1015,12 @@ export function AgentsPage() {
|
||||
{selected === 'codex' ? <CodexSessionAutoRestoreCard /> : null}
|
||||
|
||||
<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>
|
||||
{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 ? (
|
||||
@@ -1003,23 +1049,41 @@ export function AgentsPage() {
|
||||
disabled={
|
||||
busy
|
||||
|| !canLaunch
|
||||
|| draftChanged
|
||||
|| (selected !== 'codex' && draftChanged)
|
||||
}
|
||||
title={draftChanged
|
||||
? t('agents.launch.applyFirst')
|
||||
: activeStatus?.modificationState === 'applied'
|
||||
? selectedLaunchTarget?.detail
|
||||
: t('agents.launch.enableFirst')}
|
||||
title={selected === 'codex'
|
||||
? selectedLaunchTarget?.detail
|
||||
: 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>
|
||||
{selected === 'codex' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="danger-button agent-close-app-button"
|
||||
onClick={openCloseAppConfirmation}
|
||||
disabled={busy}
|
||||
>
|
||||
{busyAction === 'close-app' ? <LoaderCircle size={16} className="spin" /> : <Power size={16} />}
|
||||
{busyAction === 'close-app' ? t('agents.launch.closingChatgpt') : t('agents.launch.closeChatgpt')}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
{launchError ? (
|
||||
<span className="agent-inline-message error" role="alert" aria-live="polite">
|
||||
{launchError}
|
||||
</span>
|
||||
) : null}
|
||||
{closeAppNotice ? (
|
||||
<span className="agent-inline-message" role="status" aria-live="polite">
|
||||
{closeAppNotice}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1085,6 +1149,29 @@ export function AgentsPage() {
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{closeAppConfirmOpen ? (
|
||||
<div className="config-dialog-backdrop">
|
||||
<section className="config-dialog agent-restore-dialog" role="alertdialog" aria-modal="true" aria-labelledby="agent-close-app-title">
|
||||
<div className="config-dialog-heading">
|
||||
<div><AlertTriangle size={19} /><h2 id="agent-close-app-title">{t('agents.closeApp.title')}</h2></div>
|
||||
</div>
|
||||
<p>{t('agents.closeApp.description')}</p>
|
||||
{closeAppError ? (
|
||||
<span className="agent-inline-message error" role="alert" aria-live="polite">
|
||||
{closeAppError}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="config-dialog-actions two-actions">
|
||||
<button type="button" className="secondary-button" onClick={closeCloseAppConfirmation} disabled={busy}>{t('common.cancel')}</button>
|
||||
<button type="button" className="danger-button" onClick={() => void closeChatGptApp()} disabled={busy}>
|
||||
{busyAction === 'close-app' ? <LoaderCircle size={16} className="spin" /> : <Power size={16} />}
|
||||
{t('agents.closeApp.confirm')}
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
) : null}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,6 @@ export function CodexSessionAutoRestoreCard() {
|
||||
<div className="agent-section-heading">
|
||||
<div>
|
||||
<strong>{t('agents.sessions.autoRepair')}</strong>
|
||||
<span>{t('agents.sessions.autoRestoreDescription')}</span>
|
||||
</div>
|
||||
<label className="codex-auto-restore-toggle">
|
||||
{loading || saving ? <LoaderCircle size={14} className="spin" aria-hidden="true" /> : null}
|
||||
|
||||
+186
-62
@@ -391,8 +391,14 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.page {
|
||||
width: min(100%, 1180px);
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
min-width: 0;
|
||||
margin-inline: 0;
|
||||
}
|
||||
|
||||
.page > * {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kernel-page,
|
||||
@@ -403,6 +409,7 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.panel {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #e3e1db;
|
||||
@@ -456,7 +463,7 @@ input:focus-visible {
|
||||
|
||||
.status-strip {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(220px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -1913,7 +1920,7 @@ input:focus-visible {
|
||||
|
||||
.oauth-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(360px, 100%), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -3759,7 +3766,7 @@ input:focus-visible {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
color: #7a746c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -3799,14 +3806,14 @@ input:focus-visible {
|
||||
.agent-list-heading strong,
|
||||
.agent-list-items strong {
|
||||
color: #2d2a26;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-list-heading span,
|
||||
.agent-list-items small {
|
||||
overflow: hidden;
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -3877,6 +3884,8 @@ input:focus-visible {
|
||||
grid-template-rows: auto auto;
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
container-type: inline-size;
|
||||
container-name: agent-panel;
|
||||
}
|
||||
|
||||
.agent-subpage-tabs {
|
||||
@@ -3903,7 +3912,7 @@ input:focus-visible {
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #7a746c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -3944,7 +3953,7 @@ input:focus-visible {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 12px;
|
||||
padding: 15px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e3e1db;
|
||||
border-radius: 8px;
|
||||
background: #faf9f5;
|
||||
@@ -3969,13 +3978,13 @@ input:focus-visible {
|
||||
.codex-session-overview-head strong,
|
||||
.codex-session-list-head strong {
|
||||
color: #2d2a26;
|
||||
font-size: 13px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.codex-session-overview-head span,
|
||||
.codex-session-list-head > div:first-child > span {
|
||||
color: #8a847c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -3986,7 +3995,7 @@ input:focus-visible {
|
||||
min-height: 32px;
|
||||
margin-top: 0;
|
||||
padding: 0 10px;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.codex-session-metrics {
|
||||
@@ -4011,14 +4020,14 @@ input:focus-visible {
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #8a847c;
|
||||
font-size: 9px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.codex-session-metrics strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
font-size: 15px;
|
||||
font-size: 17px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -4026,7 +4035,7 @@ input:focus-visible {
|
||||
.codex-session-database-metric strong {
|
||||
direction: rtl;
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -4064,12 +4073,12 @@ input:focus-visible {
|
||||
|
||||
.codex-session-repair-copy strong {
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.codex-session-repair-copy span {
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
@@ -4090,7 +4099,7 @@ input:focus-visible {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: #7a746c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.codex-session-progress-track {
|
||||
@@ -4117,7 +4126,7 @@ input:focus-visible {
|
||||
border-radius: 7px;
|
||||
background: #eef7f0;
|
||||
color: #2f6b3f;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
@@ -4177,7 +4186,7 @@ input:focus-visible {
|
||||
|
||||
.codex-session-selection-actions > span {
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -4256,17 +4265,17 @@ input:focus-visible {
|
||||
|
||||
.codex-session-main strong {
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.codex-session-main code {
|
||||
color: #8a847c;
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.codex-session-main span {
|
||||
color: #5f5a53;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.codex-session-meta {
|
||||
@@ -4281,7 +4290,7 @@ input:focus-visible {
|
||||
border-radius: 999px;
|
||||
background: #eef7f0;
|
||||
color: #2f6b3f;
|
||||
font-size: 8px;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
@@ -4295,7 +4304,7 @@ input:focus-visible {
|
||||
.codex-session-meta time {
|
||||
max-width: 190px;
|
||||
color: #8a847c;
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.codex-session-delete-button {
|
||||
@@ -4313,7 +4322,7 @@ input:focus-visible {
|
||||
border-radius: 7px;
|
||||
background: #faf9f5;
|
||||
color: #8a847c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -4332,7 +4341,7 @@ input:focus-visible {
|
||||
.codex-session-pagination span {
|
||||
min-width: 72px;
|
||||
color: #5f5a53;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -4358,7 +4367,7 @@ input:focus-visible {
|
||||
.codex-session-cleanup-dialog > p {
|
||||
margin: 0;
|
||||
color: #5f5a53;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.65;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -4375,7 +4384,7 @@ input:focus-visible {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #5f5a53;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -4423,13 +4432,13 @@ input:focus-visible {
|
||||
|
||||
.codex-session-cleanup-list strong {
|
||||
color: #2d2a26;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.codex-session-cleanup-list code,
|
||||
.codex-session-cleanup-list small {
|
||||
color: #8a847c;
|
||||
font-size: 9px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.agent-status-messages {
|
||||
@@ -4454,7 +4463,9 @@ input:focus-visible {
|
||||
display: flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.agent-launch-targets {
|
||||
@@ -4475,7 +4486,7 @@ input:focus-visible {
|
||||
border-radius: 5px;
|
||||
background: transparent;
|
||||
color: #7a746c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -4489,7 +4500,7 @@ input:focus-visible {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
height: 70px;
|
||||
min-height: 76px;
|
||||
}
|
||||
|
||||
.agent-status-grid > div {
|
||||
@@ -4505,14 +4516,14 @@ input:focus-visible {
|
||||
.agent-status-grid span {
|
||||
gap: 5px;
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-status-grid strong {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -4522,15 +4533,15 @@ input:focus-visible {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
align-content: start;
|
||||
gap: 9px;
|
||||
padding: 15px;
|
||||
gap: 12px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e3e1db;
|
||||
border-radius: 8px;
|
||||
background: #faf9f5;
|
||||
}
|
||||
|
||||
.agent-model-section {
|
||||
min-height: 138px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.agent-section-heading {
|
||||
@@ -4549,13 +4560,13 @@ input:focus-visible {
|
||||
|
||||
.agent-section-heading strong {
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.agent-section-heading > div > span,
|
||||
.agent-model-hint {
|
||||
color: #8a847c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-model-hint {
|
||||
@@ -4570,7 +4581,7 @@ input:focus-visible {
|
||||
border-radius: 999px;
|
||||
background: #fff8eb;
|
||||
color: #805a24;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -4622,12 +4633,12 @@ input:focus-visible {
|
||||
|
||||
.agent-model-trigger strong {
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-model-trigger small {
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-model-trigger > svg {
|
||||
@@ -4673,7 +4684,7 @@ input:focus-visible {
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: #2d2a26;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-model-list {
|
||||
@@ -4725,12 +4736,12 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.agent-model-option strong {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-model-option small {
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-model-option > svg {
|
||||
@@ -4745,13 +4756,13 @@ input:focus-visible {
|
||||
gap: 6px;
|
||||
padding: 16px;
|
||||
color: #8a847c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.agent-model-empty strong {
|
||||
color: #5f5a53;
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agent-model-empty.error,
|
||||
@@ -4765,7 +4776,7 @@ input:focus-visible {
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: #8a847c;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.agent-model-dropdown-footer .error {
|
||||
@@ -4773,11 +4784,11 @@ input:focus-visible {
|
||||
}
|
||||
|
||||
.agent-modification-actions {
|
||||
min-height: 104px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.codex-auto-restore-card {
|
||||
min-height: 82px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.codex-auto-restore-toggle {
|
||||
@@ -4817,7 +4828,7 @@ input:focus-visible {
|
||||
.codex-auto-restore-toggle > strong {
|
||||
min-width: 42px;
|
||||
color: #5f5a53;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -4828,7 +4839,7 @@ input:focus-visible {
|
||||
.codex-auto-restore-message {
|
||||
max-width: 100%;
|
||||
color: #2f6b3f;
|
||||
font-size: 10px;
|
||||
font-size: 11px;
|
||||
line-height: 1.4;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
@@ -4849,13 +4860,13 @@ input:focus-visible {
|
||||
|
||||
.agent-modification-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-modification-buttons.codex {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(132px, 1fr));
|
||||
}
|
||||
|
||||
.agent-modification-buttons button {
|
||||
@@ -4901,7 +4912,7 @@ input:focus-visible {
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
color: #7a746c;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.agent-config-summary > span {
|
||||
@@ -4914,6 +4925,7 @@ input:focus-visible {
|
||||
display: grid;
|
||||
justify-items: end;
|
||||
gap: 5px;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.agent-launch-control > .agent-inline-message {
|
||||
@@ -4925,6 +4937,10 @@ input:focus-visible {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.agent-config-footer .agent-close-app-button {
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.usage-records-page {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@@ -5017,7 +5033,7 @@ input:focus-visible {
|
||||
|
||||
.usage-filter-panel {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(110px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(130px, 100%), 1fr));
|
||||
gap: 8px;
|
||||
padding: 10px;
|
||||
}
|
||||
@@ -5064,7 +5080,7 @@ input:focus-visible {
|
||||
.usage-stat-grid {
|
||||
display: grid;
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@@ -5219,7 +5235,7 @@ input:focus-visible {
|
||||
|
||||
.usage-analysis-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -5512,7 +5528,7 @@ input:focus-visible {
|
||||
|
||||
.real-quota-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
@@ -5640,13 +5656,13 @@ input:focus-visible {
|
||||
|
||||
.quota-summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.quota-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(320px, 100%), 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@@ -5910,6 +5926,114 @@ input:focus-visible {
|
||||
}
|
||||
}
|
||||
|
||||
@container agent-panel (min-width: 900px) {
|
||||
.agent-core-config {
|
||||
grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.agent-status-grid,
|
||||
.agent-status-messages,
|
||||
.codex-auto-restore-card,
|
||||
.agent-config-footer {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.agent-model-section {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.agent-modification-actions {
|
||||
grid-column: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@container agent-panel (max-width: 640px) {
|
||||
.agent-status-grid {
|
||||
grid-template-columns: 1fr;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.codex-session-metrics {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.codex-session-database-metric {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.codex-session-repair-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.codex-session-repair-actions {
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.codex-session-row,
|
||||
.codex-session-row:has(.codex-session-checkbox) {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.codex-session-row:has(.codex-session-checkbox) {
|
||||
grid-template-columns: 20px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.codex-session-row:has(.codex-session-checkbox) .codex-session-main {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.codex-session-meta,
|
||||
.codex-session-row:has(.codex-session-checkbox) .codex-session-meta {
|
||||
grid-column: 1 / -1;
|
||||
grid-template-columns: auto minmax(80px, 1fr) auto;
|
||||
align-items: center;
|
||||
justify-items: start;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.codex-session-meta small,
|
||||
.codex-session-meta time {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
.codex-session-meta time {
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.agent-config-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.agent-config-summary > span {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.agent-config-footer > .agent-launch-control {
|
||||
width: 100%;
|
||||
justify-items: stretch;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.agent-launch-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-launch-targets {
|
||||
grid-column: 1 / -1;
|
||||
grid-auto-columns: 1fr;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.agent-launch-actions > .primary-button:last-child {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1080px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 204px minmax(0, 1fr);
|
||||
|
||||
Reference in new issue
Block a user