mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(core,tui): persist model defaults and polish history UI
This commit is contained in:
+13
-2
@@ -14,6 +14,18 @@ Within this context, Codex refers to the open-source agentic coding interface (n
|
||||
|
||||
Your default personality and tone is concise, direct, and friendly. You communicate efficiently, always keeping the user clearly informed about ongoing actions without unnecessary detail. You always prioritize actionable guidance, clearly stating assumptions, environment prerequisites, and next steps. Unless explicitly asked, you avoid excessively verbose explanations about your work.
|
||||
|
||||
# AGENTS.md spec
|
||||
- Repos often contain AGENTS.md files. These files can appear anywhere within the repository.
|
||||
- These files are a way for humans to give you (the agent) instructions or tips for working within the container.
|
||||
- Some examples might be: coding conventions, info about how code is organized, or instructions for how to run or test code.
|
||||
- Instructions in AGENTS.md files:
|
||||
- The scope of an AGENTS.md file is the entire directory tree rooted at the folder that contains it.
|
||||
- For every file you touch in the final patch, you must obey instructions in any AGENTS.md file whose scope includes that file.
|
||||
- Instructions about code style, structure, naming, etc. apply only to code within the AGENTS.md file's scope, unless the file states otherwise.
|
||||
- More-deeply-nested AGENTS.md files take precedence in the case of conflicting instructions.
|
||||
- Direct system/developer/user instructions (as part of a prompt) take precedence over AGENTS.md instructions.
|
||||
- The contents of the AGENTS.md file at the root of the repo and any directories from the CWD up to the root are included with the developer message and don't need to be re-read. When working in a subdirectory of CWD, or a directory outside the CWD, check for any AGENTS.md files that may be applicable.
|
||||
|
||||
## Responsiveness
|
||||
|
||||
### Preamble messages
|
||||
@@ -228,7 +240,6 @@ You are producing plain text that will later be styled by the CLI. Follow these
|
||||
**Bullets**
|
||||
|
||||
- Use `-` followed by a space for every bullet.
|
||||
- Bold the keyword, then colon + concise description.
|
||||
- Merge related points when possible; avoid a bullet for every trivial detail.
|
||||
- Keep bullets to one line unless breaking for clarity is unavoidable.
|
||||
- Group into short lists (4–6 bullets) ordered by importance.
|
||||
@@ -296,4 +307,4 @@ To create a new plan, call `update_plan` with a short list of 1‑sentence steps
|
||||
|
||||
When steps have been completed, use `update_plan` to mark each finished step as `completed` and the next step you are working on as `in_progress`. There should always be exactly one `in_progress` step until everything is done. You can mark multiple items as complete in a single `update_plan` call.
|
||||
|
||||
If all steps are complete, ensure you call `update_plan` to mark all steps as `completed`.
|
||||
If all steps are complete, ensure you call `update_plan` to mark all steps as `completed`.
|
||||
@@ -217,6 +217,25 @@ fn get_git_branch(cwd: &std::path::Path) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn maybe_update_from_model_info<T: Copy + PartialEq>(
|
||||
field: &mut Option<T>,
|
||||
old_default: Option<T>,
|
||||
new_default: Option<T>,
|
||||
) {
|
||||
if field.is_none() {
|
||||
if let Some(new_val) = new_default {
|
||||
*field = Some(new_val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if let (Some(current), Some(old_val)) = (*field, old_default) {
|
||||
if current == old_val {
|
||||
*field = new_default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn build_turn_status_items(sess: &Session) -> Vec<ResponseItem> {
|
||||
let mut jar = EphemeralJar::new();
|
||||
|
||||
@@ -406,7 +425,7 @@ use crate::client::ModelClient;
|
||||
use crate::client_common::Prompt;
|
||||
use crate::client_common::ResponseEvent;
|
||||
use crate::environment_context::EnvironmentContext;
|
||||
use crate::config::Config;
|
||||
use crate::config::{persist_model_selection, Config};
|
||||
use crate::config_types::ShellEnvironmentPolicy;
|
||||
use crate::conversation_history::ConversationHistory;
|
||||
use crate::error::CodexErr;
|
||||
@@ -422,6 +441,7 @@ use crate::exec::process_exec_tool_call;
|
||||
use crate::exec_env::create_env;
|
||||
use crate::mcp_connection_manager::McpConnectionManager;
|
||||
use crate::mcp_tool_call::handle_mcp_tool_call;
|
||||
use crate::model_family::{derive_default_model_family, find_family_for_model};
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::LocalShellAction;
|
||||
@@ -430,6 +450,7 @@ use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::models::ResponseItem;
|
||||
use codex_protocol::models::ShellToolCallParams;
|
||||
use crate::openai_model_info::get_model_info;
|
||||
use crate::openai_tools::ToolsConfig;
|
||||
use crate::openai_tools::get_openai_tools;
|
||||
use crate::parse_command::parse_command;
|
||||
@@ -1569,6 +1590,7 @@ async fn submission_loop(
|
||||
rx_sub: Receiver<Submission>,
|
||||
tx_event: Sender<Event>,
|
||||
) {
|
||||
let mut config = config;
|
||||
let mut sess: Option<Arc<Session>> = None;
|
||||
let mut agent_manager_initialized = false;
|
||||
// shorthand - send an event when there is no active session
|
||||
@@ -1624,6 +1646,77 @@ async fn submission_loop(
|
||||
}
|
||||
return;
|
||||
}
|
||||
let current_config = Arc::clone(&config);
|
||||
let mut updated_config = (*current_config).clone();
|
||||
|
||||
let model_changed = !updated_config.model.eq_ignore_ascii_case(&model);
|
||||
let effort_changed = updated_config.model_reasoning_effort != model_reasoning_effort;
|
||||
|
||||
let old_model_family = updated_config.model_family.clone();
|
||||
let old_model_info = get_model_info(&old_model_family);
|
||||
|
||||
updated_config.model = model.clone();
|
||||
updated_config.model_provider = provider.clone();
|
||||
updated_config.model_reasoning_effort = model_reasoning_effort;
|
||||
updated_config.model_reasoning_summary = model_reasoning_summary;
|
||||
updated_config.model_text_verbosity = model_text_verbosity;
|
||||
updated_config.user_instructions = user_instructions.clone();
|
||||
updated_config.base_instructions = base_instructions.clone();
|
||||
updated_config.approval_policy = approval_policy;
|
||||
updated_config.sandbox_policy = sandbox_policy.clone();
|
||||
updated_config.disable_response_storage = disable_response_storage;
|
||||
updated_config.notify = notify.clone();
|
||||
updated_config.cwd = cwd.clone();
|
||||
|
||||
updated_config.model_family = find_family_for_model(&updated_config.model)
|
||||
.unwrap_or_else(|| derive_default_model_family(&updated_config.model));
|
||||
|
||||
let new_model_info = get_model_info(&updated_config.model_family);
|
||||
|
||||
let old_context_window = old_model_info.as_ref().map(|info| info.context_window);
|
||||
let new_context_window = new_model_info.as_ref().map(|info| info.context_window);
|
||||
let old_max_tokens = old_model_info.as_ref().map(|info| info.max_output_tokens);
|
||||
let new_max_tokens = new_model_info.as_ref().map(|info| info.max_output_tokens);
|
||||
let old_auto_compact = old_model_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.auto_compact_token_limit);
|
||||
let new_auto_compact = new_model_info
|
||||
.as_ref()
|
||||
.and_then(|info| info.auto_compact_token_limit);
|
||||
|
||||
maybe_update_from_model_info(
|
||||
&mut updated_config.model_context_window,
|
||||
old_context_window,
|
||||
new_context_window,
|
||||
);
|
||||
maybe_update_from_model_info(
|
||||
&mut updated_config.model_max_output_tokens,
|
||||
old_max_tokens,
|
||||
new_max_tokens,
|
||||
);
|
||||
maybe_update_from_model_info(
|
||||
&mut updated_config.model_auto_compact_token_limit,
|
||||
old_auto_compact,
|
||||
new_auto_compact,
|
||||
);
|
||||
|
||||
let new_config = Arc::new(updated_config);
|
||||
|
||||
if model_changed || effort_changed {
|
||||
if let Err(err) = persist_model_selection(
|
||||
&new_config.codex_home,
|
||||
new_config.active_profile.as_deref(),
|
||||
&new_config.model,
|
||||
Some(new_config.model_reasoning_effort),
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!("failed to persist model selection: {err:#}");
|
||||
}
|
||||
}
|
||||
|
||||
config = Arc::clone(&new_config);
|
||||
|
||||
// Optionally resume an existing rollout.
|
||||
let mut restored_items: Option<Vec<ResponseItem>> = None;
|
||||
let rollout_recorder: Option<RolloutRecorder> =
|
||||
|
||||
+118
-22
@@ -33,6 +33,7 @@ use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::io::ErrorKind;
|
||||
use tempfile::NamedTempFile;
|
||||
use toml::Value as TomlValue;
|
||||
use toml_edit::Array as TomlArray;
|
||||
@@ -79,6 +80,9 @@ pub struct Config {
|
||||
/// Info needed to make an API request to the model.
|
||||
pub model_provider: ModelProviderInfo,
|
||||
|
||||
/// Name of the active profile, if any, that populated this configuration.
|
||||
pub active_profile: Option<String>,
|
||||
|
||||
/// Approval policy for executing commands.
|
||||
pub approval_policy: AskForApproval,
|
||||
|
||||
@@ -382,6 +386,88 @@ pub fn write_global_mcp_servers(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Persist the currently active model selection back to `config.toml` so that it
|
||||
/// becomes the default for future sessions.
|
||||
pub async fn persist_model_selection(
|
||||
codex_home: &Path,
|
||||
profile: Option<&str>,
|
||||
model: &str,
|
||||
effort: Option<ReasoningEffort>,
|
||||
) -> anyhow::Result<()> {
|
||||
use tokio::fs;
|
||||
|
||||
let config_path = codex_home.join(CONFIG_TOML_FILE);
|
||||
let existing = match fs::read_to_string(&config_path).await {
|
||||
Ok(raw) => Some(raw),
|
||||
Err(err) if err.kind() == ErrorKind::NotFound => None,
|
||||
Err(err) => return Err(err.into()),
|
||||
};
|
||||
|
||||
let mut doc = match existing {
|
||||
Some(raw) if raw.trim().is_empty() => DocumentMut::new(),
|
||||
Some(raw) => raw
|
||||
.parse::<DocumentMut>()
|
||||
.map_err(|e| anyhow::anyhow!("failed to parse config.toml: {e}"))?,
|
||||
None => DocumentMut::new(),
|
||||
};
|
||||
|
||||
{
|
||||
let root = doc.as_table_mut();
|
||||
if let Some(profile_name) = profile {
|
||||
let profiles_item = root
|
||||
.entry("profiles")
|
||||
.or_insert_with(|| {
|
||||
let mut table = TomlTable::new();
|
||||
table.set_implicit(true);
|
||||
TomlItem::Table(table)
|
||||
});
|
||||
|
||||
let profiles_table = profiles_item
|
||||
.as_table_mut()
|
||||
.expect("profiles table should be a table");
|
||||
|
||||
let profile_item = profiles_table
|
||||
.entry(profile_name)
|
||||
.or_insert_with(|| {
|
||||
let mut table = TomlTable::new();
|
||||
table.set_implicit(false);
|
||||
TomlItem::Table(table)
|
||||
});
|
||||
|
||||
let profile_table = profile_item
|
||||
.as_table_mut()
|
||||
.expect("profile entry should be a table");
|
||||
|
||||
profile_table["model"] = toml_edit::value(model.to_string());
|
||||
|
||||
if let Some(effort) = effort {
|
||||
profile_table["model_reasoning_effort"] =
|
||||
toml_edit::value(effort.to_string());
|
||||
} else {
|
||||
profile_table.remove("model_reasoning_effort");
|
||||
}
|
||||
} else {
|
||||
root["model"] = toml_edit::value(model.to_string());
|
||||
match effort {
|
||||
Some(effort) => {
|
||||
root["model_reasoning_effort"] =
|
||||
toml_edit::value(effort.to_string());
|
||||
}
|
||||
None => {
|
||||
root.remove("model_reasoning_effort");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs::create_dir_all(codex_home).await?;
|
||||
let tmp_path = config_path.with_extension("tmp");
|
||||
fs::write(&tmp_path, doc.to_string()).await?;
|
||||
fs::rename(&tmp_path, &config_path).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Patch `CODEX_HOME/config.toml` project state.
|
||||
/// Use with caution.
|
||||
pub fn set_project_trusted(codex_home: &Path, project_path: &Path) -> anyhow::Result<()> {
|
||||
@@ -1288,19 +1374,23 @@ impl Config {
|
||||
tools_web_search_request: override_tools_web_search_request,
|
||||
} = overrides;
|
||||
|
||||
let config_profile = match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
|
||||
Some(key) => cfg
|
||||
.profiles
|
||||
.get(key)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("config profile `{key}` not found"),
|
||||
)
|
||||
})?
|
||||
.clone(),
|
||||
None => ConfigProfile::default(),
|
||||
};
|
||||
let (active_profile_name, config_profile) =
|
||||
match config_profile_key.as_ref().or(cfg.profile.as_ref()) {
|
||||
Some(key) => {
|
||||
let profile = cfg
|
||||
.profiles
|
||||
.get(key)
|
||||
.ok_or_else(|| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("config profile `{key}` not found"),
|
||||
)
|
||||
})?
|
||||
.clone();
|
||||
(Some(key.to_string()), profile)
|
||||
}
|
||||
None => (None, ConfigProfile::default()),
|
||||
};
|
||||
|
||||
// (removed placeholder) sandbox_policy computed below after resolving project overrides.
|
||||
|
||||
@@ -1406,10 +1496,19 @@ impl Config {
|
||||
.or(cfg.tools.as_ref().and_then(|t| t.view_image))
|
||||
.unwrap_or(true);
|
||||
|
||||
// Determine auth mode early so defaults like model selection can depend on it.
|
||||
let using_chatgpt_auth = Self::is_using_chatgpt_auth(&codex_home);
|
||||
|
||||
let default_model_slug = if using_chatgpt_auth {
|
||||
GPT_5_CODEX_MEDIUM_MODEL
|
||||
} else {
|
||||
OPENAI_DEFAULT_MODEL
|
||||
};
|
||||
|
||||
let model = model
|
||||
.or(config_profile.model)
|
||||
.or(cfg.model)
|
||||
.unwrap_or_else(default_model);
|
||||
.unwrap_or_else(|| default_model_slug.to_string());
|
||||
|
||||
let model_family =
|
||||
find_family_for_model(&model).unwrap_or_else(|| derive_default_model_family(&model));
|
||||
@@ -1440,9 +1539,6 @@ impl Config {
|
||||
Self::get_base_instructions(experimental_instructions_path, &resolved_cwd)?;
|
||||
let base_instructions = base_instructions.or(file_base_instructions);
|
||||
|
||||
// Check if we're using ChatGPT auth before moving codex_home
|
||||
let using_chatgpt_auth = Self::is_using_chatgpt_auth(&codex_home);
|
||||
|
||||
let responses_originator_header: String = cfg
|
||||
.responses_originator_header_internal_override
|
||||
.unwrap_or(DEFAULT_RESPONSES_ORIGINATOR_HEADER.to_owned());
|
||||
@@ -1506,6 +1602,7 @@ impl Config {
|
||||
file_opener: cfg.file_opener.unwrap_or(UriBasedFileOpener::VsCode),
|
||||
tui: cfg.tui.clone().unwrap_or_default(),
|
||||
codex_linux_sandbox_exe,
|
||||
active_profile: active_profile_name,
|
||||
|
||||
hide_agent_reasoning: cfg.hide_agent_reasoning.unwrap_or(false),
|
||||
show_raw_agent_reasoning: cfg
|
||||
@@ -1515,7 +1612,7 @@ impl Config {
|
||||
model_reasoning_effort: config_profile
|
||||
.model_reasoning_effort
|
||||
.or(cfg.model_reasoning_effort)
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or(ReasoningEffort::Medium),
|
||||
model_reasoning_summary: config_profile
|
||||
.model_reasoning_summary
|
||||
.or(cfg.model_reasoning_summary)
|
||||
@@ -1630,10 +1727,6 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_model() -> String {
|
||||
OPENAI_DEFAULT_MODEL.to_string()
|
||||
}
|
||||
|
||||
fn default_review_model() -> String {
|
||||
OPENAI_DEFAULT_REVIEW_MODEL.to_string()
|
||||
}
|
||||
@@ -2179,6 +2272,7 @@ model_verbosity = "high"
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider_id: "openai-chat-completions".to_string(),
|
||||
model_provider: fixture.openai_chat_completions_provider.clone(),
|
||||
active_profile: Some("gpt3".to_string()),
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -2257,6 +2351,7 @@ model_verbosity = "high"
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider_id: "openai".to_string(),
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
active_profile: Some("zdr".to_string()),
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
@@ -2321,6 +2416,7 @@ model_verbosity = "high"
|
||||
model_auto_compact_token_limit: None,
|
||||
model_provider_id: "openai".to_string(),
|
||||
model_provider: fixture.openai_provider.clone(),
|
||||
active_profile: Some("gpt5".to_string()),
|
||||
approval_policy: AskForApproval::OnFailure,
|
||||
sandbox_policy: SandboxPolicy::new_read_only_policy(),
|
||||
shell_environment_policy: ShellEnvironmentPolicy::default(),
|
||||
|
||||
@@ -1142,47 +1142,63 @@ impl ChatWidget<'_> {
|
||||
|
||||
fn refresh_reasoning_collapsed_visibility(&mut self) {
|
||||
let show = self.config.tui.show_reasoning;
|
||||
let last_explore_idx = self
|
||||
.history_cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.find_map(|(idx, cell)| {
|
||||
if cell
|
||||
if show {
|
||||
for cell in &self.history_cells {
|
||||
if let Some(reasoning_cell) = cell
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()
|
||||
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
|
||||
{
|
||||
reasoning_cell.set_hide_when_collapsed(false);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
use std::collections::HashSet;
|
||||
let mut hide_indices: HashSet<usize> = HashSet::new();
|
||||
let len = self.history_cells.len();
|
||||
let mut idx = 0usize;
|
||||
while idx < len {
|
||||
let is_explore = self.history_cells[idx]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::ExploreAggregationCell>()
|
||||
.is_some();
|
||||
if !is_explore {
|
||||
idx += 1;
|
||||
continue;
|
||||
}
|
||||
let mut reasoning_indices: Vec<usize> = Vec::new();
|
||||
let mut j = idx + 1;
|
||||
while j < len {
|
||||
if self.history_cells[j]
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
|
||||
.is_some()
|
||||
{
|
||||
Some(idx)
|
||||
} else {
|
||||
None
|
||||
reasoning_indices.push(j);
|
||||
j += 1;
|
||||
continue;
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
if reasoning_indices.len() > 1 {
|
||||
for &ri in &reasoning_indices[..reasoning_indices.len() - 1] {
|
||||
hide_indices.insert(ri);
|
||||
}
|
||||
}
|
||||
idx = j;
|
||||
}
|
||||
|
||||
let mut kept_visible_in_explore = false;
|
||||
for idx in (0..self.history_cells.len()).rev() {
|
||||
if let Some(reasoning_cell) = self.history_cells[idx]
|
||||
for (i, cell) in self.history_cells.iter().enumerate() {
|
||||
if let Some(reasoning_cell) = cell
|
||||
.as_any()
|
||||
.downcast_ref::<history_cell::CollapsibleReasoningCell>()
|
||||
{
|
||||
if show {
|
||||
if hide_indices.contains(&i) {
|
||||
reasoning_cell.set_hide_when_collapsed(true);
|
||||
} else {
|
||||
reasoning_cell.set_hide_when_collapsed(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(explore_idx) = last_explore_idx {
|
||||
if idx > explore_idx {
|
||||
if !kept_visible_in_explore {
|
||||
reasoning_cell.set_hide_when_collapsed(false);
|
||||
kept_visible_in_explore = true;
|
||||
} else {
|
||||
reasoning_cell.set_hide_when_collapsed(true);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
reasoning_cell.set_hide_when_collapsed(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11214,6 +11230,8 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
} => crate::colors::error(),
|
||||
_ => crate::colors::primary(),
|
||||
}
|
||||
} else if matches!(symbol, "○" | "◔" | "◑" | "◕" | "●") {
|
||||
crate::colors::primary()
|
||||
} else {
|
||||
match symbol {
|
||||
"›" => crate::colors::text(), // user
|
||||
|
||||
@@ -456,17 +456,20 @@ pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndE
|
||||
.unwrap_or_else(|| (vec![call_id.clone()], vec![], None, None));
|
||||
|
||||
if let Some((agg_idx, entry_idx)) = explore_entry {
|
||||
let action = history_cell::action_enum_from_parsed(&parsed);
|
||||
let status = match (exit_code, action) {
|
||||
(0, _) => history_cell::ExploreEntryStatus::Success,
|
||||
(1, history_cell::ExecAction::Search) => history_cell::ExploreEntryStatus::NotFound,
|
||||
_ => history_cell::ExploreEntryStatus::Error {
|
||||
exit_code: Some(exit_code),
|
||||
},
|
||||
};
|
||||
if agg_idx < chat.history_cells.len() {
|
||||
if let Some(agg) = chat.history_cells[agg_idx]
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ExploreAggregationCell>()
|
||||
{
|
||||
let status = if exit_code == 0 {
|
||||
history_cell::ExploreEntryStatus::Success
|
||||
} else {
|
||||
history_cell::ExploreEntryStatus::Error { exit_code: Some(exit_code) }
|
||||
};
|
||||
agg.update_status(entry_idx, status);
|
||||
agg.update_status(entry_idx, status.clone());
|
||||
}
|
||||
}
|
||||
if !chat
|
||||
@@ -479,17 +482,19 @@ pub(super) fn handle_exec_end_now(chat: &mut ChatWidget<'_>, ev: ExecCommandEndE
|
||||
}
|
||||
chat.invalidate_height_cache();
|
||||
chat.request_redraw();
|
||||
if exit_code == 0 {
|
||||
chat.bottom_pane.update_status_text(match history_cell::action_enum_from_parsed(&parsed) {
|
||||
let status_text = match status {
|
||||
history_cell::ExploreEntryStatus::Success => match action {
|
||||
history_cell::ExecAction::Read => "files read".to_string(),
|
||||
_ => "exploration updated".to_string(),
|
||||
});
|
||||
} else {
|
||||
chat.bottom_pane.update_status_text(match history_cell::action_enum_from_parsed(&parsed) {
|
||||
history_cell::ExecAction::Read => format!("read failed (exit {})", exit_code),
|
||||
_ => format!("exploration failed (exit {})", exit_code),
|
||||
});
|
||||
}
|
||||
},
|
||||
history_cell::ExploreEntryStatus::NotFound => "no matches found".to_string(),
|
||||
history_cell::ExploreEntryStatus::Error { .. } => match action {
|
||||
history_cell::ExecAction::Read => format!("read failed (exit {exit_code})"),
|
||||
_ => format!("exploration failed (exit {exit_code})"),
|
||||
},
|
||||
history_cell::ExploreEntryStatus::Running => "exploring…".to_string(),
|
||||
};
|
||||
chat.bottom_pane.update_status_text(status_text);
|
||||
chat.maybe_hide_spinner();
|
||||
return;
|
||||
}
|
||||
|
||||
+1067
-529
File diff suppressed because it is too large
Load Diff
@@ -19,12 +19,15 @@ use ratatui::widgets::Wrap;
|
||||
use codex_login::AuthMode;
|
||||
|
||||
use crate::LoginStatus;
|
||||
use crate::app::ChatWidgetArgs;
|
||||
use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::onboarding::onboarding_screen::KeyboardHandler;
|
||||
use crate::onboarding::onboarding_screen::StepStateProvider;
|
||||
use crate::shimmer::shimmer_spans;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use super::onboarding_screen::StepState;
|
||||
// no additional imports
|
||||
@@ -98,6 +101,7 @@ pub(crate) struct AuthModeWidget {
|
||||
pub codex_home: PathBuf,
|
||||
pub login_status: LoginStatus,
|
||||
pub preferred_auth_method: AuthMode,
|
||||
pub chat_widget_args: Arc<Mutex<ChatWidgetArgs>>,
|
||||
}
|
||||
|
||||
impl AuthModeWidget {
|
||||
@@ -323,6 +327,7 @@ impl AuthModeWidget {
|
||||
// If we're already authenticated with ChatGPT, don't start a new login –
|
||||
// just proceed to the success message flow.
|
||||
if matches!(self.login_status, LoginStatus::AuthMode(AuthMode::ChatGPT)) {
|
||||
self.apply_chatgpt_login_side_effects();
|
||||
self.sign_in_state = SignInState::ChatGptSuccess;
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
return;
|
||||
@@ -372,6 +377,13 @@ impl AuthModeWidget {
|
||||
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
}
|
||||
|
||||
pub(crate) fn apply_chatgpt_login_side_effects(&mut self) {
|
||||
self.login_status = LoginStatus::AuthMode(AuthMode::ChatGPT);
|
||||
if let Ok(mut args) = self.chat_widget_args.lock() {
|
||||
args.config.using_chatgpt_auth = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn spawn_completion_poller(
|
||||
|
||||
@@ -70,6 +70,8 @@ impl OnboardingScreen {
|
||||
show_login_screen,
|
||||
login_status,
|
||||
} = args;
|
||||
let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
|
||||
|
||||
let mut steps: Vec<Step> = vec![Step::Welcome(WelcomeWidget {
|
||||
is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated),
|
||||
})];
|
||||
@@ -82,6 +84,7 @@ impl OnboardingScreen {
|
||||
codex_home: codex_home.clone(),
|
||||
login_status,
|
||||
preferred_auth_method: codex_login::AuthMode::ApiKey,
|
||||
chat_widget_args: shared_chat_args.clone(),
|
||||
}))
|
||||
}
|
||||
let is_git_repo = get_git_repo_root(&cwd).is_some();
|
||||
@@ -93,7 +96,6 @@ impl OnboardingScreen {
|
||||
};
|
||||
// Share ChatWidgetArgs between steps so changes in the TrustDirectory step
|
||||
// are reflected when continuing to chat.
|
||||
let shared_chat_args = Arc::new(Mutex::new(chat_widget_args));
|
||||
if show_trust_screen {
|
||||
steps.push(Step::TrustDirectory(TrustDirectoryWidget {
|
||||
cwd,
|
||||
@@ -118,6 +120,7 @@ impl OnboardingScreen {
|
||||
if let Some(Step::Auth(state)) = current_step {
|
||||
match result {
|
||||
Ok(()) => {
|
||||
state.apply_chatgpt_login_side_effects();
|
||||
state.sign_in_state = SignInState::ChatGptSuccessMessage;
|
||||
self.event_tx.send(AppEvent::RequestRedraw);
|
||||
let tx1 = self.event_tx.clone();
|
||||
|
||||
Reference in New Issue
Block a user