mirror of
https://github.com/openai/codex.git
synced 2026-11-06 00:30:29 +00:00
Add a dedicated codex agents dashboard command (#39114)
## What changed - Add `codex agents` to open the shared agents overview without creating a new session. - Start the local background app server automatically on Unix, or connect to a server supplied with `--remote`. - Reject invocation-specific session overrides that cannot apply to shared sessions. - When the overview is opened from an embedded session, offer to start the background server without moving or interrupting the current session. ## Testing - Cover command-line parsing and rejection of incompatible overrides. - Snapshot the embedded-session background-server prompt. GitOrigin-RevId: 60845dfebc48d820dc4ff090626d30452127f062
This commit is contained in:
1 parent
13dfaab446
commit
fd5018e044
14 files changed
+343
-30
No files matched your search
+128
-4
@@ -131,6 +131,9 @@ struct MultitoolCli {
|
|||||||
|
|
||||||
#[derive(Debug, clap::Subcommand)]
|
#[derive(Debug, clap::Subcommand)]
|
||||||
enum Subcommand {
|
enum Subcommand {
|
||||||
|
/// Browse all agent sessions on the shared local app-server daemon.
|
||||||
|
Agents(AgentsCommand),
|
||||||
|
|
||||||
/// Run Codex non-interactively.
|
/// Run Codex non-interactively.
|
||||||
#[clap(visible_alias = "e")]
|
#[clap(visible_alias = "e")]
|
||||||
Exec(ExecCli),
|
Exec(ExecCli),
|
||||||
@@ -323,6 +326,20 @@ struct DebugTraceReduceCommand {
|
|||||||
output: Option<PathBuf>,
|
output: Option<PathBuf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
struct AgentsCommand {
|
||||||
|
#[clap(flatten)]
|
||||||
|
remote: InteractiveRemoteOptions,
|
||||||
|
|
||||||
|
/// Use this directory for new tasks on a remote server.
|
||||||
|
#[arg(long = "cd", short = 'C', value_name = "DIR")]
|
||||||
|
cwd: Option<PathBuf>,
|
||||||
|
|
||||||
|
/// Disable alternate screen mode.
|
||||||
|
#[arg(long = "no-alt-screen", default_value_t = false)]
|
||||||
|
no_alt_screen: bool,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
struct ResumeCommand {
|
struct ResumeCommand {
|
||||||
/// Session id (UUID) or session name. UUIDs take precedence if it parses.
|
/// Session id (UUID) or session name. UUIDs take precedence if it parses.
|
||||||
@@ -1009,8 +1026,27 @@ async fn cli_main(
|
|||||||
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
// Fold --enable/--disable into config overrides so they flow to all subcommands.
|
||||||
let toggle_overrides = feature_toggles.to_overrides()?;
|
let toggle_overrides = feature_toggles.to_overrides()?;
|
||||||
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
root_config_overrides.raw_overrides.extend(toggle_overrides);
|
||||||
let root_remote = remote.remote;
|
let agents_options = match &subcommand {
|
||||||
let root_remote_auth_token_env = remote.remote_auth_token_env;
|
Some(Subcommand::Agents(options)) => Some(options),
|
||||||
|
_ => None,
|
||||||
|
};
|
||||||
|
if let Some(options) = agents_options
|
||||||
|
&& let Some(root_endpoint) = &remote.remote
|
||||||
|
&& let Some(agents_endpoint) = &options.remote.remote
|
||||||
|
&& root_endpoint != agents_endpoint
|
||||||
|
{
|
||||||
|
anyhow::bail!("`codex agents` received conflicting remote server endpoints");
|
||||||
|
}
|
||||||
|
let root_remote = agents_options
|
||||||
|
.and_then(|options| options.remote.remote.clone())
|
||||||
|
.or(remote.remote);
|
||||||
|
let root_remote_auth_token_env = agents_options
|
||||||
|
.and_then(|options| options.remote.remote_auth_token_env.clone())
|
||||||
|
.or(remote.remote_auth_token_env);
|
||||||
|
if let Some(options) = agents_options {
|
||||||
|
interactive.cwd = options.cwd.clone().or(interactive.cwd.take());
|
||||||
|
interactive.no_alt_screen |= options.no_alt_screen;
|
||||||
|
}
|
||||||
let root_strict_config = interactive.strict_config;
|
let root_strict_config = interactive.strict_config;
|
||||||
interactive
|
interactive
|
||||||
.shared
|
.shared
|
||||||
@@ -1020,8 +1056,45 @@ async fn cli_main(
|
|||||||
profile_v2_for_subcommand(&interactive, subcommand)?;
|
profile_v2_for_subcommand(&interactive, subcommand)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let open_agents_overview = matches!(&subcommand, Some(Subcommand::Agents(_)));
|
||||||
match subcommand {
|
match subcommand {
|
||||||
None => {
|
None | Some(Subcommand::Agents(_)) => {
|
||||||
|
if open_agents_overview {
|
||||||
|
if !root_config_overrides.raw_overrides.is_empty()
|
||||||
|
|| root_strict_config
|
||||||
|
|| interactive.prompt.is_some()
|
||||||
|
|| !interactive.images.is_empty()
|
||||||
|
|| interactive.model.is_some()
|
||||||
|
|| interactive.oss
|
||||||
|
|| interactive.oss_provider.is_some()
|
||||||
|
|| interactive.config_profile_v2.is_some()
|
||||||
|
|| interactive.sandbox_mode.is_some()
|
||||||
|
|| interactive.dangerously_bypass_approvals_and_sandbox
|
||||||
|
|| interactive.bypass_hook_trust
|
||||||
|
|| interactive.cwd.is_some() && root_remote.is_none()
|
||||||
|
|| !interactive.add_dir.is_empty()
|
||||||
|
|| interactive.approval_policy.is_some()
|
||||||
|
|| interactive.web_search
|
||||||
|
{
|
||||||
|
anyhow::bail!(
|
||||||
|
"`codex agents` cannot attach to shared sessions with invocation-specific configuration overrides"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if is_workload_identity_selected() {
|
||||||
|
anyhow::bail!(
|
||||||
|
"`codex agents` is unavailable while workload identity is active"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if root_remote.is_none() {
|
||||||
|
resolve_remote_endpoint(
|
||||||
|
/*remote*/ None,
|
||||||
|
root_remote_auth_token_env.clone(),
|
||||||
|
)?;
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
anyhow::bail!("`codex agents` requires `--remote` on this platform");
|
||||||
|
}
|
||||||
|
interactive.agents_overview = true;
|
||||||
|
}
|
||||||
prepend_config_flags(
|
prepend_config_flags(
|
||||||
&mut interactive.config_overrides,
|
&mut interactive.config_overrides,
|
||||||
root_config_overrides.clone(),
|
root_config_overrides.clone(),
|
||||||
@@ -1721,7 +1794,8 @@ fn profile_v2_for_subcommand<'a>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
match subcommand {
|
match subcommand {
|
||||||
Subcommand::Exec(_)
|
Subcommand::Agents(_)
|
||||||
|
| Subcommand::Exec(_)
|
||||||
| Subcommand::Review(_)
|
| Subcommand::Review(_)
|
||||||
| Subcommand::Resume(_)
|
| Subcommand::Resume(_)
|
||||||
| Subcommand::Queue(_)
|
| Subcommand::Queue(_)
|
||||||
@@ -2288,6 +2362,7 @@ fn unsupported_subcommand_name_for_strict_config(
|
|||||||
) -> Option<&'static str> {
|
) -> Option<&'static str> {
|
||||||
match subcommand {
|
match subcommand {
|
||||||
None
|
None
|
||||||
|
| Some(Subcommand::Agents(_))
|
||||||
| Some(Subcommand::Exec(_))
|
| Some(Subcommand::Exec(_))
|
||||||
| Some(Subcommand::Review(_))
|
| Some(Subcommand::Review(_))
|
||||||
| Some(Subcommand::McpServer(_))
|
| Some(Subcommand::McpServer(_))
|
||||||
@@ -2459,6 +2534,22 @@ async fn run_interactive_tui(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if interactive.agents_overview && remote.is_none() {
|
||||||
|
if !std::io::stdin().is_terminal() {
|
||||||
|
return Ok(AppExitInfo::fatal("stdin is not a terminal"));
|
||||||
|
}
|
||||||
|
if !std::io::stdout().is_terminal() {
|
||||||
|
return Ok(AppExitInfo::fatal("stdout is not a terminal"));
|
||||||
|
}
|
||||||
|
cloud_config::load_config(&interactive.config_overrides, LoaderOverrides::default())
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
codex_app_server_daemon::run(AppServerLifecycleCommand::Start)
|
||||||
|
.await
|
||||||
|
.map_err(std::io::Error::other)?;
|
||||||
|
}
|
||||||
|
|
||||||
let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env) {
|
let remote_endpoint = match resolve_remote_endpoint(remote, remote_auth_token_env) {
|
||||||
Ok(remote_endpoint) => remote_endpoint,
|
Ok(remote_endpoint) => remote_endpoint,
|
||||||
Err(err) if is_remote_auth_usage_error(&err) => {
|
Err(err) if is_remote_auth_usage_error(&err) => {
|
||||||
@@ -4030,6 +4121,39 @@ mod tests {
|
|||||||
assert_eq!(remote.remote.as_deref(), Some("unix://codex.sock"));
|
assert_eq!(remote.remote.as_deref(), Some("unix://codex.sock"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agents_subcommand_accepts_remote_session_options() {
|
||||||
|
let cli = MultitoolCli::try_parse_from([
|
||||||
|
"codex",
|
||||||
|
"agents",
|
||||||
|
"--remote",
|
||||||
|
"ws://127.0.0.1:4500",
|
||||||
|
"--remote-auth-token-env",
|
||||||
|
"CODEX_REMOTE_AUTH_TOKEN",
|
||||||
|
"--cd",
|
||||||
|
"/workspace",
|
||||||
|
"--no-alt-screen",
|
||||||
|
])
|
||||||
|
.expect("parse");
|
||||||
|
let Some(Subcommand::Agents(options)) = cli.subcommand else {
|
||||||
|
panic!("expected agents subcommand");
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
options.remote.remote.as_deref(),
|
||||||
|
Some("ws://127.0.0.1:4500")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.remote.remote_auth_token_env.as_deref(),
|
||||||
|
Some("CODEX_REMOTE_AUTH_TOKEN")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
options.cwd.as_deref(),
|
||||||
|
Some(std::path::Path::new("/workspace"))
|
||||||
|
);
|
||||||
|
assert!(options.no_alt_screen);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn reject_remote_mode_for_non_interactive_subcommands() {
|
fn reject_remote_mode_for_non_interactive_subcommands() {
|
||||||
let err = reject_remote_mode_for_subcommand(
|
let err = reject_remote_mode_for_subcommand(
|
||||||
|
|||||||
@@ -32,6 +32,25 @@ foo = "bar"
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn agents_reject_session_overrides_before_starting_the_daemon() -> Result<()> {
|
||||||
|
let codex_home = TempDir::new()?;
|
||||||
|
|
||||||
|
for args in [
|
||||||
|
["--yolo", "agents"].as_slice(),
|
||||||
|
["--search", "agents"].as_slice(),
|
||||||
|
["--model", "gpt-5", "agents"].as_slice(),
|
||||||
|
] {
|
||||||
|
let mut cmd = codex_command(codex_home.path())?;
|
||||||
|
cmd.args(args)
|
||||||
|
.assert()
|
||||||
|
.failure()
|
||||||
|
.stderr(contains("invocation-specific configuration overrides"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn app_server_emits_json_info_events() -> Result<()> {
|
fn app_server_emits_json_info_events() -> Result<()> {
|
||||||
let codex_home = TempDir::new()?;
|
let codex_home = TempDir::new()?;
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ use super::agents_overview_view::AgentsOverviewGroup;
|
|||||||
use super::agents_overview_view::AgentsOverviewRow;
|
use super::agents_overview_view::AgentsOverviewRow;
|
||||||
use super::agents_overview_view::AgentsOverviewView;
|
use super::agents_overview_view::AgentsOverviewView;
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::bottom_pane::SelectionDescriptionLayout;
|
||||||
|
use crate::bottom_pane::SelectionItem;
|
||||||
|
use crate::bottom_pane::SelectionViewParams;
|
||||||
|
use crate::bottom_pane::popup_consts::standard_popup_hint_line_for_keymap;
|
||||||
use crate::chatwidget::ThreadInputStateRestoreMode;
|
use crate::chatwidget::ThreadInputStateRestoreMode;
|
||||||
use codex_app_server_protocol::RequestId;
|
use codex_app_server_protocol::RequestId;
|
||||||
use codex_app_server_protocol::SessionSource;
|
use codex_app_server_protocol::SessionSource;
|
||||||
@@ -38,10 +42,51 @@ pub(super) struct AgentsOverviewState {
|
|||||||
impl App {
|
impl App {
|
||||||
pub(super) fn open_agents_overview(&mut self, app_server: &AppServerSession) {
|
pub(super) fn open_agents_overview(&mut self, app_server: &AppServerSession) {
|
||||||
if matches!(self.app_server_target, AppServerTarget::Embedded) {
|
if matches!(self.app_server_target, AppServerTarget::Embedded) {
|
||||||
self.chat_widget.add_info_message(
|
let workload_identity_selected = codex_login::is_workload_identity_selected();
|
||||||
"The shared agents dashboard requires a background app server.".to_string(),
|
self.chat_widget.show_selection_view(SelectionViewParams {
|
||||||
/*hint*/ None,
|
title: Some("Shared agents unavailable".to_string()),
|
||||||
);
|
subtitle: Some(
|
||||||
|
if workload_identity_selected {
|
||||||
|
"The agents dashboard is unavailable while workload identity is active."
|
||||||
|
} else if cfg!(unix) {
|
||||||
|
"This session isn’t connected to a shared background server."
|
||||||
|
} else {
|
||||||
|
"Connect to a remote background server to use the agents dashboard."
|
||||||
|
}
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
footer_note: (cfg!(unix) && !workload_identity_selected).then(|| {
|
||||||
|
Line::from(
|
||||||
|
"Starting a background server will not interrupt or move this session."
|
||||||
|
.dim(),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
footer_hint: Some(standard_popup_hint_line_for_keymap(&self.keymap.list)),
|
||||||
|
items: [
|
||||||
|
#[cfg(unix)]
|
||||||
|
(!workload_identity_selected).then(|| SelectionItem {
|
||||||
|
name: "Start background server".to_string(),
|
||||||
|
description: Some(
|
||||||
|
"Open `codex agents` in another terminal afterward.".to_string(),
|
||||||
|
),
|
||||||
|
actions: vec![Box::new(|tx| tx.send(AppEvent::StartAgentsDaemon))],
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
Some(SelectionItem {
|
||||||
|
name: "Return to this session".to_string(),
|
||||||
|
dismiss_on_select: true,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect(),
|
||||||
|
description_layout: SelectionDescriptionLayout::StackBelowWhenNarrow {
|
||||||
|
min_description_width: 28,
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -647,6 +692,43 @@ impl App {
|
|||||||
self.refresh_agents_overview_threads(app_server);
|
self.refresh_agents_overview_threads(app_server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub(super) fn start_agents_daemon(&self) {
|
||||||
|
let app_event_tx = self.app_event_tx.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result = async {
|
||||||
|
let current_executable =
|
||||||
|
std::env::current_exe().map_err(|error| error.to_string())?;
|
||||||
|
let executable = if current_executable
|
||||||
|
.file_stem()
|
||||||
|
.is_some_and(|name| name == "codex-tui")
|
||||||
|
{
|
||||||
|
current_executable.with_file_name("codex")
|
||||||
|
} else {
|
||||||
|
current_executable
|
||||||
|
};
|
||||||
|
let output = tokio::process::Command::new(executable)
|
||||||
|
.args(["app-server", "daemon", "start"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
let message = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
Err(if message.is_empty() {
|
||||||
|
format!("daemon process exited with {}", output.status)
|
||||||
|
} else {
|
||||||
|
message
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.await;
|
||||||
|
|
||||||
|
app_event_tx.send(AppEvent::AgentsDaemonStarted { result });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|||||||
@@ -254,6 +254,25 @@ async fn shared_overview_shows_only_root_sessions() {
|
|||||||
assert!(app.last_rendered_history_tail.is_some());
|
assert!(app.last_rendered_history_tail.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
#[tokio::test]
|
||||||
|
async fn embedded_sessions_offer_to_start_a_background_server_without_migrating() {
|
||||||
|
let mut app = make_test_app().await;
|
||||||
|
let app_server = crate::start_embedded_app_server_for_picker(app.chat_widget.config_ref())
|
||||||
|
.await
|
||||||
|
.expect("embedded app server");
|
||||||
|
|
||||||
|
app.open_agents_overview(&app_server);
|
||||||
|
|
||||||
|
insta::with_settings!({snapshot_path => "../snapshots"}, {
|
||||||
|
insta::assert_snapshot!(
|
||||||
|
"agents_overview_embedded",
|
||||||
|
render_bottom_popup(&app.chat_widget, /*width*/ 96)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
app_server.shutdown().await.expect("shutdown app server");
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn failed_root_switch_keeps_background_requests_on_the_active_session() -> Result<()> {
|
async fn failed_root_switch_keeps_background_requests_on_the_active_session() -> Result<()> {
|
||||||
let mut app = make_test_app().await;
|
let mut app = make_test_app().await;
|
||||||
|
|||||||
@@ -191,7 +191,9 @@ impl App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SessionSelection::Exit | SessionSelection::StartFresh => {
|
SessionSelection::Exit
|
||||||
|
| SessionSelection::StartFresh
|
||||||
|
| SessionSelection::AgentsOverview => {
|
||||||
self.refresh_in_memory_config_from_disk_best_effort(
|
self.refresh_in_memory_config_from_disk_best_effort(
|
||||||
"closing the session picker",
|
"closing the session picker",
|
||||||
)
|
)
|
||||||
@@ -2326,6 +2328,9 @@ impl App {
|
|||||||
.select_agents_overview_thread(tui, app_server, thread_id)
|
.select_agents_overview_thread(tui, app_server, thread_id)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
|
AppRunControl::Continue if self.primary_thread_id.is_none() => {
|
||||||
|
self.open_agents_overview(app_server);
|
||||||
|
}
|
||||||
AppRunControl::Continue => {}
|
AppRunControl::Continue => {}
|
||||||
AppRunControl::Exit(reason) => return Ok(AppRunControl::Exit(reason)),
|
AppRunControl::Exit(reason) => return Ok(AppRunControl::Exit(reason)),
|
||||||
}
|
}
|
||||||
@@ -2348,6 +2353,21 @@ impl App {
|
|||||||
self.stop_agents_overview_thread(app_server, thread_id)
|
self.stop_agents_overview_thread(app_server, thread_id)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
AppEvent::StartAgentsDaemon => {
|
||||||
|
self.start_agents_daemon();
|
||||||
|
}
|
||||||
|
#[cfg(unix)]
|
||||||
|
AppEvent::AgentsDaemonStarted { result } => match result {
|
||||||
|
Ok(()) => self.chat_widget.add_info_message(
|
||||||
|
"Background server started. Run `codex agents` in another terminal; this session remains unchanged."
|
||||||
|
.to_string(),
|
||||||
|
/*hint*/ None,
|
||||||
|
),
|
||||||
|
Err(error) => self
|
||||||
|
.chat_widget
|
||||||
|
.add_error_message(format!("Failed to start the background server: {error}")),
|
||||||
|
},
|
||||||
AppEvent::OpenAgentPicker => {
|
AppEvent::OpenAgentPicker => {
|
||||||
self.open_agent_picker(app_server).await;
|
self.open_agent_picker(app_server).await;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,9 @@ impl App {
|
|||||||
let bootstrap_ms = bootstrap.duration.as_millis();
|
let bootstrap_ms = bootstrap.duration.as_millis();
|
||||||
if matches!(
|
if matches!(
|
||||||
&session_selection,
|
&session_selection,
|
||||||
SessionSelection::StartFresh | SessionSelection::Exit
|
SessionSelection::StartFresh
|
||||||
|
| SessionSelection::Exit
|
||||||
|
| SessionSelection::AgentsOverview
|
||||||
) {
|
) {
|
||||||
apply_managed_new_thread_defaults(
|
apply_managed_new_thread_defaults(
|
||||||
&mut config,
|
&mut config,
|
||||||
@@ -222,23 +224,33 @@ impl App {
|
|||||||
&session_selection,
|
&session_selection,
|
||||||
SessionSelection::StartFresh | SessionSelection::Exit
|
SessionSelection::StartFresh | SessionSelection::Exit
|
||||||
);
|
);
|
||||||
|
let start_in_agents_overview =
|
||||||
|
matches!(&session_selection, SessionSelection::AgentsOverview);
|
||||||
let (mut chat_widget, initial_started_thread) = match session_selection {
|
let (mut chat_widget, initial_started_thread) = match session_selection {
|
||||||
SessionSelection::StartFresh | SessionSelection::Exit => {
|
SessionSelection::StartFresh
|
||||||
spawn_startup_thread_start(&app_server, config.clone(), app_event_tx.clone());
|
| SessionSelection::Exit
|
||||||
|
| SessionSelection::AgentsOverview => {
|
||||||
|
if !start_in_agents_overview {
|
||||||
|
spawn_startup_thread_start(&app_server, config.clone(), app_event_tx.clone());
|
||||||
|
}
|
||||||
// Count a startup tooltip once the initial chat widget can render it.
|
// Count a startup tooltip once the initial chat widget can render it.
|
||||||
let startup_tooltip_override = match startup_draft
|
let startup_tooltip_override = if start_in_agents_overview {
|
||||||
.run_until(
|
None
|
||||||
tui,
|
} else {
|
||||||
prepare_startup_tooltip_override(
|
match startup_draft
|
||||||
&mut config,
|
.run_until(
|
||||||
&available_models,
|
tui,
|
||||||
is_first_run,
|
prepare_startup_tooltip_override(
|
||||||
),
|
&mut config,
|
||||||
)
|
&available_models,
|
||||||
.await
|
is_first_run,
|
||||||
{
|
),
|
||||||
Ok(tooltip_override) => tooltip_override,
|
)
|
||||||
Err(err) => return shutdown_on_startup_error(app_server, err).await,
|
.await
|
||||||
|
{
|
||||||
|
Ok(tooltip_override) => tooltip_override,
|
||||||
|
Err(err) => return shutdown_on_startup_error(app_server, err).await,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let init = crate::chatwidget::ChatWidgetInit {
|
let init = crate::chatwidget::ChatWidgetInit {
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
@@ -268,7 +280,9 @@ impl App {
|
|||||||
session_telemetry: session_telemetry.clone(),
|
session_telemetry: session_telemetry.clone(),
|
||||||
};
|
};
|
||||||
let mut chat_widget = ChatWidget::new_with_app_event(init);
|
let mut chat_widget = ChatWidget::new_with_app_event(init);
|
||||||
chat_widget.set_queue_submissions_until_session_configured(/*queue*/ true);
|
chat_widget.set_queue_submissions_until_session_configured(
|
||||||
|
/*queue*/ !start_in_agents_overview,
|
||||||
|
);
|
||||||
(chat_widget, None)
|
(chat_widget, None)
|
||||||
}
|
}
|
||||||
SessionSelection::Resume(target_session) => {
|
SessionSelection::Resume(target_session) => {
|
||||||
@@ -451,6 +465,9 @@ See the Codex keymap documentation for supported actions and examples."
|
|||||||
pending_plugin_enabled_writes: HashMap::new(),
|
pending_plugin_enabled_writes: HashMap::new(),
|
||||||
pending_hook_enabled_writes: HashMap::new(),
|
pending_hook_enabled_writes: HashMap::new(),
|
||||||
};
|
};
|
||||||
|
if start_in_agents_overview {
|
||||||
|
app.open_agents_overview(&app_server);
|
||||||
|
}
|
||||||
if let Some(entry) = startup_hooks_browser {
|
if let Some(entry) = startup_hooks_browser {
|
||||||
app.chat_widget.open_hooks_browser(entry);
|
app.chat_widget.open_hooks_browser(entry);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,6 +40,9 @@ fn startup_waiting_gate_is_only_for_fresh_or_exit_session_selection() {
|
|||||||
App::should_wait_for_initial_session(&SessionSelection::Exit),
|
App::should_wait_for_initial_session(&SessionSelection::Exit),
|
||||||
true
|
true
|
||||||
);
|
);
|
||||||
|
assert!(!App::should_wait_for_initial_session(
|
||||||
|
&SessionSelection::AgentsOverview
|
||||||
|
));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
App::should_wait_for_initial_session(&SessionSelection::Resume(
|
App::should_wait_for_initial_session(&SessionSelection::Resume(
|
||||||
crate::resume_picker::SessionTarget {
|
crate::resume_picker::SessionTarget {
|
||||||
|
|||||||
@@ -220,6 +220,14 @@ pub(crate) enum AppEvent {
|
|||||||
StopAgentsOverviewThread {
|
StopAgentsOverviewThread {
|
||||||
thread_id: ThreadId,
|
thread_id: ThreadId,
|
||||||
},
|
},
|
||||||
|
/// Start the shared app-server daemon without moving the current embedded session.
|
||||||
|
#[cfg(unix)]
|
||||||
|
StartAgentsDaemon,
|
||||||
|
/// Report whether starting the shared app-server daemon succeeded.
|
||||||
|
#[cfg(unix)]
|
||||||
|
AgentsDaemonStarted {
|
||||||
|
result: Result<(), String>,
|
||||||
|
},
|
||||||
/// Open the agent picker for switching active threads.
|
/// Open the agent picker for switching active threads.
|
||||||
OpenAgentPicker,
|
OpenAgentPicker,
|
||||||
/// Merge a completed root-scoped agent-picker refresh without blocking terminal input.
|
/// Merge a completed root-scoped agent-picker refresh without blocking terminal input.
|
||||||
|
|||||||
@@ -37,6 +37,10 @@ pub struct Cli {
|
|||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
pub resume_include_non_interactive: bool,
|
pub resume_include_non_interactive: bool,
|
||||||
|
|
||||||
|
/// Internal: open the daemon-wide agents overview instead of starting a thread.
|
||||||
|
#[clap(skip)]
|
||||||
|
pub agents_overview: bool,
|
||||||
|
|
||||||
// Internal controls set by the top-level `codex fork` subcommand.
|
// Internal controls set by the top-level `codex fork` subcommand.
|
||||||
// These are not exposed as user flags on the base `codex` command.
|
// These are not exposed as user flags on the base `codex` command.
|
||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
|
|||||||
@@ -1226,7 +1226,7 @@ async fn run_ratatui_app(
|
|||||||
initial_config
|
initial_config
|
||||||
};
|
};
|
||||||
startup_draft.apply_config(&config);
|
startup_draft.apply_config(&config);
|
||||||
if !(cli.resume_picker || cli.fork_picker)
|
if !(cli.resume_picker || cli.fork_picker || cli.agents_overview)
|
||||||
&& let Err(err) = startup_draft.show(&mut tui)
|
&& let Err(err) = startup_draft.show(&mut tui)
|
||||||
{
|
{
|
||||||
shutdown_startup_session(app_server.take(), &mut terminal_restore_guard).await;
|
shutdown_startup_session(app_server.take(), &mut terminal_restore_guard).await;
|
||||||
@@ -1254,7 +1254,9 @@ async fn run_ratatui_app(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
|
let use_fork = cli.fork_picker || cli.fork_last || cli.fork_session_id.is_some();
|
||||||
let session_selection = if use_fork {
|
let session_selection = if cli.agents_overview {
|
||||||
|
resume_picker::SessionSelection::AgentsOverview
|
||||||
|
} else if use_fork {
|
||||||
if let Some(id_str) = cli.fork_session_id.as_deref() {
|
if let Some(id_str) = cli.fork_session_id.as_deref() {
|
||||||
let Some(startup_app_server) = app_server.as_mut() else {
|
let Some(startup_app_server) = app_server.as_mut() else {
|
||||||
unreachable!("app server should be initialized for --fork <id>");
|
unreachable!("app server should be initialized for --fork <id>");
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ impl SessionTarget {
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum SessionSelection {
|
pub enum SessionSelection {
|
||||||
StartFresh,
|
StartFresh,
|
||||||
|
AgentsOverview,
|
||||||
Resume(SessionTarget),
|
Resume(SessionTarget),
|
||||||
Fork(SessionTarget),
|
Fork(SessionTarget),
|
||||||
Exit,
|
Exit,
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
source: tui/src/app/agents_overview_tests.rs
|
||||||
|
expression: "render_bottom_popup(&app.chat_widget, 96)"
|
||||||
|
---
|
||||||
|
Shared agents unavailable
|
||||||
|
This session isn’t connected to a shared background server.
|
||||||
|
|
||||||
|
› 1. Start background server Open `codex agents` in another terminal afterward.
|
||||||
|
2. Return to this session
|
||||||
|
|
||||||
|
Starting a background server will not interrupt or move this session.
|
||||||
|
Press enter to confirm or esc to go back
|
||||||
@@ -183,7 +183,9 @@ impl StartupDraftPump {
|
|||||||
session_selection: &SessionSelection,
|
session_selection: &SessionSelection,
|
||||||
) -> io::Result<()> {
|
) -> io::Result<()> {
|
||||||
let session_action = match session_selection {
|
let session_action = match session_selection {
|
||||||
SessionSelection::StartFresh | SessionSelection::Exit => StartupDraftSessionAction::New,
|
SessionSelection::StartFresh
|
||||||
|
| SessionSelection::Exit
|
||||||
|
| SessionSelection::AgentsOverview => StartupDraftSessionAction::New,
|
||||||
SessionSelection::Resume(_) => StartupDraftSessionAction::Resume,
|
SessionSelection::Resume(_) => StartupDraftSessionAction::Resume,
|
||||||
SessionSelection::Fork(_) => StartupDraftSessionAction::Fork,
|
SessionSelection::Fork(_) => StartupDraftSessionAction::Fork,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ pub(super) async fn run_main_inner(
|
|||||||
&& loader_overrides_are_default(&launch_loader_overrides)
|
&& loader_overrides_are_default(&launch_loader_overrides)
|
||||||
&& !strict_config
|
&& !strict_config
|
||||||
&& !cli.bypass_hook_trust;
|
&& !cli.bypass_hook_trust;
|
||||||
let initial_screen = if cli.resume_picker || cli.fork_picker {
|
let initial_screen = if cli.resume_picker || cli.fork_picker || cli.agents_overview {
|
||||||
startup_draft::StartupDraftInitialScreen::SessionPicker
|
startup_draft::StartupDraftInitialScreen::SessionPicker
|
||||||
} else if !cli.oss
|
} else if !cli.oss
|
||||||
&& explicit_remote_endpoint.is_none()
|
&& explicit_remote_endpoint.is_none()
|
||||||
|
|||||||
Reference in new issue
Block a user