diff --git a/codex-rs/config/src/host_name.rs b/codex-rs/config/src/host_name.rs index eb67b7b8d0..7f92bfcd53 100644 --- a/codex-rs/config/src/host_name.rs +++ b/codex-rs/config/src/host_name.rs @@ -9,6 +9,10 @@ use winapi_util::sysinfo::ComputerNameKind; use winapi_util::sysinfo::get_computer_name; static HOST_NAME: LazyLock> = LazyLock::new(compute_host_name); +static OS_HOST_NAME: LazyLock> = LazyLock::new(|| { + let kernel_hostname = gethostname::gethostname(); + normalize_host_name(&kernel_hostname.to_string_lossy()) +}); /// Returns a process-cached canonical hostname, falling back to the normalized /// kernel hostname. The first call on Unix may perform blocking DNS resolution. @@ -16,9 +20,13 @@ pub fn host_name() -> Option { HOST_NAME.clone() } +/// Returns the process-cached operating-system hostname without DNS resolution. +pub fn os_host_name() -> Option { + OS_HOST_NAME.clone() +} + fn compute_host_name() -> Option { - let kernel_hostname = gethostname::gethostname(); - let kernel_hostname = normalize_host_name(&kernel_hostname.to_string_lossy())?; + let kernel_hostname = os_host_name()?; // Remote sandbox requirements are meant to target remote hosts by DNS name, // so prefer the canonical FQDN when the local resolver can provide one. @@ -74,8 +82,20 @@ fn normalize_fqdn_candidate(hostname: &str) -> Option { #[cfg(test)] mod tests { use super::normalize_fqdn_candidate; + use super::normalize_host_name; + use super::os_host_name; use pretty_assertions::assert_eq; + #[test] + fn os_host_name_matches_normalized_kernel_hostname() { + let kernel_hostname = gethostname::gethostname(); + + assert_eq!( + os_host_name(), + normalize_host_name(&kernel_hostname.to_string_lossy()) + ); + } + #[test] fn normalize_fqdn_candidate_accepts_dns_qualified_name() { assert_eq!( diff --git a/codex-rs/config/src/lib.rs b/codex-rs/config/src/lib.rs index 602ad17a20..907bd2c4f6 100644 --- a/codex-rs/config/src/lib.rs +++ b/codex-rs/config/src/lib.rs @@ -120,6 +120,7 @@ pub use hook_config::HooksToml; pub use hook_config::ManagedHooksRequirementsToml; pub use hook_config::MatcherGroup; pub use host_name::host_name; +pub use host_name::os_host_name; pub use in_app_browser_requirements::InAppBrowserRequirementsToml; pub use marketplace_edit::MarketplaceConfigUpdate; pub use marketplace_edit::RemoveMarketplaceConfigOutcome; diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap new file mode 100644 index 0000000000..48a0a8a176 --- /dev/null +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__status_line_setup__tests__setup_view_snapshot_includes_hostname.snap @@ -0,0 +1,21 @@ +--- +source: tui/src/bottom_pane/status_line_setup.rs +expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect::>().join(\"\\n\")" +--- + + Configure Status Line + Select which items to display in the status line. + + Type to search + > +› [x] Use theme colors Apply colors from the active /theme + ─────────────────────── + [x] hostname Current machine hostname (omitted when unavailable) + [x] current-dir Current working directory + [ ] model Current model name + [ ] model-with-reasoning Current model name with reasoning level + [ ] reasoning Current reasoning level + [ ] project-name Project name (omitted when unavailable) + + ssh-build-01.example.com · ~/codex-rs + Press space to toggle; ←/→ to move; enter to confirm and close; esc to close diff --git a/codex-rs/tui/src/bottom_pane/status_line_setup.rs b/codex-rs/tui/src/bottom_pane/status_line_setup.rs index 0092d170bd..a5f728ed82 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_setup.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_setup.rs @@ -11,6 +11,7 @@ //! //! - Model information (name, reasoning level) //! - Directory paths (current dir, project root) +//! - Machine hostname //! - Git information (branch name) //! - Permissions profile //! - Approval mode @@ -74,6 +75,9 @@ pub(crate) enum StatusLineItem { )] ProjectRoot, + /// Hostname of the machine running Codex. + Hostname, + /// Current git branch name (if in a repository). GitBranch, @@ -159,6 +163,7 @@ impl StatusLineItem { StatusLineItem::Reasoning => "Current reasoning level", StatusLineItem::CurrentDir => "Current working directory", StatusLineItem::ProjectRoot => "Project name (omitted when unavailable)", + StatusLineItem::Hostname => "Current machine hostname (omitted when unavailable)", StatusLineItem::GitBranch => "Current Git branch (omitted when unavailable)", StatusLineItem::PullRequestNumber => { "Open pull request number for the current branch (omitted when unavailable)" @@ -216,6 +221,7 @@ impl StatusLineItem { StatusLineItem::Reasoning => StatusSurfacePreviewItem::Reasoning, StatusLineItem::CurrentDir => StatusSurfacePreviewItem::CurrentDir, StatusLineItem::ProjectRoot => StatusSurfacePreviewItem::ProjectRoot, + StatusLineItem::Hostname => StatusSurfacePreviewItem::Hostname, StatusLineItem::GitBranch => StatusSurfacePreviewItem::GitBranch, StatusLineItem::PullRequestNumber => StatusSurfacePreviewItem::PullRequestNumber, StatusLineItem::BranchChanges => StatusSurfacePreviewItem::BranchChanges, @@ -726,6 +732,38 @@ mod tests { assert_snapshot!(render_lines(&view, /*width*/ 100)); } + #[test] + fn setup_view_snapshot_includes_hostname() { + let (tx_raw, _rx) = unbounded_channel::(); + let view = StatusLineSetupView::new( + Some(&[ + StatusLineItem::Hostname.to_string(), + StatusLineItem::CurrentDir.to_string(), + ]), + /*use_theme_colors*/ true, + StatusSurfacePreviewData::from_iter([ + ( + StatusLineItem::Hostname.preview_item(), + "ssh-build-01.example.com".to_string(), + ), + ( + StatusLineItem::CurrentDir.preview_item(), + "~/codex-rs".to_string(), + ), + ]), + AppEventSender::new(tx_raw), + crate::keymap::RuntimeKeymap::defaults().list, + ); + + assert_snapshot!( + render_lines(&view, /*width*/ 100) + .lines() + .map(str::trim_end) + .collect::>() + .join("\n") + ); + } + fn render_lines(view: &StatusLineSetupView, width: u16) -> String { let height = view.desired_height(width); let area = Rect::new(0, 0, width, height); diff --git a/codex-rs/tui/src/bottom_pane/status_line_style.rs b/codex-rs/tui/src/bottom_pane/status_line_style.rs index 29e82b8e21..4783f5523b 100644 --- a/codex-rs/tui/src/bottom_pane/status_line_style.rs +++ b/codex-rs/tui/src/bottom_pane/status_line_style.rs @@ -47,7 +47,9 @@ impl StatusLineAccent { | StatusLineItem::ThreadCredits | StatusLineItem::EstimatedThreadCost => Self::Usage, StatusLineItem::FiveHourLimit | StatusLineItem::WeeklyLimit => Self::Limit, - StatusLineItem::CodexVersion | StatusLineItem::SessionId => Self::Metadata, + StatusLineItem::CodexVersion | StatusLineItem::Hostname | StatusLineItem::SessionId => { + Self::Metadata + } StatusLineItem::FastMode | StatusLineItem::RawOutput => Self::Mode, StatusLineItem::Permissions => Self::Mode, StatusLineItem::ApprovalMode => Self::Mode, diff --git a/codex-rs/tui/src/bottom_pane/status_surface_preview.rs b/codex-rs/tui/src/bottom_pane/status_surface_preview.rs index ffa9ae9269..c0ce0f6c47 100644 --- a/codex-rs/tui/src/bottom_pane/status_surface_preview.rs +++ b/codex-rs/tui/src/bottom_pane/status_surface_preview.rs @@ -11,6 +11,7 @@ pub(crate) enum StatusSurfacePreviewItem { ProjectName, ProjectRoot, CurrentDir, + Hostname, Status, ThreadTitle, GitBranch, @@ -46,6 +47,7 @@ impl StatusSurfacePreviewItem { StatusSurfacePreviewItem::ProjectName => "my-project", StatusSurfacePreviewItem::ProjectRoot => "my-project", StatusSurfacePreviewItem::CurrentDir => "~/my-project/subdir", + StatusSurfacePreviewItem::Hostname => "my-host", StatusSurfacePreviewItem::Status => "Working", StatusSurfacePreviewItem::ThreadTitle => "thread title", StatusSurfacePreviewItem::GitBranch => "feat/awesome-feature", @@ -81,6 +83,7 @@ impl StatusSurfacePreviewItem { Self::ProjectName, Self::ProjectRoot, Self::CurrentDir, + Self::Hostname, Self::Status, Self::ThreadTitle, Self::GitBranch, diff --git a/codex-rs/tui/src/chatwidget/status_surfaces.rs b/codex-rs/tui/src/chatwidget/status_surfaces.rs index 04b876aa29..66c7b26d8a 100644 --- a/codex-rs/tui/src/chatwidget/status_surfaces.rs +++ b/codex-rs/tui/src/chatwidget/status_surfaces.rs @@ -14,6 +14,7 @@ use crate::status::format_estimated_usd_micros; use crate::status::format_tokens_compact; use codex_app_server_protocol::AskForApproval; use codex_config::ConfigLayerSource; +use codex_config::os_host_name; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::ServiceTier; use codex_protocol::models::PermissionProfile; @@ -682,6 +683,7 @@ impl ChatWidget { )) } StatusLineItem::ProjectRoot => self.status_line_project_root_name(), + StatusLineItem::Hostname => os_host_name(), StatusLineItem::GitBranch => self.status_line_branch.clone(), StatusLineItem::PullRequestNumber => self .status_line_git_summary @@ -800,6 +802,7 @@ impl ChatWidget { StatusSurfacePreviewItem::Status => return Some(self.run_state_status_text()), StatusSurfacePreviewItem::TaskProgress => return self.terminal_title_task_progress(), StatusSurfacePreviewItem::CurrentDir => StatusLineItem::CurrentDir, + StatusSurfacePreviewItem::Hostname => StatusLineItem::Hostname, StatusSurfacePreviewItem::ThreadTitle => StatusLineItem::ThreadTitle, StatusSurfacePreviewItem::GitBranch => StatusLineItem::GitBranch, StatusSurfacePreviewItem::PullRequestNumber => StatusLineItem::PullRequestNumber, diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index b28bc6a63e..b80923dfd9 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -2805,6 +2805,21 @@ async fn status_line_invalid_items_warn_once() { ); } +#[tokio::test] +async fn status_line_hostname_renders_current_machine_hostname() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.config.tui_status_line = Some(vec!["hostname".to_string()]); + + chat.refresh_status_line(); + + assert_eq!(status_line_text(&chat), codex_config::os_host_name()); + assert!( + drain_insert_history(&mut rx).is_empty(), + "hostname should be accepted as a status line item" + ); +} + #[tokio::test] async fn status_line_context_used_renders_labeled_percent() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; diff --git a/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs b/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs index 5891301f55..0a830ff9f7 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_surface_previews.rs @@ -85,6 +85,16 @@ fn cache_rate_limit_snapshot(chat: &mut ChatWidget) { })); } +#[tokio::test] +async fn status_surface_hostname_preview_uses_current_machine_hostname() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + + assert_eq!( + status_preview_line(&mut chat, &[StatusLineItem::Hostname]), + codex_config::os_host_name().expect("machine hostname") + ); +} + #[tokio::test] async fn status_surface_preview_lines_live_only_snapshot() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;