mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
Merge 6fbc466cc5 into sapling-pr-archive-bolinfest
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
# Example announcement tips for Codex TUI.
|
||||
# Each [[announcements]] entry is evaluated in order; the last matching one is shown.
|
||||
# Dates are UTC, formatted as YYYY-MM-DD. The from_date is inclusive and the to_date is exclusive.
|
||||
# version_regex matches against the CLI version (env!("CARGO_PKG_VERSION")); omit to apply to all versions.
|
||||
# target_app specify which app should display the announcement (cli, vsce, ...).
|
||||
|
||||
[[announcements]]
|
||||
content = "Welcome to Codex! Check out the new onboarding flow."
|
||||
from_date = "2024-10-01"
|
||||
to_date = "2024-10-15"
|
||||
target_app = "cli"
|
||||
|
||||
[[announcements]]
|
||||
content = "This is a test announcement"
|
||||
version_regex = "^0\\.0\\.0$"
|
||||
to_date = "2026-01-10"
|
||||
@@ -26,6 +26,7 @@ use codex_execpolicy::ExecPolicyCheckCommand;
|
||||
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
|
||||
use codex_tui::AppExitInfo;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
use codex_tui::ExitReason;
|
||||
use codex_tui::update_action::UpdateAction;
|
||||
use codex_tui2 as tui2;
|
||||
use owo_colors::OwoColorize;
|
||||
@@ -313,6 +314,14 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
|
||||
|
||||
/// Handle the app exit and print the results. Optionally run the update action.
|
||||
fn handle_app_exit(exit_info: AppExitInfo) -> anyhow::Result<()> {
|
||||
match exit_info.exit_reason {
|
||||
ExitReason::Fatal(message) => {
|
||||
eprintln!("ERROR: {message}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
ExitReason::UserRequested => { /* normal exit */ }
|
||||
}
|
||||
|
||||
let update_action = exit_info.update_action;
|
||||
let color_enabled = supports_color::on(Stream::Stdout).is_some();
|
||||
for line in format_exit_messages(exit_info, color_enabled) {
|
||||
@@ -833,6 +842,7 @@ mod tests {
|
||||
token_usage,
|
||||
thread_id: conversation.map(ThreadId::from_string).map(Result::unwrap),
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,6 +852,7 @@ mod tests {
|
||||
token_usage: TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
};
|
||||
let lines = format_exit_messages(exit_info, false);
|
||||
assert!(lines.is_empty());
|
||||
|
||||
7
codex-rs/core/hierarchical_agents_message.md
Normal file
7
codex-rs/core/hierarchical_agents_message.md
Normal file
@@ -0,0 +1,7 @@
|
||||
Files called AGENTS.md commonly appear in many places inside a container - at "/", in "~", deep within git repositories, or in any other directory; their location is not limited to version-controlled folders.
|
||||
|
||||
Their purpose is to pass along human guidance to you, the agent. Such guidance can include coding standards, explanations of the project layout, steps for building or testing, and even wording that must accompany a GitHub pull-request description produced by the agent; all of it is to be followed.
|
||||
|
||||
Each AGENTS.md governs the entire directory that contains it and every child directory beneath that point. Whenever you change a file, you have to comply with every AGENTS.md whose scope covers that file. Naming conventions, stylistic rules and similar directives are restricted to the code that falls inside that scope unless the document explicitly states otherwise.
|
||||
|
||||
When two AGENTS.md files disagree, the one located deeper in the directory structure overrides the higher-level file, while instructions given directly in the prompt by the system, developer, or user outrank any AGENTS.md content.
|
||||
@@ -86,6 +86,8 @@ pub enum Feature {
|
||||
RemoteModels,
|
||||
/// Experimental shell snapshotting.
|
||||
ShellSnapshot,
|
||||
/// Append additional AGENTS.md guidance to user instructions.
|
||||
HierarchicalAgents,
|
||||
/// Experimental TUI v2 (viewport) implementation.
|
||||
Tui2,
|
||||
/// Enforce UTF8 output in Powershell.
|
||||
@@ -352,6 +354,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
},
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::HierarchicalAgents,
|
||||
key: "hierarchical_agents",
|
||||
stage: Stage::Experimental,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::ApplyPatchFreeform,
|
||||
key: "apply_patch_freeform",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
//! 3. We do **not** walk past the Git root.
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::features::Feature;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::render_skills_section;
|
||||
use dunce::canonicalize as normalize_path;
|
||||
@@ -21,6 +22,9 @@ use std::path::PathBuf;
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tracing::error;
|
||||
|
||||
pub(crate) const HIERARCHICAL_AGENTS_MESSAGE: &str =
|
||||
include_str!("../hierarchical_agents_message.md");
|
||||
|
||||
/// Default filename scanned for project-level docs.
|
||||
pub const DEFAULT_PROJECT_DOC_FILENAME: &str = "AGENTS.md";
|
||||
/// Preferred local override for project-level docs.
|
||||
@@ -36,35 +40,46 @@ pub(crate) async fn get_user_instructions(
|
||||
config: &Config,
|
||||
skills: Option<&[SkillMetadata]>,
|
||||
) -> Option<String> {
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
let project_docs = read_project_docs(config).await;
|
||||
|
||||
let project_docs = match read_project_docs(config).await {
|
||||
Ok(docs) => docs,
|
||||
let mut output = String::new();
|
||||
|
||||
if let Some(instructions) = config.user_instructions.clone() {
|
||||
output.push_str(&instructions);
|
||||
}
|
||||
|
||||
match project_docs {
|
||||
Ok(Some(docs)) => {
|
||||
if !output.is_empty() {
|
||||
output.push_str(PROJECT_DOC_SEPARATOR);
|
||||
}
|
||||
output.push_str(&docs);
|
||||
}
|
||||
Ok(None) => {}
|
||||
Err(e) => {
|
||||
error!("error trying to find project doc: {e:#}");
|
||||
return config.user_instructions.clone();
|
||||
}
|
||||
};
|
||||
|
||||
let combined_project_docs = merge_project_docs_with_skills(project_docs, skills_section);
|
||||
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(instructions) = config.user_instructions.clone() {
|
||||
parts.push(instructions);
|
||||
}
|
||||
|
||||
if let Some(project_doc) = combined_project_docs {
|
||||
if !parts.is_empty() {
|
||||
parts.push(PROJECT_DOC_SEPARATOR.to_string());
|
||||
let skills_section = skills.and_then(render_skills_section);
|
||||
if let Some(skills_section) = skills_section {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
parts.push(project_doc);
|
||||
output.push_str(&skills_section);
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
None
|
||||
if config.features.enabled(Feature::HierarchicalAgents) {
|
||||
if !output.is_empty() {
|
||||
output.push_str("\n\n");
|
||||
}
|
||||
output.push_str(HIERARCHICAL_AGENTS_MESSAGE);
|
||||
}
|
||||
|
||||
if !output.is_empty() {
|
||||
Some(output)
|
||||
} else {
|
||||
Some(parts.concat())
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,18 +232,6 @@ fn candidate_filenames<'a>(config: &'a Config) -> Vec<&'a str> {
|
||||
names
|
||||
}
|
||||
|
||||
fn merge_project_docs_with_skills(
|
||||
project_doc: Option<String>,
|
||||
skills_section: Option<String>,
|
||||
) -> Option<String> {
|
||||
match (project_doc, skills_section) {
|
||||
(Some(doc), Some(skills)) => Some(format!("{doc}\n\n{skills}")),
|
||||
(Some(doc), None) => Some(doc),
|
||||
(None, Some(skills)) => Some(skills),
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
71
codex-rs/core/tests/suite/hierarchical_agents.rs
Normal file
71
codex-rs/core/tests/suite/hierarchical_agents.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
use codex_core::features::Feature;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::responses::mount_sse_once;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
|
||||
const HIERARCHICAL_AGENTS_SNIPPET: &str =
|
||||
"Files called AGENTS.md commonly appear in many places inside a container";
|
||||
|
||||
fn sse_completed(id: &str) -> String {
|
||||
load_sse_fixture_with_id("../fixtures/completed_template.json", id)
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn hierarchical_agents_appends_to_project_doc_in_user_instructions() {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::HierarchicalAgents);
|
||||
std::fs::write(config.cwd.join("AGENTS.md"), "be nice").expect("write AGENTS.md");
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
assert!(
|
||||
instructions.contains("be nice"),
|
||||
"expected AGENTS.md text included: {instructions}"
|
||||
);
|
||||
let snippet_pos = instructions
|
||||
.find(HIERARCHICAL_AGENTS_SNIPPET)
|
||||
.expect("expected hierarchical agents snippet");
|
||||
let base_pos = instructions
|
||||
.find("be nice")
|
||||
.expect("expected AGENTS.md text");
|
||||
assert!(
|
||||
snippet_pos > base_pos,
|
||||
"expected hierarchical agents message appended after base instructions: {instructions}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn hierarchical_agents_emits_when_no_project_doc() {
|
||||
let server = start_mock_server().await;
|
||||
let resp_mock = mount_sse_once(&server, sse_completed("resp1")).await;
|
||||
|
||||
let mut builder = test_codex().with_config(|config| {
|
||||
config.features.enable(Feature::HierarchicalAgents);
|
||||
});
|
||||
let test = builder.build(&server).await.expect("build test codex");
|
||||
|
||||
test.submit_turn("hello").await.expect("submit turn");
|
||||
|
||||
let request = resp_mock.single_request();
|
||||
let user_messages = request.message_input_texts("user");
|
||||
let instructions = user_messages
|
||||
.iter()
|
||||
.find(|text| text.starts_with("# AGENTS.md instructions for "))
|
||||
.expect("instructions message");
|
||||
assert!(
|
||||
instructions.contains(HIERARCHICAL_AGENTS_SNIPPET),
|
||||
"expected hierarchical agents message appended: {instructions}"
|
||||
);
|
||||
}
|
||||
@@ -30,6 +30,7 @@ mod exec;
|
||||
mod exec_policy;
|
||||
mod fork_thread;
|
||||
mod grep_files;
|
||||
mod hierarchical_agents;
|
||||
mod items;
|
||||
mod json_result;
|
||||
mod list_dir;
|
||||
|
||||
@@ -76,6 +76,19 @@ pub struct AppExitInfo {
|
||||
pub token_usage: TokenUsage,
|
||||
pub thread_id: Option<ThreadId>,
|
||||
pub update_action: Option<UpdateAction>,
|
||||
pub exit_reason: ExitReason,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppRunControl {
|
||||
Continue,
|
||||
Exit(ExitReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExitReason {
|
||||
UserRequested,
|
||||
Fatal(String),
|
||||
}
|
||||
|
||||
fn session_summary(token_usage: TokenUsage, thread_id: Option<ThreadId>) -> Option<SessionSummary> {
|
||||
@@ -277,6 +290,7 @@ async fn handle_model_migration_prompt_if_needed(
|
||||
token_usage: TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -468,14 +482,23 @@ impl App {
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
if let Some(latest_version) = upgrade_version {
|
||||
app.handle_event(
|
||||
tui,
|
||||
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
|
||||
latest_version,
|
||||
crate::update_action::get_update_action(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
let control = app
|
||||
.handle_event(
|
||||
tui,
|
||||
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
|
||||
latest_version,
|
||||
crate::update_action::get_update_action(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
if let AppRunControl::Exit(exit_reason) = control {
|
||||
return Ok(AppExitInfo {
|
||||
token_usage: app.token_usage(),
|
||||
thread_id: app.chat_widget.thread_id(),
|
||||
update_action: app.pending_update_action,
|
||||
exit_reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let tui_events = tui.event_stream();
|
||||
@@ -483,19 +506,26 @@ impl App {
|
||||
|
||||
tui.frame_requester().schedule_frame();
|
||||
|
||||
while select! {
|
||||
Some(event) = app_event_rx.recv() => {
|
||||
app.handle_event(tui, event).await?
|
||||
let exit_reason = loop {
|
||||
let control = select! {
|
||||
Some(event) = app_event_rx.recv() => {
|
||||
app.handle_event(tui, event).await?
|
||||
}
|
||||
Some(event) = tui_events.next() => {
|
||||
app.handle_tui_event(tui, event).await?
|
||||
}
|
||||
};
|
||||
match control {
|
||||
AppRunControl::Continue => {}
|
||||
AppRunControl::Exit(reason) => break reason,
|
||||
}
|
||||
Some(event) = tui_events.next() => {
|
||||
app.handle_tui_event(tui, event).await?
|
||||
}
|
||||
} {}
|
||||
};
|
||||
tui.terminal.clear()?;
|
||||
Ok(AppExitInfo {
|
||||
token_usage: app.token_usage(),
|
||||
thread_id: app.chat_widget.thread_id(),
|
||||
update_action: app.pending_update_action,
|
||||
exit_reason,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -503,7 +533,7 @@ impl App {
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
event: TuiEvent,
|
||||
) -> Result<bool> {
|
||||
) -> Result<AppRunControl> {
|
||||
if self.overlay.is_some() {
|
||||
let _ = self.handle_backtrack_overlay_event(tui, event).await?;
|
||||
} else {
|
||||
@@ -525,7 +555,7 @@ impl App {
|
||||
.chat_widget
|
||||
.handle_paste_burst_tick(tui.frame_requester())
|
||||
{
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
tui.draw(
|
||||
self.chat_widget.desired_height(tui.terminal.size()?.width),
|
||||
@@ -544,10 +574,10 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<bool> {
|
||||
async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<AppRunControl> {
|
||||
let model_info = self
|
||||
.server
|
||||
.get_models_manager()
|
||||
@@ -707,7 +737,7 @@ impl App {
|
||||
&& matches!(event.msg, EventMsg::ShutdownComplete)
|
||||
{
|
||||
self.suppress_shutdown_complete = false;
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
if let EventMsg::ListSkillsResponse(response) = &event.msg {
|
||||
let cwd = self.chat_widget.config_ref().cwd.clone();
|
||||
@@ -720,7 +750,10 @@ impl App {
|
||||
self.on_conversation_history_for_backtrack(tui, ev).await?;
|
||||
}
|
||||
AppEvent::ExitRequest => {
|
||||
return Ok(false);
|
||||
return Ok(AppRunControl::Exit(ExitReason::UserRequested));
|
||||
}
|
||||
AppEvent::FatalExitRequest(message) => {
|
||||
return Ok(AppRunControl::Exit(ExitReason::Fatal(message)));
|
||||
}
|
||||
AppEvent::CodexOp(op) => self.chat_widget.submit_op(op),
|
||||
AppEvent::DiffResult(text) => {
|
||||
@@ -989,7 +1022,7 @@ impl App {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on app config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|
||||
@@ -1001,7 +1034,7 @@ impl App {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on chat config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
|
||||
@@ -1010,7 +1043,7 @@ impl App {
|
||||
// One-shot suppression if the user just confirmed continue.
|
||||
if self.skip_world_writable_scan_once {
|
||||
self.skip_world_writable_scan_once = false;
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
let should_check = codex_core::get_platform_sandbox().is_some()
|
||||
@@ -1035,7 +1068,7 @@ impl App {
|
||||
}
|
||||
AppEvent::UpdateFeatureFlags { updates } => {
|
||||
if updates.is_empty() {
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let mut builder = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_profile(self.active_profile.as_deref());
|
||||
@@ -1194,7 +1227,7 @@ impl App {
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(true)
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
fn reasoning_label(reasoning_effort: Option<ReasoningEffortConfig>) -> &'static str {
|
||||
|
||||
@@ -42,6 +42,9 @@ pub(crate) enum AppEvent {
|
||||
/// Request to exit the application gracefully.
|
||||
ExitRequest,
|
||||
|
||||
/// Request to exit the application due to a fatal error.
|
||||
FatalExitRequest(String),
|
||||
|
||||
/// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids
|
||||
/// bubbling channels through layers of widgets.
|
||||
CodexOp(codex_core::protocol::Op),
|
||||
|
||||
@@ -30,16 +30,14 @@ pub(crate) fn spawn_agent(
|
||||
..
|
||||
} = match server.start_thread(config).await {
|
||||
Ok(v) => v,
|
||||
#[allow(clippy::print_stderr)]
|
||||
Err(err) => {
|
||||
let message = err.to_string();
|
||||
eprintln!("{message}");
|
||||
let message = format!("Failed to initialize codex: {err}");
|
||||
tracing::error!("{message}");
|
||||
app_event_tx_clone.send(AppEvent::CodexEvent(Event {
|
||||
id: "".to_string(),
|
||||
msg: EventMsg::Error(err.to_error_event(None)),
|
||||
}));
|
||||
app_event_tx_clone.send(AppEvent::ExitRequest);
|
||||
tracing::error!("failed to initialize codex: {err}");
|
||||
app_event_tx_clone.send(AppEvent::FatalExitRequest(message));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use additional_dirs::add_dir_warning_message;
|
||||
use app::App;
|
||||
pub use app::AppExitInfo;
|
||||
pub use app::ExitReason;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_common::oss::ensure_oss_provider_ready;
|
||||
use codex_common::oss::get_default_model_for_oss_provider;
|
||||
@@ -376,6 +377,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: Some(action),
|
||||
fatal_error_message: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -415,6 +417,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
// if the user acknowledged windows or made an explicit decision ato trust the directory, reload the config accordingly
|
||||
@@ -450,6 +453,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -488,6 +492,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
thread_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
});
|
||||
}
|
||||
other => other,
|
||||
|
||||
@@ -97,6 +97,7 @@ pub struct AppExitInfo {
|
||||
pub token_usage: TokenUsage,
|
||||
pub conversation_id: Option<ThreadId>,
|
||||
pub update_action: Option<UpdateAction>,
|
||||
pub exit_reason: ExitReason,
|
||||
/// ANSI-styled transcript lines to print after the TUI exits.
|
||||
///
|
||||
/// These lines are rendered against the same width as the final TUI
|
||||
@@ -105,12 +106,29 @@ pub struct AppExitInfo {
|
||||
pub session_lines: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum AppRunControl {
|
||||
Continue,
|
||||
Exit(ExitReason),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ExitReason {
|
||||
UserRequested,
|
||||
Fatal(String),
|
||||
}
|
||||
|
||||
impl From<AppExitInfo> for codex_tui::AppExitInfo {
|
||||
fn from(info: AppExitInfo) -> Self {
|
||||
let exit_reason = match info.exit_reason {
|
||||
ExitReason::UserRequested => codex_tui::ExitReason::UserRequested,
|
||||
ExitReason::Fatal(message) => codex_tui::ExitReason::Fatal(message),
|
||||
};
|
||||
codex_tui::AppExitInfo {
|
||||
token_usage: info.token_usage,
|
||||
thread_id: info.conversation_id,
|
||||
update_action: info.update_action.map(Into::into),
|
||||
exit_reason,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -314,6 +332,7 @@ async fn handle_model_migration_prompt_if_needed(
|
||||
token_usage: TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
@@ -558,14 +577,24 @@ impl App {
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
if let Some(latest_version) = upgrade_version {
|
||||
app.handle_event(
|
||||
tui,
|
||||
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
|
||||
latest_version,
|
||||
crate::update_action::get_update_action(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
let control = app
|
||||
.handle_event(
|
||||
tui,
|
||||
AppEvent::InsertHistoryCell(Box::new(UpdateAvailableHistoryCell::new(
|
||||
latest_version,
|
||||
crate::update_action::get_update_action(),
|
||||
))),
|
||||
)
|
||||
.await?;
|
||||
if let AppRunControl::Exit(exit_reason) = control {
|
||||
return Ok(AppExitInfo {
|
||||
token_usage: app.token_usage(),
|
||||
conversation_id: app.chat_widget.conversation_id(),
|
||||
update_action: app.pending_update_action,
|
||||
exit_reason,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let tui_events = tui.event_stream();
|
||||
@@ -573,14 +602,20 @@ impl App {
|
||||
|
||||
tui.frame_requester().schedule_frame();
|
||||
|
||||
while select! {
|
||||
Some(event) = app_event_rx.recv() => {
|
||||
app.handle_event(tui, event).await?
|
||||
let exit_reason = loop {
|
||||
let control = select! {
|
||||
Some(event) = app_event_rx.recv() => {
|
||||
app.handle_event(tui, event).await?
|
||||
}
|
||||
Some(event) = tui_events.next() => {
|
||||
app.handle_tui_event(tui, event).await?
|
||||
}
|
||||
};
|
||||
match control {
|
||||
AppRunControl::Continue => {}
|
||||
AppRunControl::Exit(reason) => break reason,
|
||||
}
|
||||
Some(event) = tui_events.next() => {
|
||||
app.handle_tui_event(tui, event).await?
|
||||
}
|
||||
} {}
|
||||
};
|
||||
let width = tui.terminal.last_known_screen_size.width;
|
||||
let session_lines = if width == 0 {
|
||||
Vec::new()
|
||||
@@ -601,6 +636,7 @@ impl App {
|
||||
token_usage: app.token_usage(),
|
||||
conversation_id: app.chat_widget.conversation_id(),
|
||||
update_action: app.pending_update_action,
|
||||
exit_reason,
|
||||
session_lines,
|
||||
})
|
||||
}
|
||||
@@ -609,7 +645,7 @@ impl App {
|
||||
&mut self,
|
||||
tui: &mut tui::Tui,
|
||||
event: TuiEvent,
|
||||
) -> Result<bool> {
|
||||
) -> Result<AppRunControl> {
|
||||
if matches!(&event, TuiEvent::Draw) {
|
||||
self.handle_scroll_tick(tui);
|
||||
}
|
||||
@@ -638,7 +674,7 @@ impl App {
|
||||
.chat_widget
|
||||
.handle_paste_burst_tick(tui.frame_requester())
|
||||
{
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let cells = self.transcript_cells.clone();
|
||||
tui.draw(tui.terminal.size()?.height, |frame| {
|
||||
@@ -698,7 +734,7 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
pub(crate) fn render_transcript_cells(
|
||||
@@ -1349,7 +1385,7 @@ impl App {
|
||||
Some(TranscriptSelectionPoint { line_index, column })
|
||||
}
|
||||
|
||||
async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<bool> {
|
||||
async fn handle_event(&mut self, tui: &mut tui::Tui, event: AppEvent) -> Result<AppRunControl> {
|
||||
match event {
|
||||
AppEvent::NewSession => {
|
||||
let summary = session_summary(
|
||||
@@ -1489,7 +1525,7 @@ impl App {
|
||||
&& matches!(event.msg, EventMsg::ShutdownComplete)
|
||||
{
|
||||
self.suppress_shutdown_complete = false;
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
if let EventMsg::ListSkillsResponse(response) = &event.msg {
|
||||
let cwd = self.chat_widget.config_ref().cwd.clone();
|
||||
@@ -1502,7 +1538,10 @@ impl App {
|
||||
self.on_conversation_history_for_backtrack(tui, ev).await?;
|
||||
}
|
||||
AppEvent::ExitRequest => {
|
||||
return Ok(false);
|
||||
return Ok(AppRunControl::Exit(ExitReason::UserRequested));
|
||||
}
|
||||
AppEvent::FatalExitRequest(message) => {
|
||||
return Ok(AppRunControl::Exit(ExitReason::Fatal(message)));
|
||||
}
|
||||
AppEvent::CodexOp(op) => self.chat_widget.submit_op(op),
|
||||
AppEvent::DiffResult(text) => {
|
||||
@@ -1597,7 +1636,7 @@ impl App {
|
||||
preset,
|
||||
mode: WindowsSandboxEnableMode::Elevated,
|
||||
});
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
self.chat_widget.show_windows_sandbox_setup_status();
|
||||
@@ -1766,7 +1805,7 @@ impl App {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on app config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
if !matches!(&policy, codex_core::protocol::SandboxPolicy::ReadOnly)
|
||||
@@ -1778,7 +1817,7 @@ impl App {
|
||||
tracing::warn!(%err, "failed to set sandbox policy on chat config");
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to set sandbox policy: {err}"));
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
// If sandbox policy becomes workspace-write or read-only, run the Windows world-writable scan.
|
||||
@@ -1787,7 +1826,7 @@ impl App {
|
||||
// One-shot suppression if the user just confirmed continue.
|
||||
if self.skip_world_writable_scan_once {
|
||||
self.skip_world_writable_scan_once = false;
|
||||
return Ok(true);
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
|
||||
let should_check = codex_core::get_platform_sandbox().is_some()
|
||||
@@ -1935,7 +1974,7 @@ impl App {
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(true)
|
||||
Ok(AppRunControl::Continue)
|
||||
}
|
||||
|
||||
fn reasoning_label(reasoning_effort: Option<ReasoningEffortConfig>) -> &'static str {
|
||||
|
||||
@@ -41,6 +41,9 @@ pub(crate) enum AppEvent {
|
||||
/// Request to exit the application gracefully.
|
||||
ExitRequest,
|
||||
|
||||
/// Request to exit the application due to a fatal error.
|
||||
FatalExitRequest(String),
|
||||
|
||||
/// Forward an `Op` to the Agent. Using an `AppEvent` for this avoids
|
||||
/// bubbling channels through layers of widgets.
|
||||
CodexOp(codex_core::protocol::Op),
|
||||
|
||||
@@ -30,16 +30,14 @@ pub(crate) fn spawn_agent(
|
||||
thread_id: _,
|
||||
} = match server.start_thread(config).await {
|
||||
Ok(v) => v,
|
||||
#[allow(clippy::print_stderr)]
|
||||
Err(err) => {
|
||||
let message = err.to_string();
|
||||
eprintln!("{message}");
|
||||
let message = format!("Failed to initialize codex: {err}");
|
||||
tracing::error!("{message}");
|
||||
app_event_tx_clone.send(AppEvent::CodexEvent(Event {
|
||||
id: "".to_string(),
|
||||
msg: EventMsg::Error(err.to_error_event(None)),
|
||||
}));
|
||||
app_event_tx_clone.send(AppEvent::ExitRequest);
|
||||
tracing::error!("failed to initialize codex: {err}");
|
||||
app_event_tx_clone.send(AppEvent::FatalExitRequest(message));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
use additional_dirs::add_dir_warning_message;
|
||||
use app::App;
|
||||
pub use app::AppExitInfo;
|
||||
pub use app::ExitReason;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_common::oss::ensure_oss_provider_ready;
|
||||
use codex_common::oss::get_default_model_for_oss_provider;
|
||||
@@ -394,6 +395,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: Some(action),
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
@@ -434,6 +436,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
@@ -470,6 +473,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
@@ -509,6 +513,7 @@ async fn run_ratatui_app(
|
||||
token_usage: codex_core::protocol::TokenUsage::default(),
|
||||
conversation_id: None,
|
||||
update_action: None,
|
||||
exit_reason: ExitReason::UserRequested,
|
||||
session_lines: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
# AGENTS.md
|
||||
|
||||
For information about AGENTS.md, see [this documentation](https://developers.openai.com/codex/guides/agents-md).
|
||||
|
||||
## Hierarchical agents message
|
||||
|
||||
When the `hierarchical_agents` feature flag is enabled (via `[features]` in `config.toml`), Codex appends additional guidance about AGENTS.md scope and precedence to the user instructions message and emits that message even when no AGENTS.md is present.
|
||||
|
||||
Reference in New Issue
Block a user