Add hostname to the configurable TUI status line (#39795)

## What changed

- Add `hostname` as a selectable status-line item and show it in setup previews.
- Read the normalized operating-system hostname without triggering DNS resolution, and omit the item when no hostname is available.

## Testing

- Cover hostname normalization, status-line rendering, and setup and surface previews.

GitOrigin-RevId: c5e4e0ee1dba6e4bfdf942562a6b037835ff9e69
This commit is contained in:
Ian MacLeod
2026-08-20 19:58:13 +00:00
committed by copyberry
parent bfb8986f7f
commit d9fd91edab
9 changed files with 116 additions and 3 deletions

View File

@@ -9,6 +9,10 @@ use winapi_util::sysinfo::ComputerNameKind;
use winapi_util::sysinfo::get_computer_name;
static HOST_NAME: LazyLock<Option<String>> = LazyLock::new(compute_host_name);
static OS_HOST_NAME: LazyLock<Option<String>> = 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<String> {
HOST_NAME.clone()
}
/// Returns the process-cached operating-system hostname without DNS resolution.
pub fn os_host_name() -> Option<String> {
OS_HOST_NAME.clone()
}
fn compute_host_name() -> Option<String> {
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<String> {
#[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!(

View File

@@ -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;

View File

@@ -0,0 +1,21 @@
---
source: tui/src/bottom_pane/status_line_setup.rs
expression: "render_lines(&view,\n100).lines().map(str::trim_end).collect::<Vec<_>>().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

View File

@@ -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::<AppEvent>();
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::<Vec<_>>()
.join("\n")
);
}
fn render_lines(view: &StatusLineSetupView, width: u16) -> String {
let height = view.desired_height(width);
let area = Rect::new(0, 0, width, height);

View File

@@ -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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;