mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Show remote connection details in /status (#24420)
## Summary Fixes #24411. `/status` currently has no way to show when the TUI is talking to Codex through a remote transport. That makes embedded local sessions, local daemon sessions, and true remote sessions look the same, and it hides the remote server version when debugging connection-specific behavior. This PR adds a single `Remote` row for non-embedded connections only. The row shows the sanitized connection address and a dimmed version parenthetical, preserving the existing status output for embedded local sessions. <img width="791" height="144" alt="image" src="https://github.com/user-attachments/assets/529d7940-1c45-4586-8b06-f20a1f04b771" /> ## Verification - Manually validated when connecting remotely (either implicitly to local daemon or explicitly)
This commit is contained in:
committed by
GitHub
Unverified
parent
caebff3d66
commit
913270a689
@@ -1122,7 +1122,9 @@ mod tests {
|
||||
websocket,
|
||||
JSONRPCMessage::Response(JSONRPCResponse {
|
||||
id: request.id,
|
||||
result: serde_json::json!({}),
|
||||
result: serde_json::json!({
|
||||
"userAgent": "codex_cli_rs/9.8.7-test (Test OS; x86_64) rust",
|
||||
}),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -1457,6 +1459,7 @@ mod tests {
|
||||
.await
|
||||
.expect("remote client should connect");
|
||||
|
||||
assert_eq!(client.server_version(), Some("9.8.7-test"));
|
||||
let response: GetAccountResponse = client
|
||||
.request_typed(ClientRequest::GetAccount {
|
||||
request_id: RequestId::Integer(1),
|
||||
|
||||
@@ -150,6 +150,7 @@ pub struct RemoteAppServerClient {
|
||||
command_tx: mpsc::Sender<RemoteClientCommand>,
|
||||
event_rx: mpsc::UnboundedReceiver<AppServerEvent>,
|
||||
pending_events: VecDeque<AppServerEvent>,
|
||||
server_version: Option<String>,
|
||||
worker_handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
@@ -180,6 +181,10 @@ impl RemoteAppServerClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_version(&self) -> Option<&str> {
|
||||
self.server_version.as_deref()
|
||||
}
|
||||
|
||||
async fn connect_with_stream<S>(
|
||||
channel_capacity: usize,
|
||||
endpoint: String,
|
||||
@@ -190,7 +195,7 @@ impl RemoteAppServerClient {
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
let mut stream = stream;
|
||||
let pending_events = initialize_remote_connection(
|
||||
let (pending_events, server_version) = initialize_remote_connection(
|
||||
&mut stream,
|
||||
&endpoint,
|
||||
initialize_params,
|
||||
@@ -466,6 +471,7 @@ impl RemoteAppServerClient {
|
||||
command_tx,
|
||||
event_rx,
|
||||
pending_events: pending_events.into(),
|
||||
server_version,
|
||||
worker_handle,
|
||||
})
|
||||
}
|
||||
@@ -606,6 +612,7 @@ impl RemoteAppServerClient {
|
||||
command_tx,
|
||||
event_rx,
|
||||
pending_events: _pending_events,
|
||||
server_version: _server_version,
|
||||
worker_handle,
|
||||
} = self;
|
||||
let mut worker_handle = worker_handle;
|
||||
@@ -793,12 +800,13 @@ async fn initialize_remote_connection<S>(
|
||||
endpoint: &str,
|
||||
params: InitializeParams,
|
||||
initialize_timeout: Duration,
|
||||
) -> IoResult<Vec<AppServerEvent>>
|
||||
) -> IoResult<(Vec<AppServerEvent>, Option<String>)>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let initialize_request_id = RequestId::String("initialize".to_string());
|
||||
let mut pending_events = Vec::new();
|
||||
let mut server_version = None;
|
||||
write_jsonrpc_message(
|
||||
stream,
|
||||
JSONRPCMessage::Request(jsonrpc_request_from_client_request(
|
||||
@@ -822,6 +830,14 @@ where
|
||||
})?;
|
||||
match message {
|
||||
JSONRPCMessage::Response(response) if response.id == initialize_request_id => {
|
||||
server_version = response
|
||||
.result
|
||||
.get("userAgent")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.and_then(|user_agent| {
|
||||
let (_, rest) = user_agent.split_once('/')?;
|
||||
rest.split_whitespace().next().map(str::to_string)
|
||||
});
|
||||
break Ok(());
|
||||
}
|
||||
JSONRPCMessage::Error(error) if error.id == initialize_request_id => {
|
||||
@@ -913,7 +929,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(pending_events)
|
||||
Ok((pending_events, server_version))
|
||||
}
|
||||
|
||||
fn app_server_event_from_notification(notification: JSONRPCNotification) -> Option<AppServerEvent> {
|
||||
@@ -1007,6 +1023,7 @@ mod tests {
|
||||
command_tx,
|
||||
event_rx,
|
||||
pending_events: VecDeque::new(),
|
||||
server_version: None,
|
||||
worker_handle,
|
||||
};
|
||||
|
||||
|
||||
@@ -754,6 +754,10 @@ impl App {
|
||||
let bootstrap_ms = bootstrap_started_at.elapsed().as_millis();
|
||||
let mut model = bootstrap.default_model;
|
||||
let available_models = bootstrap.available_models;
|
||||
let remote_connection = crate::status::remote_connection::remote_connection_status_value(
|
||||
&app_server_target,
|
||||
app_server.server_version(),
|
||||
);
|
||||
let exit_info = handle_model_migration_prompt_if_needed(
|
||||
tui,
|
||||
&mut config,
|
||||
@@ -942,6 +946,7 @@ impl App {
|
||||
(ChatWidget::new_with_app_event(init), Some(forked))
|
||||
}
|
||||
};
|
||||
chat_widget.remote_connection = remote_connection;
|
||||
let thread_and_widget_ms = thread_and_widget_started_at.elapsed().as_millis();
|
||||
if let Some(message) = external_agent_config_migration_message {
|
||||
chat_widget.add_info_message(message, /*hint*/ None);
|
||||
|
||||
@@ -268,6 +268,7 @@ impl App {
|
||||
if chat_widget.last_terminal_title.is_none() {
|
||||
chat_widget.last_terminal_title = previous_terminal_title;
|
||||
}
|
||||
chat_widget.remote_connection = self.chat_widget.remote_connection.clone();
|
||||
for (thread_id, entry) in self.agent_navigation.ordered_threads() {
|
||||
chat_widget.set_collab_agent_metadata(
|
||||
thread_id,
|
||||
|
||||
@@ -227,6 +227,13 @@ impl AppServerSession {
|
||||
matches!(self.thread_params_mode, ThreadParamsMode::Remote)
|
||||
}
|
||||
|
||||
pub(crate) fn server_version(&self) -> Option<&str> {
|
||||
let AppServerClient::Remote(client) = &self.client else {
|
||||
return None;
|
||||
};
|
||||
client.server_version()
|
||||
}
|
||||
|
||||
pub(crate) async fn bootstrap(&mut self, config: &Config) -> Result<AppServerBootstrap> {
|
||||
let account = self.read_account().await?;
|
||||
let model_request_id = self.next_request_id();
|
||||
|
||||
@@ -315,6 +315,7 @@ use crate::render::renderable::RenderableExt;
|
||||
use crate::render::renderable::RenderableItem;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::status::RateLimitSnapshotDisplay;
|
||||
use crate::status::remote_connection::RemoteConnectionStatus;
|
||||
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
|
||||
use crate::status_indicator_widget::StatusDetailsCapitalization;
|
||||
use crate::text_formatting::truncate_text;
|
||||
@@ -535,6 +536,7 @@ pub(crate) struct ChatWidget {
|
||||
initial_user_message: Option<UserMessage>,
|
||||
status_account_display: Option<StatusAccountDisplay>,
|
||||
runtime_model_provider_base_url: Option<String>,
|
||||
pub(crate) remote_connection: Option<RemoteConnectionStatus>,
|
||||
token_info: Option<TokenUsageInfo>,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap<String, RateLimitSnapshotDisplay>,
|
||||
refreshing_status_outputs: Vec<(u64, StatusHistoryHandle)>,
|
||||
|
||||
@@ -119,6 +119,7 @@ impl ChatWidget {
|
||||
initial_user_message,
|
||||
status_account_display,
|
||||
runtime_model_provider_base_url,
|
||||
remote_connection: None,
|
||||
token_info: None,
|
||||
rate_limit_snapshots_by_limit_id: BTreeMap::new(),
|
||||
refreshing_status_outputs: Vec::new(),
|
||||
|
||||
@@ -225,6 +225,7 @@ impl ChatWidget {
|
||||
let (cell, handle) = crate::status::new_status_output_with_rate_limits_handle(
|
||||
&self.config,
|
||||
self.runtime_model_provider_base_url.as_deref(),
|
||||
self.remote_connection.as_ref(),
|
||||
self.status_account_display.as_ref(),
|
||||
token_info,
|
||||
total_usage,
|
||||
|
||||
@@ -45,8 +45,10 @@ use super::rate_limits::compose_rate_limit_data;
|
||||
use super::rate_limits::compose_rate_limit_data_many;
|
||||
use super::rate_limits::format_status_limit_summary;
|
||||
use super::rate_limits::render_status_limit_progress_bar;
|
||||
use super::remote_connection::RemoteConnectionStatus;
|
||||
use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::adaptive_wrap_lines;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use std::sync::Arc;
|
||||
use std::sync::RwLock;
|
||||
|
||||
@@ -106,6 +108,7 @@ struct StatusHistoryCell {
|
||||
agents_summary: Arc<RwLock<String>>,
|
||||
collaboration_mode: Option<String>,
|
||||
model_provider: Option<String>,
|
||||
remote_connection: Option<RemoteConnectionStatus>,
|
||||
show_chatgpt_usage_link: bool,
|
||||
account: Option<StatusAccountDisplay>,
|
||||
thread_name: Option<String>,
|
||||
@@ -172,6 +175,7 @@ pub(crate) fn new_status_output_with_rate_limits(
|
||||
new_status_output_with_rate_limits_handle(
|
||||
config,
|
||||
/*runtime_model_provider_base_url*/ None,
|
||||
/*remote_connection*/ None,
|
||||
account_display,
|
||||
token_info,
|
||||
total_usage,
|
||||
@@ -194,6 +198,7 @@ pub(crate) fn new_status_output_with_rate_limits(
|
||||
pub(crate) fn new_status_output_with_rate_limits_handle(
|
||||
config: &Config,
|
||||
runtime_model_provider_base_url: Option<&str>,
|
||||
remote_connection: Option<&RemoteConnectionStatus>,
|
||||
account_display: Option<&StatusAccountDisplay>,
|
||||
token_info: Option<&TokenUsageInfo>,
|
||||
total_usage: &TokenUsage,
|
||||
@@ -213,6 +218,7 @@ pub(crate) fn new_status_output_with_rate_limits_handle(
|
||||
let (card, handle) = StatusHistoryCell::new(
|
||||
config,
|
||||
runtime_model_provider_base_url,
|
||||
remote_connection,
|
||||
account_display,
|
||||
token_info,
|
||||
total_usage,
|
||||
@@ -240,6 +246,7 @@ impl StatusHistoryCell {
|
||||
fn new(
|
||||
config: &Config,
|
||||
runtime_model_provider_base_url: Option<&str>,
|
||||
remote_connection: Option<&RemoteConnectionStatus>,
|
||||
account_display: Option<&StatusAccountDisplay>,
|
||||
token_info: Option<&TokenUsageInfo>,
|
||||
total_usage: &TokenUsage,
|
||||
@@ -349,6 +356,7 @@ impl StatusHistoryCell {
|
||||
permissions,
|
||||
collaboration_mode: collaboration_mode.map(ToString::to_string),
|
||||
model_provider,
|
||||
remote_connection: remote_connection.cloned(),
|
||||
show_chatgpt_usage_link,
|
||||
account,
|
||||
thread_name,
|
||||
@@ -689,7 +697,6 @@ impl HistoryCell for StatusHistoryCell {
|
||||
Span::from(" ").dim(),
|
||||
Span::from(format!("(v{CODEX_CLI_VERSION})")).dim(),
|
||||
]));
|
||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||
|
||||
let available_inner_width = usize::from(width.saturating_sub(4));
|
||||
if available_inner_width == 0 {
|
||||
@@ -768,12 +775,30 @@ impl HistoryCell for StatusHistoryCell {
|
||||
[note_first_line, note_second_line],
|
||||
RtOptions::new(available_inner_width),
|
||||
);
|
||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||
// The ChatGPT usage page only applies to providers backed by OpenAI auth;
|
||||
// providers like Bedrock manage limits and billing elsewhere.
|
||||
if self.show_chatgpt_usage_link {
|
||||
lines.extend(note_lines);
|
||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||
}
|
||||
if let Some(remote_connection) = self.remote_connection.as_ref() {
|
||||
let wrapped_remote = word_wrap_lines(
|
||||
[Line::from(vec![
|
||||
Span::from(remote_connection.address.clone()),
|
||||
Span::from(" (").dim(),
|
||||
Span::from(remote_connection.version.clone()).dim(),
|
||||
Span::from(")").dim(),
|
||||
])],
|
||||
RtOptions::new(value_width.max(1)),
|
||||
);
|
||||
let mut wrapped_remote = wrapped_remote.into_iter();
|
||||
if let Some(first) = wrapped_remote.next() {
|
||||
lines.push(formatter.line("Remote", first.spans));
|
||||
lines.extend(wrapped_remote.map(|line| formatter.continuation(line.spans)));
|
||||
}
|
||||
lines.push(Line::from(Vec::<Span<'static>>::new()));
|
||||
}
|
||||
|
||||
let mut model_spans = vec![Span::from(self.model_name.clone())];
|
||||
if !self.model_details.is_empty() {
|
||||
|
||||
@@ -11,6 +11,7 @@ mod card;
|
||||
mod format;
|
||||
mod helpers;
|
||||
mod rate_limits;
|
||||
pub(crate) mod remote_connection;
|
||||
|
||||
pub(crate) use account::StatusAccountDisplay;
|
||||
pub(crate) use card::StatusHistoryHandle;
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
use crate::AppServerTarget;
|
||||
use crate::RemoteAppServerEndpoint;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct RemoteConnectionStatus {
|
||||
pub(crate) address: String,
|
||||
pub(crate) version: String,
|
||||
}
|
||||
|
||||
pub(crate) fn remote_connection_status_value(
|
||||
app_server_target: &AppServerTarget,
|
||||
server_version: Option<&str>,
|
||||
) -> Option<RemoteConnectionStatus> {
|
||||
let endpoint = match app_server_target {
|
||||
AppServerTarget::Embedded => return None,
|
||||
AppServerTarget::LocalDaemon { endpoint } | AppServerTarget::Remote { endpoint } => {
|
||||
endpoint
|
||||
}
|
||||
};
|
||||
let address = match endpoint {
|
||||
RemoteAppServerEndpoint::WebSocket { websocket_url, .. } => {
|
||||
sanitized_websocket_display_address(websocket_url)
|
||||
.unwrap_or_else(|| "<invalid websocket URL>".to_string())
|
||||
}
|
||||
RemoteAppServerEndpoint::UnixSocket { socket_path } => {
|
||||
format!("unix://{}", socket_path.display())
|
||||
}
|
||||
};
|
||||
let version = server_version
|
||||
.map(|version| format!("v{version}"))
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
Some(RemoteConnectionStatus { address, version })
|
||||
}
|
||||
|
||||
fn sanitized_websocket_display_address(raw: &str) -> Option<String> {
|
||||
let mut url = Url::parse(raw).ok()?;
|
||||
let _ = url.set_username("");
|
||||
let _ = url.set_password(None);
|
||||
url.set_query(None);
|
||||
url.set_fragment(None);
|
||||
Some(url.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
#[test]
|
||||
fn remote_connection_status_value_formats_display_value() -> color_eyre::Result<()> {
|
||||
assert_eq!(
|
||||
remote_connection_status_value(&AppServerTarget::Embedded, Some("1.2.3")),
|
||||
None
|
||||
);
|
||||
|
||||
let websocket_target = AppServerTarget::Remote {
|
||||
endpoint: RemoteAppServerEndpoint::WebSocket {
|
||||
websocket_url: "ws://user:secret@127.0.0.1:4500/?token=abc#frag".to_string(),
|
||||
auth_token: Some("abc".to_string()),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
remote_connection_status_value(&websocket_target, Some("1.2.3")),
|
||||
Some(RemoteConnectionStatus {
|
||||
address: "ws://127.0.0.1:4500/".to_string(),
|
||||
version: "v1.2.3".to_string(),
|
||||
})
|
||||
);
|
||||
|
||||
let socket_path = AbsolutePathBuf::relative_to_current_dir("codex.sock")?;
|
||||
let daemon_target = AppServerTarget::LocalDaemon {
|
||||
endpoint: RemoteAppServerEndpoint::UnixSocket {
|
||||
socket_path: socket_path.clone(),
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
remote_connection_status_value(&daemon_target, /*server_version*/ None),
|
||||
Some(RemoteConnectionStatus {
|
||||
address: format!("unix://{}", socket_path.display()),
|
||||
version: "unknown".to_string(),
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+18
-15
@@ -4,18 +4,21 @@ expression: sanitized
|
||||
---
|
||||
/status
|
||||
|
||||
╭─────────────────────────────────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
|
||||
│ information on rate limits and credits │
|
||||
│ │
|
||||
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │
|
||||
│ Directory: [[workspace]] │
|
||||
│ Permissions: Custom (workspace with network access, on-request) │
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 750 total (500 input + 250 output) │
|
||||
│ Context window: 100% left (750 used / 272K) │
|
||||
│ Limits: data not available yet │
|
||||
╰─────────────────────────────────────────────────────────────────────────╯
|
||||
╭──────────────────────────────────────────────────────────────────────────╮
|
||||
│ >_ OpenAI Codex (v0.0.0) │
|
||||
│ │
|
||||
│ Visit https://chatgpt.com/codex/settings/usage for up-to-date │
|
||||
│ information on rate limits and credits │
|
||||
│ │
|
||||
│ Remote: unix:///tmp/codex-home/app-server-control/app-server- │
|
||||
│ control.sock (v0.133.0) │
|
||||
│ │
|
||||
│ Model: gpt-5.1-codex-max (reasoning medium, summaries auto) │
|
||||
│ Directory: [[workspace]] │
|
||||
│ Permissions: Custom (workspace with network access, on-request) │
|
||||
│ Agents.md: <none> │
|
||||
│ │
|
||||
│ Token usage: 750 total (500 input + 250 output) │
|
||||
│ Context window: 100% left (750 used / 272K) │
|
||||
│ Limits: data not available yet │
|
||||
╰──────────────────────────────────────────────────────────────────────────╯
|
||||
|
||||
@@ -7,6 +7,7 @@ use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::PermissionProfileSnapshot;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
use crate::status::remote_connection::RemoteConnectionStatus;
|
||||
use crate::test_support::PathBufExt;
|
||||
use crate::test_support::test_path_buf;
|
||||
use crate::token_usage::TokenUsage;
|
||||
@@ -604,6 +605,7 @@ async fn status_model_provider_uses_bedrock_runtime_base_url_and_gates_usage_lin
|
||||
let (composite, _handle) = new_status_output_with_rate_limits_handle(
|
||||
&config,
|
||||
Some(runtime_base_url),
|
||||
/*remote_connection*/ None,
|
||||
test_status_account_display().as_ref(),
|
||||
/*token_info*/ None,
|
||||
&usage,
|
||||
@@ -644,6 +646,7 @@ async fn status_model_provider_uses_bedrock_runtime_base_url_and_gates_usage_lin
|
||||
let (composite, _handle) = new_status_output_with_rate_limits_handle(
|
||||
&config,
|
||||
/*runtime_model_provider_base_url*/ None,
|
||||
/*remote_connection*/ None,
|
||||
test_status_account_display().as_ref(),
|
||||
/*token_info*/ None,
|
||||
&usage,
|
||||
@@ -1321,12 +1324,17 @@ async fn status_snapshot_uses_default_reasoning_when_config_empty() {
|
||||
.with_ymd_and_hms(2024, 2, 3, 4, 5, 6)
|
||||
.single()
|
||||
.expect("timestamp");
|
||||
let remote_connection = RemoteConnectionStatus {
|
||||
address: "unix:///tmp/codex-home/app-server-control/app-server-control.sock".to_string(),
|
||||
version: "v0.133.0".to_string(),
|
||||
};
|
||||
|
||||
let model_slug = crate::legacy_core::test_support::get_model_offline(config.model.as_deref());
|
||||
let token_info = token_info_for(&model_slug, &config, &usage);
|
||||
let (composite, _) = new_status_output_with_rate_limits_handle(
|
||||
&config,
|
||||
/*runtime_model_provider_base_url*/ None,
|
||||
Some(&remote_connection),
|
||||
account_display.as_ref(),
|
||||
Some(&token_info),
|
||||
&usage,
|
||||
|
||||
Reference in New Issue
Block a user