//! Status output and setup controls for `ChatWidget`.//!//! Rendering details live in `status_surfaces`; this module owns the mutable//! widget entrypoints that apply status state, open setup views, and update the//! history-facing `/status` surface.use super::*;impl ChatWidget { /// Update the status indicator header and details. /// /// Passing `None` clears any existing details. pub(super) fn set_status( &mut self, header: String, details: Option<String>, details_capitalization: StatusDetailsCapitalization, details_max_lines: usize, ) { let details = details .filter(|details| !details.is_empty()) .map(|details| { let trimmed = details.trim_start(); match details_capitalization { StatusDetailsCapitalization::CapitalizeFirst => { crate::text_formatting::capitalize_first(trimmed) } StatusDetailsCapitalization::Preserve => trimmed.to_string(), } }); self.status_state.set_status(StatusIndicatorState { header: header.clone(), details: details.clone(), details_max_lines, }); self.bottom_pane.update_status( header, details, StatusDetailsCapitalization::Preserve, details_max_lines, ); let title_uses_status = self .config .tui_terminal_title .as_ref() .is_some_and(|items| { items .iter() .any(|item| item == "run-state" || item == "status") }); if title_uses_status { self.refresh_status_surfaces(); } } /// Convenience wrapper around [`Self::set_status`]; /// updates the status indicator header and clears any existing details. pub(super) fn set_status_header(&mut self, header: String) { self.set_status( header, /*details*/ None, StatusDetailsCapitalization::CapitalizeFirst, STATUS_DETAILS_DEFAULT_MAX_LINES, ); } /// Sets the currently rendered footer status-line value. pub(crate) fn set_status_line(&mut self, status_line: Option<Line<'static>>) { self.bottom_pane.set_status_line(status_line); } /// Sets the terminal hyperlink target for the currently rendered footer status line. pub(crate) fn set_status_line_hyperlink(&mut self, url: Option<String>) { self.bottom_pane.set_status_line_hyperlink(url); } /// Forwards the contextual active-agent label into the bottom-pane footer pipeline. /// /// `ChatWidget` stays a pass-through here so `App` remains the owner of "which thread is the /// user actually looking at?" and the footer stack remains a pure renderer of that decision. pub(crate) fn set_active_agent_label(&mut self, active_agent_label: Option<String>) { self.bottom_pane.set_active_agent_label(active_agent_label); } /// Recomputes footer status-line content from config and current runtime state. /// /// This method is the status-line orchestrator: it parses configured item identifiers, /// warns once per session about invalid items, updates whether status-line mode is enabled, /// schedules async git-branch lookup when needed, and renders only values that are currently /// available. /// /// The omission behavior is intentional. If selected items are unavailable (for example before /// a session id exists or before branch lookup completes), those items are skipped without /// placeholders so the line remains compact and stable. pub(crate) fn refresh_status_line(&mut self) { self.refresh_status_surfaces(); } /// Records that status-line setup was canceled. /// /// Cancellation is intentionally side-effect free for config state; the existing configuration /// remains active and no persistence is attempted. pub(crate) fn cancel_status_line_setup(&self) { tracing::info!("Status line setup canceled by user"); } /// Applies status-line item selection from the setup view to in-memory config. /// /// An empty selection persists as an explicit empty list. pub(crate) fn setup_status_line(&mut self, items: Vec<StatusLineItem>, use_theme_colors: bool) { tracing::info!( "status line setup confirmed with items: {items:#?}, use_theme_colors: {use_theme_colors}" ); let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>(); self.config.tui_status_line = Some(ids); self.config.tui_status_line_use_colors = use_theme_colors; self.refresh_status_line(); } /// Applies a temporary terminal-title selection while the setup UI is open. pub(crate) fn preview_terminal_title(&mut self, items: Vec<TerminalTitleItem>) { if self.terminal_title_setup_original_items.is_none() { self.terminal_title_setup_original_items = Some(self.config.tui_terminal_title.clone()); } let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>(); self.config.tui_terminal_title = Some(ids); self.refresh_terminal_title(); } /// Restores the terminal-title config that was active before the setup UI /// opened, undoing any preview changes. No-op if no setup session is active. pub(crate) fn revert_terminal_title_setup_preview(&mut self) { let Some(original_items) = self.terminal_title_setup_original_items.take() else { return; }; self.config.tui_terminal_title = original_items; self.refresh_terminal_title(); } /// Dismisses the terminal-title setup UI and reverts to the pre-setup config. pub(crate) fn cancel_terminal_title_setup(&mut self) { tracing::info!("Terminal title setup canceled by user"); self.revert_terminal_title_setup_preview(); } /// Commits a confirmed terminal-title selection, ending the setup session. /// /// After this call, `revert_terminal_title_setup_preview` becomes a no-op /// because the original config snapshot is discarded. pub(crate) fn setup_terminal_title(&mut self, items: Vec<TerminalTitleItem>) { tracing::info!("terminal title setup confirmed with items: {items:#?}"); let ids = items.iter().map(ToString::to_string).collect::<Vec<_>>(); self.terminal_title_setup_original_items = None; self.config.tui_terminal_title = Some(ids); self.refresh_terminal_title(); } /// Stores async git-branch lookup results for the current status-line cwd. /// /// Results are dropped when they target an out-of-date cwd to avoid rendering stale branch /// names after directory changes. pub(crate) fn set_status_line_branch(&mut self, cwd: PathBuf, branch: Option<String>) { if self.status_line_branch_cwd.as_ref() != Some(&cwd) { self.status_line_branch_pending = false; return; } self.status_line_branch = branch; self.status_line_branch_pending = false; self.status_line_branch_lookup_complete = true; self.refresh_status_surfaces(); } /// Stores async Git summary lookup results for the current status-line cwd. pub(crate) fn set_status_line_git_summary( &mut self, cwd: PathBuf, summary: StatusLineGitSummary, ) { if self.status_line_git_summary_cwd.as_ref() != Some(&cwd) { self.status_line_git_summary_pending = false; return; } self.status_line_git_summary = Some(summary); self.status_line_git_summary_pending = false; self.status_line_git_summary_lookup_complete = true; self.refresh_status_surfaces(); } pub(crate) fn add_status_output( &mut self, refreshing_rate_limits: bool, request_id: Option<u64>, ) { let default_usage = TokenUsage::default(); let token_info = self.token_info.as_ref(); let total_usage = token_info .map(|ti| &ti.total_token_usage) .unwrap_or(&default_usage); let collaboration_mode = self.collaboration_mode_label(); let model = self.current_model().to_string(); let model_default_reasoning_effort = self.model_catalog .try_list_models() .ok() .and_then(|models| { models .into_iter() .find(|preset| preset.model == model) .map(|preset| preset.default_reasoning_effort) }); let reasoning_effort_override = Some( self.effective_reasoning_effort() .or_else(|| self.config.model_reasoning_effort.clone()) .or(model_default_reasoning_effort), ); let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self .rate_limit_snapshots_by_limit_id .values() .cloned() .collect(); let agents_summary = crate::status::compose_agents_summary(&self.config, &self.instruction_source_paths); 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, &self.thread_id, self.thread_name.clone(), self.forked_from, rate_limit_snapshots.as_slice(), self.plan_type, Local::now(), self.model_display_name(), collaboration_mode, reasoning_effort_override, agents_summary, refreshing_rate_limits, ); if let Some(request_id) = request_id { self.refreshing_status_outputs.push((request_id, handle)); } self.add_to_history(cell); } pub(crate) fn finish_status_rate_limit_refresh(&mut self, request_id: u64) { if self.refreshing_status_outputs.is_empty() { return; } let rate_limit_snapshots: Vec<RateLimitSnapshotDisplay> = self .rate_limit_snapshots_by_limit_id .values() .cloned() .collect(); let now = Local::now(); let mut remaining = Vec::with_capacity(self.refreshing_status_outputs.len()); let mut updated_any = false; for (pending_request_id, handle) in self.refreshing_status_outputs.drain(..) { if pending_request_id == request_id { updated_any = true; handle.finish_rate_limit_refresh(rate_limit_snapshots.as_slice(), now); } else { remaining.push((pending_request_id, handle)); } } self.refreshing_status_outputs = remaining; if updated_any { self.request_redraw(); } } pub(super) fn open_status_line_setup(&mut self) { let configured_status_line_items = self.configured_status_line_items(); let view = StatusLineSetupView::new( Some(configured_status_line_items.as_slice()), self.config.tui_status_line_use_colors, self.status_surface_preview_data(), self.app_event_tx.clone(), self.bottom_pane.list_keymap(), ); self.bottom_pane.show_view(Box::new(view)); } pub(super) fn open_terminal_title_setup(&mut self) { let configured_terminal_title_items = self.configured_terminal_title_items(); self.terminal_title_setup_original_items = Some(self.config.tui_terminal_title.clone()); let view = TerminalTitleSetupView::new( Some(configured_terminal_title_items.as_slice()), self.terminal_title_preview_data(), self.app_event_tx.clone(), self.bottom_pane.list_keymap(), ); self.bottom_pane.show_view(Box::new(view)); } pub(super) fn status_surface_preview_data(&mut self) -> StatusSurfacePreviewData { let mut preview_data = StatusSurfacePreviewData::from_iter( StatusSurfacePreviewItem::iter().filter_map(|item| { self.status_surface_preview_value_for_item(item) .map(|value| (item, value)) }), ); if self.rate_limit_snapshots_by_limit_id.contains_key("codex") { for item in [ StatusSurfacePreviewItem::FiveHourLimit, StatusSurfacePreviewItem::WeeklyLimit, ] { if self.status_surface_preview_value_for_item(item).is_none() { preview_data.suppress_placeholder(item); } } } preview_data } pub(super) fn terminal_title_preview_data(&mut self) -> StatusSurfacePreviewData { let mut preview_data = self.status_surface_preview_data(); let now = Instant::now(); for item in TerminalTitleItem::iter() { let Some(preview_item) = item.preview_item() else { continue; }; let Some(value) = self.terminal_title_value_for_item(item, now) else { continue; }; preview_data.set_live(preview_item, value); } preview_data } pub(super) fn status_line_context_window_size(&self) -> Option<i64> { self.token_info .as_ref() .and_then(|info| info.model_context_window) .or(self.config.model_context_window) } pub(super) fn status_line_context_remaining_percent(&self) -> Option<i64> { let Some(context_window) = self.status_line_context_window_size() else { return Some(100); }; let default_usage = TokenUsage::default(); let usage = self .token_info .as_ref() .map(|info| &info.last_token_usage) .unwrap_or(&default_usage); Some( usage .percent_of_context_window_remaining(context_window) .clamp(0, 100), ) } pub(super) fn status_line_context_used_percent(&self) -> Option<i64> { let remaining = self.status_line_context_remaining_percent().unwrap_or(100); Some((100 - remaining).clamp(0, 100)) } pub(super) fn status_line_total_usage(&self) -> TokenUsage { self.token_info .as_ref() .map(|info| info.total_token_usage.clone()) .unwrap_or_default() } pub(super) fn status_line_limit_display( &self, window: Option<&RateLimitWindowDisplay>, label: &str, ) -> Option<String> { let window = window?; let remaining = (100.0f64 - window.used_percent).clamp(0.0f64, 100.0f64); Some(format!("{label} {remaining:.0}% left")) } pub(super) fn status_line_reasoning_effort_label( effort: Option<&ReasoningEffortConfig>, ) -> String { match effort { None | Some(ReasoningEffortConfig::None) => "default".to_string(), Some(effort) => effort.as_str().to_string(), } }}