Dev (#162)
* chore(release): stop publishing legacy Windows update packages * feat(agents): support OpenCode Desktop across platforms Detect CLI and desktop installations on Windows, macOS, and Linux, and add separate launch and restart controls. Closes #156 * fix(codex): align model display names and show total session count - preserve runtime model identity in generated catalogs - count distinct sessions across database and rollout sources - update session count labels in all supported languages * fix(codex): keep config and auth files in sync - generate auth.json for API key configuration - preserve and validate OAuth credentials - include auth.json in transactional updates and rollback - retain compatibility with legacy Codex configuration states * fix(cpa-gui): update version to 0.2.59 in Cargo files
This commit is contained in:
1 parent
93fe5baa54
commit
d0bd5a890a
15 files changed
+205
-20
No files matched your search
Generated
+1
-1
@@ -593,7 +593,7 @@ checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cpa-gui"
|
||||
version = "0.2.26"
|
||||
version = "0.2.59"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "cpa-gui"
|
||||
# Single source of truth for the desktop app version.
|
||||
version = "0.2.26"
|
||||
version = "0.2.59"
|
||||
description = "CPA desktop management shell"
|
||||
authors = ["lzt404"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -145,6 +145,7 @@ pub(crate) fn build_agent_updates_with_oauth(
|
||||
path: codex_model_catalog_path(home),
|
||||
after: catalog.to_string(),
|
||||
});
|
||||
updates.push(build_codex_auth_update(home, api_key, oauth_configuration)?);
|
||||
Ok(updates)
|
||||
}
|
||||
AgentClient::OpenCode => {
|
||||
@@ -2524,6 +2525,32 @@ pub(crate) fn build_codex_agent_config_with_oauth(
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub(crate) fn build_codex_api_auth(api_key: &str) -> Result<String, String> {
|
||||
let mut rendered = serde_json::to_string_pretty(&serde_json::json!({
|
||||
"auth_mode": "apikey",
|
||||
"OPENAI_API_KEY": api_key,
|
||||
}))
|
||||
.map_err(|error| format!("生成 Codex auth.json 失败: {error}"))?;
|
||||
rendered.push('\n');
|
||||
Ok(rendered)
|
||||
}
|
||||
|
||||
pub(crate) fn build_codex_auth_update(
|
||||
home: &Path,
|
||||
api_key: &str,
|
||||
oauth_configuration: bool,
|
||||
) -> Result<AgentFileUpdate, String> {
|
||||
let path = codex_configuration_directory(home).join("auth.json");
|
||||
let after = if oauth_configuration {
|
||||
validate_codex_oauth_login_at(&path)?;
|
||||
read_optional_text(&path)?
|
||||
.ok_or_else(|| "Codex OAuth 登录凭据在应用配置前已被删除,请重新登录".to_string())?
|
||||
} else {
|
||||
build_codex_api_auth(api_key)?
|
||||
};
|
||||
Ok(AgentFileUpdate { path, after })
|
||||
}
|
||||
|
||||
const CODEX_MANAGED_ROOT_KEYS: [&str; 3] = ["model_provider", "model", "model_catalog_json"];
|
||||
const CODEX_MANAGED_PROVIDER_KEYS: [&str; 5] = [
|
||||
"name",
|
||||
|
||||
@@ -686,6 +686,23 @@ pub(crate) fn codex_auth_file_has_oauth_tokens(path: &Path) -> bool {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn codex_auth_file_has_api_key(path: &Path, api_key: &str) -> bool {
|
||||
let Ok(content) = fs::read_to_string(path) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(root) = serde_json::from_str::<serde_json::Value>(&content) else {
|
||||
return false;
|
||||
};
|
||||
let api_key_matches = root
|
||||
.get("OPENAI_API_KEY")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_some_and(|value| value == api_key);
|
||||
let mode_matches = root
|
||||
.get("auth_mode")
|
||||
.is_none_or(|value| value.as_str() == Some("apikey"));
|
||||
api_key_matches && mode_matches
|
||||
}
|
||||
|
||||
pub(crate) fn validate_codex_oauth_login(home: &Path) -> Result<(), String> {
|
||||
validate_codex_oauth_login_at(&codex_configuration_directory(home).join("auth.json"))
|
||||
}
|
||||
@@ -738,6 +755,7 @@ pub(crate) fn expected_agent_record_paths(client: AgentClient, paths: &[PathBuf]
|
||||
if client == AgentClient::Codex && paths.len() == 1 {
|
||||
let mut expected = paths.to_vec();
|
||||
expected.push(paths[0].with_file_name(CODEX_MODEL_CATALOG_FILE));
|
||||
expected.push(paths[0].with_file_name("auth.json"));
|
||||
expected
|
||||
} else {
|
||||
paths.to_vec()
|
||||
@@ -3187,7 +3205,13 @@ pub(crate) fn inspect_codex_agent_config(
|
||||
.and_then(|provider| provider.get("requires_openai_auth"))
|
||||
.and_then(toml::Value::as_bool)
|
||||
== Some(true);
|
||||
let configured = root.get("model_provider").and_then(toml::Value::as_str)
|
||||
let auth_path = path.with_file_name("auth.json");
|
||||
let auth_configured = if oauth_configuration {
|
||||
codex_auth_file_has_oauth_tokens(&auth_path)
|
||||
} else {
|
||||
codex_auth_file_has_api_key(&auth_path, api_key)
|
||||
};
|
||||
let configured_without_auth = root.get("model_provider").and_then(toml::Value::as_str)
|
||||
== Some(MANAGED_AGENT_PROVIDER_ID)
|
||||
&& root.get("model_catalog_json").and_then(toml::Value::as_str)
|
||||
== Some(CODEX_MODEL_CATALOG_FILE)
|
||||
@@ -3204,6 +3228,7 @@ pub(crate) fn inspect_codex_agent_config(
|
||||
.and_then(|provider| provider.get("wire_api"))
|
||||
.and_then(toml::Value::as_str)
|
||||
== Some("responses");
|
||||
let configured = configured_without_auth && auth_configured;
|
||||
let model = root
|
||||
.get("model")
|
||||
.and_then(toml::Value::as_str)
|
||||
|
||||
@@ -189,7 +189,14 @@ pub(crate) fn validate_agent_applied_state(
|
||||
.iter()
|
||||
.map(|file| file.path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let codex_paths_without_auth = (client == AgentClient::Codex
|
||||
&& expected_paths
|
||||
.last()
|
||||
.and_then(|path| path.file_name())
|
||||
.is_some_and(|name| name == "auth.json"))
|
||||
.then(|| &expected_paths[..expected_paths.len().saturating_sub(1)]);
|
||||
let valid_paths = state_paths == expected_paths
|
||||
|| codex_paths_without_auth.is_some_and(|expected| state_paths == expected)
|
||||
|| (client == AgentClient::Codex && state_paths.as_slice() == paths)
|
||||
|| (client == AgentClient::ZCode
|
||||
&& paths
|
||||
@@ -1686,6 +1693,7 @@ pub(crate) fn reset_agent_configuration_to_default_with_oauth(
|
||||
path: codex_model_catalog_path(home),
|
||||
after: catalog.to_string(),
|
||||
});
|
||||
updates.push(build_codex_auth_update(home, api_key, oauth_configuration)?);
|
||||
}
|
||||
commit_agent_configuration(
|
||||
client,
|
||||
|
||||
@@ -145,7 +145,7 @@ pub(crate) fn parse_runtime_models(payload: &Value) -> Result<Vec<CodexRuntimeMo
|
||||
models.push(make_runtime_model(slug, display_name));
|
||||
}
|
||||
if seen.insert(normalize_id(&model_alias)) {
|
||||
models.push(make_runtime_model(&model_alias, Some(slug.to_string())));
|
||||
models.push(make_runtime_model(&model_alias, None));
|
||||
}
|
||||
} else if seen.insert(normalize_id(slug)) {
|
||||
models.push(make_runtime_model(slug, display_name));
|
||||
@@ -345,6 +345,15 @@ fn prepare_catalog_with_sources(
|
||||
if let Some(template) = sources.templates.get(&key) {
|
||||
let mut value = template.value.clone();
|
||||
value.insert("slug".to_string(), Value::String(runtime.slug.clone()));
|
||||
value.insert(
|
||||
"display_name".to_string(),
|
||||
Value::String(
|
||||
runtime
|
||||
.display_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| runtime.slug.clone()),
|
||||
),
|
||||
);
|
||||
enable_fast_mode(&mut value);
|
||||
entries.push(CatalogEntry {
|
||||
value,
|
||||
@@ -877,9 +886,9 @@ mod tests {
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["gpt-5.5", "gpt-5.5-xhigh", "visible-alias"]
|
||||
);
|
||||
assert_eq!(models[1].display_name.as_deref(), Some("gpt-5.5"));
|
||||
assert_eq!(models[1].display_name, None);
|
||||
assert_eq!(models[1].context_window, Some(200_000));
|
||||
assert_eq!(models[2].display_name.as_deref(), Some("hidden-source"));
|
||||
assert_eq!(models[2].display_name, None);
|
||||
|
||||
let catalog = prepare_catalog_with_sources(&models, &test_sources()).unwrap();
|
||||
assert_eq!(
|
||||
@@ -890,7 +899,7 @@ mod tests {
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["gpt-5.5", "gpt-5.5-xhigh", "visible-alias"]
|
||||
);
|
||||
assert_eq!(catalog.models[1].alias.as_deref(), Some("gpt-5.5"));
|
||||
assert_eq!(catalog.models[1].alias, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -975,7 +984,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_template_is_preserved_except_for_runtime_slug() {
|
||||
fn known_template_is_preserved_except_for_runtime_identity() {
|
||||
let sources = test_sources();
|
||||
let runtime = runtime(serde_json::json!({"data":[{
|
||||
"id":"a",
|
||||
@@ -989,12 +998,13 @@ mod tests {
|
||||
for (key, expected) in template {
|
||||
if !matches!(
|
||||
key.as_str(),
|
||||
"slug" | "service_tiers" | "additional_speed_tiers"
|
||||
"slug" | "display_name" | "service_tiers" | "additional_speed_tiers"
|
||||
) {
|
||||
assert_eq!(model.get(key), Some(expected), "changed field {key}");
|
||||
}
|
||||
}
|
||||
assert_eq!(model["slug"], "a");
|
||||
assert_eq!(model["display_name"], "Overwrite");
|
||||
assert_eq!(model["nested"]["unknown"], true);
|
||||
assert_eq!(
|
||||
model["service_tiers"],
|
||||
@@ -1007,6 +1017,16 @@ mod tests {
|
||||
assert_eq!(model["additional_speed_tiers"], serde_json::json!(["fast"]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_template_uses_runtime_slug_when_display_name_is_missing() {
|
||||
let runtime = runtime(serde_json::json!({"models":[{"id":"A"}]}));
|
||||
let catalog = prepare_catalog_with_sources(&runtime, &test_sources()).unwrap();
|
||||
let model = &output_models(&catalog)[0];
|
||||
|
||||
assert_eq!(model["slug"], "A");
|
||||
assert_eq!(model["display_name"], "A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fallback_accepts_only_safe_valid_metadata() {
|
||||
let runtime = runtime(serde_json::json!({"models":[{
|
||||
|
||||
@@ -71,6 +71,7 @@ pub(crate) struct CodexSessionPage {
|
||||
codex_home: String,
|
||||
database_paths: Vec<String>,
|
||||
sessions: Vec<CodexSessionSummary>,
|
||||
total_count: usize,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
has_more: bool,
|
||||
@@ -446,15 +447,26 @@ fn list_codex_sessions_from_home(
|
||||
let fetch_limit = offset.saturating_add(limit).saturating_add(1);
|
||||
let (database_paths, mut warnings) = discover_database_paths(codex_home, false);
|
||||
let mut sessions = Vec::new();
|
||||
let mut total_session_ids = HashSet::new();
|
||||
for path in &database_paths {
|
||||
match list_sessions_from_database(path, fetch_limit) {
|
||||
Ok(mut items) => sessions.append(&mut items),
|
||||
Err(error) => warnings.push(error),
|
||||
}
|
||||
match list_session_ids_from_database(path) {
|
||||
Ok(ids) => total_session_ids.extend(ids),
|
||||
Err(error) => warnings.push(error),
|
||||
}
|
||||
}
|
||||
let (mut rollout_sessions, rollout_warnings) = list_sessions_from_rollouts(codex_home);
|
||||
sessions.append(&mut rollout_sessions);
|
||||
warnings.extend(rollout_warnings);
|
||||
total_session_ids.extend(
|
||||
sessions
|
||||
.iter()
|
||||
.map(|session| session_identity_key(&session.id))
|
||||
.filter(|id| !id.is_empty()),
|
||||
);
|
||||
let mut sessions = merge_session_summaries(sessions);
|
||||
sessions.sort_by(|left, right| {
|
||||
right
|
||||
@@ -472,6 +484,7 @@ fn list_codex_sessions_from_home(
|
||||
.map(|path| path.to_string_lossy().to_string())
|
||||
.collect(),
|
||||
sessions,
|
||||
total_count: total_session_ids.len(),
|
||||
offset,
|
||||
limit,
|
||||
has_more,
|
||||
@@ -483,12 +496,7 @@ fn merge_session_summaries(sessions: Vec<CodexSessionSummary>) -> Vec<CodexSessi
|
||||
let mut merged = Vec::<CodexSessionSummary>::new();
|
||||
let mut positions = HashMap::<String, usize>::new();
|
||||
for session in sessions {
|
||||
let key = normalize_thread_id(&session.id);
|
||||
let key = if key.is_empty() {
|
||||
session.id.clone()
|
||||
} else {
|
||||
key
|
||||
};
|
||||
let key = session_identity_key(&session.id);
|
||||
let Some(index) = positions.get(&key).copied() else {
|
||||
positions.insert(key, merged.len());
|
||||
merged.push(session);
|
||||
@@ -719,6 +727,36 @@ fn list_sessions_from_database(
|
||||
Ok(sessions)
|
||||
}
|
||||
|
||||
fn list_session_ids_from_database(path: &Path) -> Result<HashSet<String>, String> {
|
||||
let connection = open_read_only(path)?;
|
||||
let mut ids = HashSet::new();
|
||||
for (table, column) in [("threads", "id"), ("automation_runs", "thread_id")] {
|
||||
let columns = table_columns(&connection, table)?;
|
||||
if !columns.contains(column) {
|
||||
continue;
|
||||
}
|
||||
let sql = format!(
|
||||
"SELECT DISTINCT \"{column}\" FROM \"{table}\" WHERE COALESCE(\"{column}\", '') <> ''"
|
||||
);
|
||||
let mut statement = connection
|
||||
.prepare(&sql)
|
||||
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?;
|
||||
let rows = statement
|
||||
.query_map([], |row| row.get::<_, String>(0))
|
||||
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?;
|
||||
for id in rows
|
||||
.collect::<rusqlite::Result<Vec<_>>>()
|
||||
.map_err(|error| format!("统计会话总数失败 {}: {error}", path.display()))?
|
||||
{
|
||||
let id = session_identity_key(&id);
|
||||
if !id.is_empty() {
|
||||
ids.insert(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(ids)
|
||||
}
|
||||
|
||||
fn list_thread_rows(
|
||||
connection: &Connection,
|
||||
path: &Path,
|
||||
@@ -822,6 +860,15 @@ fn normalize_thread_id(value: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn session_identity_key(value: &str) -> String {
|
||||
let normalized = normalize_thread_id(value);
|
||||
if normalized.is_empty() {
|
||||
value.trim().to_string()
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
}
|
||||
|
||||
fn delete_codex_sessions_from_home(
|
||||
codex_home: &Path,
|
||||
session_ids: Vec<String>,
|
||||
@@ -2421,6 +2468,7 @@ mod tests {
|
||||
|
||||
let page = list_codex_sessions_from_home(&root, 0, 50).unwrap();
|
||||
assert_eq!(page.sessions.len(), 2);
|
||||
assert_eq!(page.total_count, 2);
|
||||
assert_eq!(page.sessions[0].id, "thread-1");
|
||||
assert_eq!(page.sessions[0].title, "First new");
|
||||
assert_eq!(page.sessions[0].updated_at_ms, Some(4000));
|
||||
@@ -2428,6 +2476,7 @@ mod tests {
|
||||
assert!(page.sessions[1].archived);
|
||||
let second_page = list_codex_sessions_from_home(&root, 1, 1).unwrap();
|
||||
assert_eq!(second_page.sessions.len(), 1);
|
||||
assert_eq!(second_page.total_count, 2);
|
||||
assert_eq!(second_page.sessions[0].id, "thread-2");
|
||||
assert!(!second_page.has_more);
|
||||
fs::remove_dir_all(root).unwrap();
|
||||
|
||||
@@ -332,6 +332,11 @@ fn codex_oauth_configuration_uses_openai_auth_with_bearer_token() {
|
||||
|
||||
fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||
fs::write(&path, rendered).unwrap();
|
||||
fs::write(
|
||||
path.with_file_name("auth.json"),
|
||||
r#"{"tokens":{"access_token":"oauth-access-token"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let (configured, model, oauth_configuration) =
|
||||
inspect_codex_agent_config(&path, 8317, DEFAULT_API_KEY).unwrap();
|
||||
assert!(configured);
|
||||
@@ -343,6 +348,12 @@ fn codex_oauth_configuration_uses_openai_auth_with_bearer_token() {
|
||||
#[test]
|
||||
fn codex_api_and_oauth_modes_write_the_same_catalog() {
|
||||
let home = agent_test_home("codex-auth-mode-catalog");
|
||||
fs::create_dir_all(home.join(".codex")).unwrap();
|
||||
fs::write(
|
||||
home.join(".codex/auth.json"),
|
||||
r#"{"tokens":{"access_token":"oauth-access-token"}}"#,
|
||||
)
|
||||
.unwrap();
|
||||
let models = test_agent_models(&["gpt-5.5", "third-party-model"]);
|
||||
let catalog = test_codex_models(&["gpt-5.5", "third-party-model"]);
|
||||
let build = |oauth_configuration| {
|
||||
@@ -366,6 +377,19 @@ fn codex_api_and_oauth_modes_write_the_same_catalog() {
|
||||
let oauth_updates = build(true);
|
||||
|
||||
assert_eq!(api_updates[1].after, oauth_updates[1].after);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&api_updates[2].after).unwrap()["OPENAI_API_KEY"],
|
||||
DEFAULT_API_KEY
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&api_updates[2].after).unwrap()["auth_mode"],
|
||||
"apikey"
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_str::<serde_json::Value>(&oauth_updates[2].after).unwrap()["tokens"]
|
||||
["access_token"]
|
||||
.is_string()
|
||||
);
|
||||
assert!(api_updates[0].after.contains("model_catalog_json"));
|
||||
assert!(oauth_updates[0].after.contains("model_catalog_json"));
|
||||
assert!(!api_updates[0].after.contains("service_tier"));
|
||||
@@ -394,6 +418,11 @@ fn codex_status_requires_a_valid_catalog_containing_the_default_model() {
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
fs::write(
|
||||
config_path.with_file_name("auth.json"),
|
||||
build_codex_api_auth(DEFAULT_API_KEY).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let missing = inspect_agent_config(AgentClient::Codex, &home, 8317, DEFAULT_API_KEY);
|
||||
assert!(!missing.configured);
|
||||
|
||||
@@ -1324,6 +1324,11 @@ fn direct_agent_apply_preserves_unrelated_fields_and_default_reset_removes_them(
|
||||
assert!(applied.contains("[model_providers.other]"));
|
||||
assert!(applied.contains("model = \"gpt-test\""));
|
||||
assert!(applied.contains("experimental_bearer_token = \"custom-agent-key\""));
|
||||
let auth_path = home.join(".codex/auth.json");
|
||||
let auth = serde_json::from_str::<serde_json::Value>(&fs::read_to_string(&auth_path).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(auth["OPENAI_API_KEY"], api_key);
|
||||
assert_eq!(auth["auth_mode"], "apikey");
|
||||
assert!(!agent_backup_path(&path).unwrap().exists());
|
||||
assert!(!agent_backup_path(&codex_model_catalog_path(&home))
|
||||
.unwrap()
|
||||
@@ -1400,6 +1405,24 @@ fn clear_codex_config_removes_only_requested_config_files_and_application_state(
|
||||
assert!(!state_path.exists());
|
||||
assert_eq!(fs::read_to_string(&preserved_path).unwrap(), "keep");
|
||||
assert!(clear_codex_config_files(&home).unwrap().is_empty());
|
||||
|
||||
let models = test_agent_models(&["gpt-test"]);
|
||||
let catalog = test_codex_models(&["gpt-test"]);
|
||||
apply_agent_configuration(
|
||||
AgentClient::Codex,
|
||||
&home,
|
||||
8317,
|
||||
"reapplied-api-key",
|
||||
"gpt-test",
|
||||
&models,
|
||||
Some(&catalog),
|
||||
)
|
||||
.unwrap();
|
||||
assert!(config_path.is_file());
|
||||
let auth = serde_json::from_str::<serde_json::Value>(&fs::read_to_string(&auth_path).unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(auth["OPENAI_API_KEY"], "reapplied-api-key");
|
||||
assert_eq!(auth["auth_mode"], "apikey");
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
|
||||
@@ -384,7 +384,7 @@ fn updating_legacy_codex_state_adds_catalog_without_replacing_original_backup()
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(upgraded.version, AGENT_MODIFICATION_STATE_VERSION);
|
||||
assert_eq!(upgraded.files.len(), 2);
|
||||
assert_eq!(upgraded.files.len(), 3);
|
||||
assert_eq!(
|
||||
fs::read(agent_backup_path(&path).unwrap()).unwrap(),
|
||||
original_config
|
||||
@@ -394,10 +394,13 @@ fn updating_legacy_codex_state_adds_catalog_without_replacing_original_backup()
|
||||
original_catalog
|
||||
);
|
||||
fs::write(&catalog_path, b"{\"models\":[]}\n").unwrap();
|
||||
let auth_path = path.with_file_name("auth.json");
|
||||
assert!(auth_path.is_file());
|
||||
|
||||
disable_agent_modification(AgentClient::Codex, &home, 8317, false).unwrap();
|
||||
assert_eq!(fs::read(&path).unwrap(), original_config);
|
||||
assert_eq!(fs::read(&catalog_path).unwrap(), original_catalog);
|
||||
assert!(!auth_path.exists());
|
||||
fs::remove_dir_all(home).unwrap();
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -541,7 +541,7 @@ export const jaOverrides = {
|
||||
'agents.sessions.title': 'Codex セッション管理',
|
||||
'agents.sessions.description': 'ローカルの Codex セッションをスキャンし、過去のセッションの復元、バックアップ、削除を安全に行います。',
|
||||
'agents.sessions.refresh': '更新',
|
||||
'agents.sessions.currentPage': '現在のページ',
|
||||
'agents.sessions.total': 'セッション総数',
|
||||
'agents.sessions.active': '未アーカイブ',
|
||||
'agents.sessions.archived': 'アーカイブ済み',
|
||||
'agents.sessions.database': 'データベース',
|
||||
|
||||
@@ -541,7 +541,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'agents.sessions.title': 'Codex Session Management',
|
||||
'agents.sessions.description': 'Scan local Codex sessions and safely restore historical sessions, back them up, or delete them.',
|
||||
'agents.sessions.refresh': 'Refresh',
|
||||
'agents.sessions.currentPage': 'Current page',
|
||||
'agents.sessions.total': 'Total sessions',
|
||||
'agents.sessions.active': 'Active',
|
||||
'agents.sessions.archived': 'Archived',
|
||||
'agents.sessions.database': 'Database source',
|
||||
|
||||
@@ -539,7 +539,7 @@ export const zhCN = {
|
||||
'agents.sessions.title': 'Codex 会话管理',
|
||||
'agents.sessions.description': '扫描本地 Codex 会话,并安全地恢复历史会话、备份或删除。',
|
||||
'agents.sessions.refresh': '刷新',
|
||||
'agents.sessions.currentPage': '当前页',
|
||||
'agents.sessions.total': '会话总数',
|
||||
'agents.sessions.active': '未归档',
|
||||
'agents.sessions.archived': '已归档',
|
||||
'agents.sessions.database': '数据库来源',
|
||||
|
||||
@@ -287,7 +287,7 @@ export function CodexSessionsPanel() {
|
||||
</div>
|
||||
|
||||
<div className="codex-session-metrics">
|
||||
<div><span>{t('agents.sessions.currentPage')}</span><strong>{sessions.length}</strong></div>
|
||||
<div><span>{t('agents.sessions.total')}</span><strong>{page?.totalCount ?? sessions.length}</strong></div>
|
||||
<div><span>{t('agents.sessions.active')}</span><strong>{pageCounts.active}</strong></div>
|
||||
<div><span>{t('agents.sessions.archived')}</span><strong>{pageCounts.archived}</strong></div>
|
||||
<div
|
||||
|
||||
@@ -12,6 +12,7 @@ export type CodexSessionPage = {
|
||||
codexHome: string;
|
||||
databasePaths: string[];
|
||||
sessions: CodexSessionSummary[];
|
||||
totalCount: number;
|
||||
offset: number;
|
||||
limit: number;
|
||||
hasMore: boolean;
|
||||
|
||||
Reference in new issue
Block a user