mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
## What changed Add `tui.mouse_scroll_speed` as a finite positive multiplier for transcript mouse wheel scrolling, including transcript overlays and session previews. Change the default from three rows to one row per wheel event; set the multiplier to `3.0` to retain the previous speed. Fractional values accumulate movement across events and discard the remainder when direction reverses. For example, `0.5` scrolls one row every two events. Following the latest output pauses only when a whole row moves. ## Testing Add tests for configuration validation, integer and fractional scroll speeds, direction changes, decimal accumulation, and following behavior. Extend local settings coverage to include a command-line override for the new setting. GitOrigin-RevId: d355e246fd943415f0ce5a6affe849f77f3615e6
503 lines
19 KiB
Rust
503 lines
19 KiB
Rust
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use std::io::IsTerminal;
|
|
use std::io::Read;
|
|
use std::io::Write;
|
|
use std::sync::Arc;
|
|
|
|
use anyhow::Context;
|
|
use anyhow::bail;
|
|
use clap::Parser;
|
|
use codex_core_api::AbsolutePathBuf;
|
|
use codex_core_api::AltScreenMode;
|
|
use codex_core_api::ApprovalsReviewer;
|
|
use codex_core_api::Arg0DispatchPaths;
|
|
use codex_core_api::AskForApproval;
|
|
use codex_core_api::AuthKeyringBackendKind;
|
|
use codex_core_api::AuthManager;
|
|
use codex_core_api::AutoCompactTokenLimitScope;
|
|
use codex_core_api::CodexAppsToolsCache;
|
|
use codex_core_api::CodexHomeUserInstructionsProvider;
|
|
use codex_core_api::CodexThread;
|
|
use codex_core_api::Config;
|
|
use codex_core_api::ConfigLayerStack;
|
|
use codex_core_api::ConfigLoadOptions;
|
|
use codex_core_api::ConfigRequirements;
|
|
use codex_core_api::ConfigRequirementsToml;
|
|
use codex_core_api::Constrained;
|
|
use codex_core_api::EnvironmentManager;
|
|
use codex_core_api::EventMsg;
|
|
use codex_core_api::ExecServerRuntimeOptions;
|
|
use codex_core_api::ExtensionRegistryBuilder;
|
|
use codex_core_api::Feature;
|
|
use codex_core_api::Features;
|
|
use codex_core_api::GhostSnapshotConfig;
|
|
use codex_core_api::History;
|
|
use codex_core_api::MemoriesConfig;
|
|
use codex_core_api::ModelAvailabilityNuxConfig;
|
|
use codex_core_api::MultiAgentV2Config;
|
|
use codex_core_api::NewThread;
|
|
use codex_core_api::Notice;
|
|
use codex_core_api::OPENAI_PROVIDER_ID;
|
|
use codex_core_api::OtelConfig;
|
|
use codex_core_api::PermissionProfile;
|
|
use codex_core_api::Permissions;
|
|
use codex_core_api::ProjectConfig;
|
|
use codex_core_api::RealtimeAudioConfig;
|
|
use codex_core_api::RealtimeConfig;
|
|
use codex_core_api::SessionPickerViewMode;
|
|
use codex_core_api::SessionSource;
|
|
use codex_core_api::SqliteConfig;
|
|
use codex_core_api::StartIfIdleSubmission;
|
|
use codex_core_api::StartThreadOptions;
|
|
use codex_core_api::TerminalResizeReflowConfig;
|
|
use codex_core_api::ThreadManager;
|
|
use codex_core_api::ThreadStoreConfig;
|
|
use codex_core_api::ToolSuggestConfig;
|
|
use codex_core_api::TuiKeymap;
|
|
use codex_core_api::TuiNotificationSettings;
|
|
use codex_core_api::TuiPetAnchor;
|
|
use codex_core_api::TurnInputRequest;
|
|
use codex_core_api::UriBasedFileOpener;
|
|
use codex_core_api::UserInput;
|
|
use codex_core_api::WebSearchMode;
|
|
use codex_core_api::arg0_dispatch_or_else;
|
|
use codex_core_api::bootstrap_auth_config;
|
|
use codex_core_api::build_models_manager;
|
|
use codex_core_api::built_in_model_providers;
|
|
use codex_core_api::find_codex_home;
|
|
use codex_core_api::init_state_db;
|
|
use codex_core_api::install_image_generation_extension;
|
|
use codex_core_api::item_event_to_server_notification;
|
|
use codex_core_api::load_config_toml_with_layer_stack;
|
|
use codex_core_api::local_agent_graph_store_from_state_db;
|
|
use codex_core_api::passthrough_image_store;
|
|
use codex_core_api::resolve_bootstrap_respect_system_proxy;
|
|
use codex_core_api::resolve_installation_id;
|
|
use codex_core_api::set_default_originator;
|
|
use codex_core_api::thread_store_from_config;
|
|
|
|
#[derive(Debug, Parser)]
|
|
#[command(
|
|
name = "codex-thread-manager-sample",
|
|
about = "Run one Codex turn through ThreadManager and print mapped notifications as newline-delimited JSON."
|
|
)]
|
|
struct Args {
|
|
/// Override the model for this run.
|
|
#[arg(long, value_name = "MODEL")]
|
|
model: Option<String>,
|
|
|
|
/// Prompt text. If omitted, the prompt is read from piped stdin.
|
|
#[arg(value_name = "PROMPT", num_args = 0.., trailing_var_arg = true)]
|
|
prompt: Vec<String>,
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
arg0_dispatch_or_else(run_main)
|
|
}
|
|
|
|
async fn run_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
|
if let Err(err) = set_default_originator("codex_thread_manager_sample".to_string()) {
|
|
tracing::warn!("failed to set originator: {err:?}");
|
|
}
|
|
|
|
let args = Args::parse();
|
|
let prompt = if args.prompt.is_empty() {
|
|
if std::io::stdin().is_terminal() {
|
|
bail!("no prompt provided; pass a prompt argument or pipe one into stdin");
|
|
}
|
|
|
|
let mut prompt = String::new();
|
|
std::io::stdin()
|
|
.read_to_string(&mut prompt)
|
|
.context("read prompt from stdin")?;
|
|
let prompt = prompt.replace("\r\n", "\n").replace('\r', "\n");
|
|
if prompt.trim().is_empty() {
|
|
bail!("no prompt provided via stdin");
|
|
}
|
|
prompt
|
|
} else {
|
|
args.prompt.join(" ")
|
|
};
|
|
|
|
let config = new_config(args.model, arg0_paths).await?;
|
|
let state_db = init_state_db(&config).await;
|
|
|
|
let auth_manager =
|
|
AuthManager::shared_from_config(&config, /*enable_codex_api_key_env*/ false).await?;
|
|
let local_runtime_paths = ExecServerRuntimeOptions::from_optional_paths(
|
|
config.codex_self_exe.clone(),
|
|
config.codex_linux_sandbox_exe.clone(),
|
|
)?;
|
|
let thread_store = thread_store_from_config(&config, state_db.clone());
|
|
let environment_manager = Arc::new(
|
|
EnvironmentManager::from_codex_home(
|
|
config.codex_home.clone(),
|
|
Some(local_runtime_paths),
|
|
config.http_client_factory(),
|
|
)
|
|
.await?,
|
|
);
|
|
let installation_id = resolve_installation_id(&config.codex_home).await?;
|
|
let user_instructions_provider = Arc::new(CodexHomeUserInstructionsProvider::new(
|
|
config.codex_home.clone(),
|
|
));
|
|
let mut extensions = ExtensionRegistryBuilder::<Config>::new();
|
|
install_image_generation_extension(&mut extensions, auth_manager.clone(), |config: &Config| {
|
|
Some(config.codex_home.clone())
|
|
});
|
|
let thread_manager = ThreadManager::new(
|
|
&config,
|
|
Arc::clone(&auth_manager),
|
|
build_models_manager(&config, auth_manager),
|
|
CodexAppsToolsCache::default(),
|
|
SessionSource::Exec,
|
|
environment_manager,
|
|
Arc::new(extensions.build()),
|
|
user_instructions_provider,
|
|
/*analytics_events_client*/ None,
|
|
passthrough_image_store(),
|
|
Arc::clone(&thread_store),
|
|
local_agent_graph_store_from_state_db(state_db.as_ref()),
|
|
installation_id,
|
|
/*attestation_provider*/ None,
|
|
/*external_time_provider*/ None,
|
|
);
|
|
|
|
let NewThread {
|
|
thread_id, thread, ..
|
|
} = thread_manager
|
|
.start_thread(StartThreadOptions::new(config))
|
|
.await
|
|
.context("start Codex thread")?;
|
|
|
|
let thread_id_string = thread_id.to_string();
|
|
let turn_output = run_turn(&thread, &thread_id_string, prompt).await;
|
|
let shutdown_result = thread.shutdown_and_wait().await;
|
|
let _ = thread_manager.remove_thread(&thread_id).await;
|
|
|
|
turn_output?;
|
|
shutdown_result.context("shut down Codex thread")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn new_config(
|
|
model: Option<String>,
|
|
arg0_paths: Arg0DispatchPaths,
|
|
) -> anyhow::Result<Config> {
|
|
let codex_home = find_codex_home().context("find Codex home")?;
|
|
let cwd = AbsolutePathBuf::current_dir().context("resolve current directory")?;
|
|
// Resolve auth through the shared bootstrap path so cargo-run builds retain the configured
|
|
// secure store instead of the full Config loader's local-development file-store override.
|
|
let bootstrap = load_config_toml_with_layer_stack(
|
|
codex_home.as_path(),
|
|
Some(&cwd),
|
|
Vec::new(),
|
|
ConfigLoadOptions::default(),
|
|
)
|
|
.await
|
|
.context("load authentication configuration")?;
|
|
let auth_config = bootstrap_auth_config(codex_home.as_path(), &bootstrap)?;
|
|
let requirements = bootstrap.config_layer_stack.requirements();
|
|
let requirements_toml = bootstrap.config_layer_stack.requirements_toml();
|
|
// Preserve auth restrictions for every manager rebuilt from Config without importing
|
|
// execution settings, plugins, or hooks from the bootstrap configuration.
|
|
let config_layer_stack = ConfigLayerStack::new(
|
|
Vec::new(),
|
|
ConfigRequirements {
|
|
allowed_login_methods: requirements.allowed_login_methods.clone(),
|
|
allowed_chatgpt_workspaces: requirements.allowed_chatgpt_workspaces.clone(),
|
|
..Default::default()
|
|
},
|
|
ConfigRequirementsToml {
|
|
allowed_login_methods: requirements_toml.allowed_login_methods.clone(),
|
|
allowed_chatgpt_workspaces: requirements_toml.allowed_chatgpt_workspaces.clone(),
|
|
..Default::default()
|
|
},
|
|
)?;
|
|
let mcp_oauth_credentials_store_mode = bootstrap
|
|
.config_toml
|
|
.mcp_oauth_credentials_store
|
|
.unwrap_or_default();
|
|
let respect_system_proxy = resolve_bootstrap_respect_system_proxy(
|
|
&bootstrap.config_toml,
|
|
bootstrap
|
|
.config_layer_stack
|
|
.requirements()
|
|
.feature_requirements
|
|
.as_ref(),
|
|
)?;
|
|
let model_provider_id = OPENAI_PROVIDER_ID.to_string();
|
|
let model_providers = built_in_model_providers(/*openai_base_url*/ None);
|
|
let model_provider = model_providers
|
|
.get(&model_provider_id)
|
|
.context("OpenAI model provider should be available")?
|
|
.clone();
|
|
|
|
let mut config = Config {
|
|
application_network_policy: Default::default(),
|
|
application_auth_route_config: Some(auth_config.auth_route_config.clone()),
|
|
config_layer_stack,
|
|
startup_warnings: Vec::new(),
|
|
bypass_hook_trust: false,
|
|
model,
|
|
daybreak_enabled: false,
|
|
service_tier: None,
|
|
review_model: None,
|
|
model_context_window: None,
|
|
model_auto_compact_token_limit: None,
|
|
model_auto_compact_token_limit_scope: AutoCompactTokenLimitScope::Total,
|
|
model_post_turn_compact_threshold_percent: 0,
|
|
model_provider_id,
|
|
model_provider,
|
|
personality: None,
|
|
permissions: Permissions::from_approval_and_profile(
|
|
Constrained::allow_any(AskForApproval::Never),
|
|
Constrained::allow_any(PermissionProfile::read_only()),
|
|
)?,
|
|
explicit_permission_profile_mode: false,
|
|
custom_permission_profiles: Vec::new(),
|
|
approvals_reviewer: ApprovalsReviewer::User,
|
|
enforce_residency: Constrained::allow_any(/*initial_value*/ None),
|
|
hide_agent_reasoning: false,
|
|
show_raw_agent_reasoning: false,
|
|
base_instructions: None,
|
|
base_instructions_provenance: None,
|
|
developer_instructions: None,
|
|
guardian_policy_config: None,
|
|
guardian_extra_policy: None,
|
|
guardian_policy_template: None,
|
|
guardian_conversation_history_prompt: None,
|
|
guardian_conversation_history_max_output_tokens: None,
|
|
guardian_circuit_break_action: Default::default(),
|
|
include_permissions_instructions: false,
|
|
include_apps_instructions: false,
|
|
include_collaboration_mode_instructions: false,
|
|
include_skill_instructions: false,
|
|
skill_max_context_tokens: None,
|
|
cloud_skill_enabled: false,
|
|
orchestrator_mcp_enabled: false,
|
|
include_environment_context: false,
|
|
compact_prompt: None,
|
|
notify: None,
|
|
tui_notifications: TuiNotificationSettings::default(),
|
|
animations: true,
|
|
tui_effects: Default::default(),
|
|
tui_rendering: Default::default(),
|
|
show_tooltips: true,
|
|
tui_show_server_version_notice: true,
|
|
tui_auto_recap: true,
|
|
model_availability_nux: ModelAvailabilityNuxConfig::default(),
|
|
tui_fullscreen_transcript: false,
|
|
tui_mouse_scroll_speed: None,
|
|
tui_copy_on_select: Default::default(),
|
|
tui_right_click_paste: Default::default(),
|
|
tui_alternate_screen: AltScreenMode::Auto,
|
|
tui_status_line: None,
|
|
tui_status_line_use_colors: true,
|
|
tui_terminal_title: None,
|
|
tui_theme: None,
|
|
tui_raw_output_mode: false,
|
|
tui_pet: None,
|
|
tui_pet_anchor: TuiPetAnchor::Composer,
|
|
terminal_resize_reflow: TerminalResizeReflowConfig::default(),
|
|
tui_keymap: TuiKeymap::default(),
|
|
tui_session_picker_view: SessionPickerViewMode::Dense,
|
|
tui_resume_cwd: None,
|
|
tui_vim_mode_default: false,
|
|
tui_question_esc_back: true,
|
|
cwd: cwd.clone(),
|
|
workspace_roots: vec![cwd],
|
|
workspace_roots_explicit: false,
|
|
cli_auth_credentials_store_mode: auth_config.auth_credentials_store_mode,
|
|
mcp_servers: Constrained::allow_any(HashMap::new()),
|
|
mcp_enterprise_managed_auth: None,
|
|
non_prefixed_mcp_tool_servers: None,
|
|
mcp_oauth_credentials_store_mode,
|
|
mcp_oauth_callback_port: None,
|
|
mcp_oauth_callback_url: None,
|
|
mcp_optional_startup_grace: std::time::Duration::from_secs(1),
|
|
model_providers,
|
|
project_doc_max_bytes: 32 * 1024,
|
|
project_doc_fallback_filenames: Vec::new(),
|
|
tool_output_token_limit: None,
|
|
agents_enabled: true,
|
|
agent_max_threads: Some(6),
|
|
agent_default_subagent_model: None,
|
|
agent_default_subagent_reasoning_effort: None,
|
|
agent_interrupt_message_enabled: false,
|
|
agent_max_depth: 1,
|
|
agent_roles: BTreeMap::new(),
|
|
memories: MemoriesConfig::default(),
|
|
sqlite: SqliteConfig::from_sqlite_home(codex_home.clone()),
|
|
log_dir: codex_home.join("log").to_path_buf(),
|
|
codex_home,
|
|
history: History::default(),
|
|
ephemeral: true,
|
|
extra_config: None,
|
|
file_opener: UriBasedFileOpener::VsCode,
|
|
codex_self_exe: arg0_paths.codex_self_exe,
|
|
codex_linux_sandbox_exe: arg0_paths.codex_linux_sandbox_exe,
|
|
main_execve_wrapper_exe: arg0_paths.main_execve_wrapper_exe,
|
|
zsh_path: None,
|
|
model_reasoning_effort: None,
|
|
plan_mode_reasoning_effort: None,
|
|
model_reasoning_summary: None,
|
|
model_catalog: None,
|
|
model_verbosity: None,
|
|
chatgpt_base_url: auth_config
|
|
.chatgpt_base_url
|
|
.clone()
|
|
.unwrap_or_else(|| "https://chatgpt.com/backend-api/".to_string()),
|
|
respect_system_proxy,
|
|
apps_mcp_product_sku: None,
|
|
responses_api_metadata: BTreeMap::new(),
|
|
realtime_audio: RealtimeAudioConfig::default(),
|
|
experimental_realtime_ws_base_url: None,
|
|
experimental_realtime_webrtc_call_base_url: None,
|
|
experimental_realtime_ws_model: None,
|
|
realtime: RealtimeConfig::default(),
|
|
experimental_realtime_ws_backend_prompt: None,
|
|
experimental_realtime_ws_startup_context: None,
|
|
experimental_realtime_start_instructions: None,
|
|
experimental_thread_store: ThreadStoreConfig::Local,
|
|
forced_chatgpt_workspace_id: auth_config.forced_chatgpt_workspace_id.clone(),
|
|
forced_login_method: auth_config.forced_login_method,
|
|
web_search_mode: Constrained::allow_any(WebSearchMode::Disabled),
|
|
web_search_config: None,
|
|
experimental_request_user_input_enabled: true,
|
|
update_plan_enabled: true,
|
|
tool_registry: Default::default(),
|
|
code_mode: Default::default(),
|
|
background_terminal_max_timeout: 300_000,
|
|
thread_unload_delay: std::time::Duration::from_secs(60),
|
|
ghost_snapshot: GhostSnapshotConfig::default(),
|
|
multi_agent_v2: MultiAgentV2Config::default(),
|
|
max_goal_token_budget: None,
|
|
token_budget: None,
|
|
token_budget_startup_config: None,
|
|
rollout_budget: None,
|
|
current_time_reminder: None,
|
|
sleep_tool_mode: Default::default(),
|
|
features: Default::default(),
|
|
prefer_mxc: false,
|
|
|
|
runtime_feature_defaults: Default::default(),
|
|
suppress_unstable_features_warning: false,
|
|
active_project: ProjectConfig { trust_level: None },
|
|
notices: Notice::default(),
|
|
check_for_update_on_startup: false,
|
|
disable_paste_burst: false,
|
|
analytics_enabled: Some(false),
|
|
feedback_enabled: false,
|
|
tool_suggest: ToolSuggestConfig::default(),
|
|
otel: OtelConfig::default(),
|
|
};
|
|
config
|
|
.features
|
|
.set(Features::with_defaults())
|
|
.context("configure default features")?;
|
|
// AuthManager and MCP both derive their credential backend from Config.
|
|
config.features.set_enabled(
|
|
Feature::SecretAuthStorage,
|
|
auth_config.keyring_backend_kind == AuthKeyringBackendKind::Secrets,
|
|
)?;
|
|
Ok(config)
|
|
}
|
|
|
|
async fn run_turn(thread: &CodexThread, thread_id: &str, prompt: String) -> anyhow::Result<()> {
|
|
let submission = thread
|
|
.start_turn_if_idle(TurnInputRequest::user_input(vec![UserInput::Text {
|
|
text: prompt,
|
|
text_elements: Vec::new(),
|
|
}]))
|
|
.await
|
|
.context("submit user input")?;
|
|
if let StartIfIdleSubmission::NotSubmitted { reason } = submission {
|
|
bail!("turn input was not submitted: {reason:?}");
|
|
}
|
|
|
|
let mut current_turn_id: Option<String> = None;
|
|
let mut stdout = std::io::stdout().lock();
|
|
loop {
|
|
let event = thread.next_event().await.context("read Codex event")?;
|
|
let notification = match &event.msg {
|
|
EventMsg::TurnStarted(event) => {
|
|
current_turn_id = Some(event.turn_id.clone());
|
|
None
|
|
}
|
|
EventMsg::DynamicToolCallResponse(_)
|
|
| EventMsg::McpToolCallBegin(_)
|
|
| EventMsg::McpToolCallEnd(_)
|
|
| EventMsg::CollabAgentSpawnBegin(_)
|
|
| EventMsg::CollabAgentSpawnEnd(_)
|
|
| EventMsg::CollabAgentInteractionBegin(_)
|
|
| EventMsg::CollabAgentInteractionEnd(_)
|
|
| EventMsg::CollabWaitingBegin(_)
|
|
| EventMsg::CollabWaitingEnd(_)
|
|
| EventMsg::CollabCloseBegin(_)
|
|
| EventMsg::CollabCloseEnd(_)
|
|
| EventMsg::CollabResumeBegin(_)
|
|
| EventMsg::CollabResumeEnd(_)
|
|
| EventMsg::SubAgentActivity(_)
|
|
| EventMsg::AgentMessageContentDelta(_)
|
|
| EventMsg::PlanDelta(_)
|
|
| EventMsg::ReasoningContentDelta(_)
|
|
| EventMsg::ReasoningRawContentDelta(_)
|
|
| EventMsg::AgentReasoningSectionBreak(_)
|
|
| EventMsg::ItemStarted(_)
|
|
| EventMsg::ItemCompleted(_)
|
|
| EventMsg::PatchApplyBegin(_)
|
|
| EventMsg::PatchApplyUpdated(_)
|
|
| EventMsg::TerminalInteraction(_)
|
|
| EventMsg::ExecCommandBegin(_)
|
|
| EventMsg::ExecCommandOutputDelta(_)
|
|
| EventMsg::ExecCommandEnd(_) => Some(item_event_to_server_notification(
|
|
event.msg.clone(),
|
|
thread_id,
|
|
current_turn_id
|
|
.as_deref()
|
|
.context("mapped notification arrived before turn started")?,
|
|
)),
|
|
_ => None,
|
|
};
|
|
if let Some(notification) = notification {
|
|
serde_json::to_writer(&mut stdout, ¬ification)
|
|
.context("serialize mapped notification")?;
|
|
stdout
|
|
.write_all(b"\n")
|
|
.context("write notification newline")?;
|
|
stdout.flush().context("flush notification output")?;
|
|
}
|
|
|
|
match event.msg {
|
|
EventMsg::TurnComplete(_) => {
|
|
return Ok(());
|
|
}
|
|
EventMsg::Error(event) => {
|
|
bail!(event.message);
|
|
}
|
|
EventMsg::TurnAborted(_) => {
|
|
bail!("turn aborted");
|
|
}
|
|
EventMsg::ExecApprovalRequest(_) => {
|
|
bail!("turn requested exec approval");
|
|
}
|
|
EventMsg::ApplyPatchApprovalRequest(_) => {
|
|
bail!("turn requested patch approval");
|
|
}
|
|
EventMsg::RequestPermissions(_) => {
|
|
bail!("turn requested permissions");
|
|
}
|
|
EventMsg::RequestUserInput(_) => {
|
|
bail!("turn requested user input");
|
|
}
|
|
EventMsg::DynamicToolCallRequest(_) => {
|
|
bail!("turn requested a dynamic tool call");
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|