use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering;
use std::time::Duration;
use std::time::Instant;
use crate::tui_internal::app::app_server_requests::ResolvedAppServerRequest;
use crate::tui_internal::app_command::AppCommand;
use crate::tui_internal::app_event::HistoryLookupResponse;
use crate::tui_internal::app_server_approval_conversions::file_update_changes_to_display;
use crate::tui_internal::approval_events::ApplyPatchApprovalRequestEvent;
use crate::tui_internal::approval_events::ExecApprovalRequestEvent;
use crate::tui_internal::bottom_pane::StatusLineItem;
use crate::tui_internal::bottom_pane::StatusLineSetupView;
use crate::tui_internal::bottom_pane::StatusSurfacePreviewData;
use crate::tui_internal::bottom_pane::StatusSurfacePreviewItem;
use crate::tui_internal::bottom_pane::TerminalTitleItem;
use crate::tui_internal::bottom_pane::TerminalTitleSetupView;
use crate::tui_internal::diff_model::FileChange;
use crate::tui_internal::git_action_directives::parse_assistant_markdown;
use crate::tui_internal::legacy_core::config::Config;
use crate::tui_internal::legacy_core::config::PermissionProfileSnapshot;
use crate::tui_internal::mention_codec::LinkedMention;
use crate::tui_internal::mention_codec::encode_history_mentions;
use crate::tui_internal::model_catalog::ModelCatalog;
use crate::tui_internal::multi_agents;
use crate::tui_internal::multi_agents::AgentMetadata;
use crate::tui_internal::session_state::SessionNetworkProxyRuntime;
use crate::tui_internal::session_state::ThreadSessionState;
use crate::tui_internal::status::RateLimitWindowDisplay;
use crate::tui_internal::status::StatusAccountDisplay;
use crate::tui_internal::status::StatusHistoryHandle;
use crate::tui_internal::status::format_directory_display;
use crate::tui_internal::status::format_tokens_compact;
use crate::tui_internal::status::rate_limit_snapshot_display_for_limit;
use crate::tui_internal::terminal_hyperlinks::HyperlinkLine;
use crate::tui_internal::terminal_title::SetTerminalTitleResult;
use crate::tui_internal::terminal_title::clear_terminal_title;
use crate::tui_internal::terminal_title::set_terminal_title;
use crate::tui_internal::text_formatting::proper_join;
use crate::tui_internal::token_usage::TokenUsage;
use crate::tui_internal::token_usage::TokenUsageInfo;
use crate::tui_internal::version::CODEX_CLI_VERSION;
use lemurclaw_core::app_server_protocol::AddCreditsNudgeCreditType;
use lemurclaw_core::app_server_protocol::AddCreditsNudgeEmailStatus;
use lemurclaw_core::app_server_protocol::AppSummary;
use lemurclaw_core::app_server_protocol::CodexErrorInfo as AppServerCodexErrorInfo;
use lemurclaw_core::app_server_protocol::CollabAgentTool;
use lemurclaw_core::app_server_protocol::CollabAgentToolCallStatus;
use lemurclaw_core::app_server_protocol::CommandExecutionRequestApprovalParams;
use lemurclaw_core::app_server_protocol::CommandExecutionSource as ExecCommandSource;
use lemurclaw_core::app_server_protocol::CreditsSnapshot;
use lemurclaw_core::app_server_protocol::ErrorNotification;
use lemurclaw_core::app_server_protocol::FileChangeRequestApprovalParams;
use lemurclaw_core::app_server_protocol::GuardianApprovalReviewAction;
use lemurclaw_core::app_server_protocol::ItemCompletedNotification;
use lemurclaw_core::app_server_protocol::ItemStartedNotification;
use lemurclaw_core::app_server_protocol::McpServerElicitationRequest;
use lemurclaw_core::app_server_protocol::McpServerElicitationRequestParams;
use lemurclaw_core::app_server_protocol::McpServerStatusDetail;
use lemurclaw_core::app_server_protocol::ModelVerification as AppServerModelVerification;
use lemurclaw_core::app_server_protocol::RateLimitReachedType;
use lemurclaw_core::app_server_protocol::RateLimitSnapshot;
use lemurclaw_core::app_server_protocol::RequestId as AppServerRequestId;
use lemurclaw_core::app_server_protocol::ReviewTarget;
use lemurclaw_core::app_server_protocol::ServerNotification;
use lemurclaw_core::app_server_protocol::ServerRequest;
use lemurclaw_core::app_server_protocol::SkillMetadata as ProtocolSkillMetadata;
use lemurclaw_core::app_server_protocol::SkillsListResponse;
use lemurclaw_core::app_server_protocol::ThreadGoal as AppThreadGoal;
use lemurclaw_core::app_server_protocol::ThreadGoalStatus as AppThreadGoalStatus;
use lemurclaw_core::app_server_protocol::ThreadItem;
use lemurclaw_core::app_server_protocol::ThreadSettings;
use lemurclaw_core::app_server_protocol::ThreadSettingsUpdatedNotification;
use lemurclaw_core::app_server_protocol::ThreadTokenUsage;
use lemurclaw_core::app_server_protocol::ToolRequestUserInputParams;
use lemurclaw_core::app_server_protocol::Turn;
use lemurclaw_core::app_server_protocol::TurnCompletedNotification;
use lemurclaw_core::app_server_protocol::TurnPlanStepStatus;
use lemurclaw_core::app_server_protocol::TurnStatus;
use lemurclaw_core::app_server_protocol::UserInput;
use lemurclaw_core::config::ConfigLayerStackOrdering;
use lemurclaw_core::config::Constrained;
use lemurclaw_core::config::ConstraintResult;
use lemurclaw_core::config::types::ApprovalsReviewer;
use lemurclaw_core::config::types::Notifications;
use lemurclaw_core::config::types::WindowsSandboxModeToml;
use lemurclaw_core::connectors::AppInfo;
use lemurclaw_core::core_skills::model::SkillMetadata;
use lemurclaw_core::features::FEATURES;
use lemurclaw_core::features::Feature;
#[cfg(test)]
use lemurclaw_core::git_utils::CommitLogEntry;
use lemurclaw_core::git_utils::current_branch_name;
use lemurclaw_core::git_utils::get_git_repo_root;
use lemurclaw_core::git_utils::local_git_branches;
use lemurclaw_core::git_utils::recent_commits;
use lemurclaw_core::otel::RuntimeMetricsSummary;
use lemurclaw_core::otel::SessionTelemetry;
use lemurclaw_core::plugin::PluginCapabilitySummary;
use lemurclaw_core::protocol::ThreadId;
use lemurclaw_core::protocol::account::PlanType;
use lemurclaw_core::protocol::approvals::GuardianAssessmentAction;
use lemurclaw_core::protocol::approvals::GuardianAssessmentDecisionSource;
use lemurclaw_core::protocol::approvals::GuardianAssessmentEvent;
use lemurclaw_core::protocol::approvals::GuardianAssessmentStatus;
use lemurclaw_core::protocol::config_types::CollaborationMode;
use lemurclaw_core::protocol::config_types::CollaborationModeMask;
use lemurclaw_core::protocol::config_types::ModeKind;
use lemurclaw_core::protocol::config_types::Personality;
use lemurclaw_core::protocol::config_types::Settings;
#[cfg(any(target_os = "windows", test))]
use lemurclaw_core::protocol::config_types::WindowsSandboxLevel;
use lemurclaw_core::protocol::items::AgentMessageContent;
use lemurclaw_core::protocol::items::AgentMessageItem;
use lemurclaw_core::protocol::models::MessagePhase;
use lemurclaw_core::protocol::plan_tool::PlanItemArg as UpdatePlanItemArg;
use lemurclaw_core::protocol::plan_tool::StepStatus as UpdatePlanItemStatus;
use lemurclaw_core::protocol::request_permissions::RequestPermissionsEvent;
use lemurclaw_core::protocol::user_input::ByteRange;
use lemurclaw_core::protocol::user_input::TextElement;
use lemurclaw_core::terminal_detection::Multiplexer;
use lemurclaw_core::terminal_detection::TerminalInfo;
use lemurclaw_core::terminal_detection::TerminalName;
use lemurclaw_core::terminal_detection::terminal_info;
use lemurclaw_core::utils_absolute_path::AbsolutePathBuf;
use lemurclaw_core::utils_cli::resume_hint;
use lemurclaw_core::utils_path_uri::PathUri;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use rand::Rng;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::text::Text;
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::Wrap;
use tokio::sync::mpsc::UnboundedSender;
use tracing::debug;
use tracing::warn;
const DEFAULT_MODEL_DISPLAY_NAME: &str = "loading";
const MULTI_AGENT_ENABLE_TITLE: &str = "Enable subagents?";
const MULTI_AGENT_ENABLE_YES: &str = "Yes, enable";
const MULTI_AGENT_ENABLE_NO: &str = "Not now";
const MULTI_AGENT_ENABLE_NOTICE: &str = "Subagents will be enabled in the next session.";
const TRUSTED_ACCESS_FOR_CYBER_VERIFICATION_WARNING: &str = "Your conversations have multiple flags for possible cybersecurity risk. Responses may take longer because extra safety checks are on. To get authorized for security work, join the Trusted Access for Cyber program: https://chatgpt.com/cyber";
const MEMORIES_DOC_URL: &str = "https://developers.openai.com/codex/memories";
const MEMORIES_ENABLE_TITLE: &str = "Enable memories?";
const MEMORIES_ENABLE_YES: &str = "Yes, enable";
const MEMORIES_ENABLE_NO: &str = "Not now";
const MEMORIES_ENABLE_NOTICE: &str = "Memories will be enabled in the next session.";
const PLAN_MODE_REASONING_SCOPE_TITLE: &str = "Apply reasoning change";
const PLAN_MODE_REASONING_SCOPE_PLAN_ONLY: &str = "Apply to Plan mode override";
const PLAN_MODE_REASONING_SCOPE_ALL_MODES: &str = "Apply to global default and Plan mode override";
const CONNECTORS_SELECTION_VIEW_ID: &str = "connectors-selection";
const PET_SELECTION_LOADING_VIEW_ID: &str = "pet-selection-loading";
const AMBIENT_PET_WRAP_GAP_COLUMNS: u16 = 2;
const TUI_STUB_MESSAGE: &str = "Not available in TUI yet.";
const PARENT_OWNED_INPUT_MESSAGE: &str =
"This sub-agent is controlled by its parent. Direct input is disabled.";
fn queued_message_edit_binding_for_terminal(terminal_info: TerminalInfo) -> KeyBinding {
if matches!(
terminal_info.multiplexer.as_ref(),
Some(Multiplexer::Tmux { .. })
) {
return key_hint::shift(KeyCode::Left);
}
match terminal_info.name {
TerminalName::AppleTerminal | TerminalName::WarpTerminal | TerminalName::VsCode => {
key_hint::shift(KeyCode::Left)
}
TerminalName::Ghostty
| TerminalName::Iterm2
| TerminalName::WezTerm
| TerminalName::Kitty
| TerminalName::Alacritty
| TerminalName::Konsole
| TerminalName::GnomeTerminal
| TerminalName::Vte
| TerminalName::WindowsTerminal
| TerminalName::Dumb
| TerminalName::Unknown => key_hint::alt(KeyCode::Up),
}
}
fn queued_message_edit_hint_binding(
bindings: &[KeyBinding],
terminal_info: TerminalInfo,
) -> Option<KeyBinding> {
let terminal_binding = queued_message_edit_binding_for_terminal(terminal_info);
bindings
.contains(&terminal_binding)
.then_some(terminal_binding)
.or_else(|| bindings.first().copied())
}
fn normalize_thread_name(name: &str) -> Option<String> {
let trimmed = name.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
use crate::tui_internal::app_event::AppEvent;
use crate::tui_internal::app_event::ExitMode;
use crate::tui_internal::app_event::PermissionProfileSelection;
use crate::tui_internal::app_event::RateLimitRefreshOrigin;
#[cfg(target_os = "windows")]
use crate::tui_internal::app_event::WindowsSandboxEnableMode;
use crate::tui_internal::app_event_sender::AppEventSender;
use crate::tui_internal::auto_review_denials;
use crate::tui_internal::auto_review_denials::RecentAutoReviewDenials;
use crate::tui_internal::bottom_pane::ApplyPatchApprovalRequest;
use crate::tui_internal::bottom_pane::ApprovalRequest;
use crate::tui_internal::bottom_pane::BottomPane;
use crate::tui_internal::bottom_pane::BottomPaneParams;
use crate::tui_internal::bottom_pane::CancellationEvent;
use crate::tui_internal::bottom_pane::CollaborationModeIndicator;
use crate::tui_internal::bottom_pane::ColumnWidthMode;
use crate::tui_internal::bottom_pane::DOUBLE_PRESS_QUIT_SHORTCUT_ENABLED;
use crate::tui_internal::bottom_pane::ExecApprovalRequest;
use crate::tui_internal::bottom_pane::ExperimentalFeatureItem;
use crate::tui_internal::bottom_pane::ExperimentalFeaturesView;
use crate::tui_internal::bottom_pane::GoalStatusIndicator;
use crate::tui_internal::bottom_pane::HistoryEntry;
use crate::tui_internal::bottom_pane::InputResult;
use crate::tui_internal::bottom_pane::LocalImageAttachment;
use crate::tui_internal::bottom_pane::McpElicitationApprovalRequest;
use crate::tui_internal::bottom_pane::McpServerElicitationFormRequest;
use crate::tui_internal::bottom_pane::MemoriesSettingsView;
use crate::tui_internal::bottom_pane::MentionBinding;
use crate::tui_internal::bottom_pane::PermissionsApprovalRequest;
use crate::tui_internal::bottom_pane::QUIT_SHORTCUT_TIMEOUT;
use crate::tui_internal::bottom_pane::QueuedInputAction;
use crate::tui_internal::bottom_pane::SelectionAction;
use crate::tui_internal::bottom_pane::SelectionItem;
use crate::tui_internal::bottom_pane::SelectionViewParams;
use crate::tui_internal::bottom_pane::custom_prompt_view::CustomPromptView;
use crate::tui_internal::bottom_pane::popup_consts::standard_popup_hint_line;
use crate::tui_internal::clipboard_paste::paste_image_to_temp_png;
use crate::tui_internal::collaboration_modes;
use crate::tui_internal::diff_render::display_path_for;
use crate::tui_internal::exec_cell::CommandOutput;
use crate::tui_internal::exec_cell::ExecCell;
use crate::tui_internal::exec_cell::new_active_exec_command;
use crate::tui_internal::exec_command::split_command_string;
use crate::tui_internal::exec_command::strip_bash_lc_and_escape;
use crate::tui_internal::get_git_diff::get_git_diff;
use crate::tui_internal::history_cell;
use crate::tui_internal::history_cell::HistoryCell;
use crate::tui_internal::history_cell::HistoryRenderMode;
use crate::tui_internal::history_cell::HookCell;
use crate::tui_internal::history_cell::McpInvocation;
use crate::tui_internal::history_cell::McpToolCallCell;
use crate::tui_internal::history_cell::PlainHistoryCell;
use crate::tui_internal::history_cell::WebSearchCell;
use crate::tui_internal::key_hint;
use crate::tui_internal::key_hint::KeyBinding;
use crate::tui_internal::key_hint::KeyBindingListExt;
use crate::tui_internal::keymap::ChatKeymap;
use crate::tui_internal::keymap::RuntimeKeymap;
use crate::tui_internal::render::Insets;
use crate::tui_internal::render::renderable::ColumnRenderable;
use crate::tui_internal::render::renderable::FlexRenderable;
use crate::tui_internal::render::renderable::Renderable;
use crate::tui_internal::render::renderable::RenderableExt;
use crate::tui_internal::render::renderable::RenderableItem;
use crate::tui_internal::slash_command::SlashCommand;
use crate::tui_internal::status::RateLimitSnapshotDisplay;
use crate::tui_internal::status::remote_connection::RemoteConnectionStatus;
use crate::tui_internal::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
use crate::tui_internal::status_indicator_widget::StatusDetailsCapitalization;
use crate::tui_internal::text_formatting::truncate_text;
use crate::tui_internal::tui::FrameRequester;
mod command_lifecycle;
mod connectors;
mod constructor;
use self::connectors::ConnectorsState;
mod exec_state;
use self::exec_state::RunningCommand;
use self::exec_state::UnifiedExecProcessSummary;
use self::exec_state::UnifiedExecWaitState;
use self::exec_state::UnifiedExecWaitStreak;
use self::exec_state::command_execution_command_and_parsed;
use self::exec_state::is_standard_tool_call;
use self::exec_state::is_unified_exec_source;
mod goal_status;
use self::goal_status::GoalStatusState;
#[cfg(test)]
use self::goal_status::goal_status_indicator_from_app_goal;
mod goal_menu;
mod ide_context;
use self::ide_context::IdeContextState;
mod input_queue;
use self::input_queue::InputQueueState;
mod input_flow;
mod input_restore;
mod input_submission;
mod interrupts;
use self::interrupts::InterruptManager;
mod keymap_picker;
mod mcp_startup;
use self::mcp_startup::McpStartupStatus;
mod pets;
mod session_flow;
mod session_header;
use self::session_header::SessionHeader;
mod hook_lifecycle;
mod hooks;
mod interaction;
mod skills;
mod slash_dispatch;
use self::skills::collect_tool_mentions;
use self::skills::find_app_mentions;
use self::skills::find_skill_mentions_with_tool_mentions;
use self::skills::is_app_mentionable;
mod plugin_catalog;
mod plugins;
use self::plugins::PluginInstallAuthFlowState;
use self::plugins::PluginListFetchState;
use self::plugins::PluginsCacheState;
mod plan_implementation;
use self::plan_implementation::PLAN_IMPLEMENTATION_TITLE;
mod model_popups;
mod notifications;
use self::notifications::Notification;
mod permission_popups;
mod permissions_menu;
mod protocol;
mod protocol_requests;
mod rate_limits;
use self::rate_limits::RateLimitErrorKind;
use self::rate_limits::RateLimitSwitchPromptState;
use self::rate_limits::RateLimitWarningState;
use self::rate_limits::app_server_rate_limit_error_kind;
pub(crate) use self::rate_limits::fallback_limit_label;
use self::rate_limits::is_app_server_cyber_policy_error;
mod reset_credits;
pub(crate) use self::rate_limits::limit_label_for_window;
mod reasoning_shortcuts;
mod rendering;
mod replay;
mod review;
mod review_popups;
use self::review::ReviewState;
#[cfg(test)]
pub(crate) use self::review_popups::show_review_commit_picker_with_entries;
mod safety_buffering;
mod service_tiers;
mod settings;
mod settings_popups;
mod side;
use self::safety_buffering::SafetyBufferingState;
mod status_state;
mod windows_sandbox_prompts;
use self::status_state::StatusIndicatorState;
use self::status_state::StatusState;
use self::status_state::TerminalTitleStatusKind;
mod status_controls;
mod status_surfaces;
mod streaming;
use self::status_surfaces::CachedProjectRootName;
mod tokens;
pub(crate) use self::tokens::TokenActivityView;
mod tool_lifecycle;
mod tool_requests;
mod transcript;
use self::transcript::TranscriptState;
mod turn_lifecycle;
mod turn_runtime;
use self::turn_lifecycle::TurnLifecycleState;
mod usage;
mod user_messages;
use self::user_messages::PendingSteer;
use self::user_messages::PendingSteerCompareKey;
use self::user_messages::QueueDrain;
use self::user_messages::QueuedUserMessage;
use self::user_messages::ShellEscapePolicy;
use self::user_messages::ThreadComposerState;
pub(crate) use self::user_messages::ThreadInputState;
pub(crate) use self::user_messages::ThreadInputStateRestoreMode;
pub(crate) use self::user_messages::UserMessage;
use self::user_messages::UserMessageDisplay;
#[cfg(test)]
use self::user_messages::UserMessageHistoryOverride;
use self::user_messages::UserMessageHistoryRecord;
use self::user_messages::app_server_text_elements;
pub(crate) use self::user_messages::create_initial_user_message;
pub(crate) use self::user_messages::mention_bindings_from_user_inputs;
use self::user_messages::merge_user_messages;
use self::user_messages::merge_user_messages_with_history_record;
#[cfg(test)]
use self::user_messages::remap_placeholders_for_message;
use self::user_messages::user_message_display_for_history;
use self::user_messages::user_message_for_restore;
use self::user_messages::user_message_preview_text;
mod warnings;
use self::warnings::WarningDisplayState;
pub(crate) use crate::tui_internal::branch_summary::StatusLineGitSummary;
use crate::tui_internal::streaming::chunking::AdaptiveChunkingPolicy;
use crate::tui_internal::streaming::commit_tick::CommitTickScope;
use crate::tui_internal::streaming::commit_tick::run_commit_tick;
use crate::tui_internal::streaming::controller::PlanStreamController;
use crate::tui_internal::streaming::controller::StreamController;
use crate::tui_internal::workspace_command::WorkspaceCommandRunner;
use chrono::Local;
use lemurclaw_core::app_server_protocol::AskForApproval;
use lemurclaw_core::file_search::FileMatch;
use lemurclaw_core::protocol::models::ActivePermissionProfile;
use lemurclaw_core::protocol::models::PermissionProfile;
use lemurclaw_core::protocol::openai_models::InputModality;
use lemurclaw_core::protocol::openai_models::ModelPreset;
use lemurclaw_core::protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
use lemurclaw_core::protocol::plan_tool::StepStatus;
use lemurclaw_core::protocol::plan_tool::UpdatePlanArgs;
use lemurclaw_core::utils_approval_presets::ApprovalPreset;
use lemurclaw_core::utils_approval_presets::builtin_approval_presets;
use strum::IntoEnumIterator;
use unicode_segmentation::UnicodeSegmentation;
const USER_SHELL_COMMAND_HELP_TITLE: &str = "Prefix a command with ! to run it locally";
const USER_SHELL_COMMAND_HELP_HINT: &str = "Example: !ls";
const ASK_FOR_APPROVAL_LABEL: &str = "Ask for approval";
const APPROVE_FOR_ME_LABEL: &str = "Approve for me";
const AUTO_REVIEW_DESCRIPTION: &str = "Only ask for actions detected as potentially unsafe.";
const DEFAULT_OPENAI_BASE_URL: &str = "https://api.openai.com/v1";
const DEFAULT_STATUS_LINE_ITEMS: [&str; 2] = ["model-with-reasoning", "current-dir"];
pub(crate) struct ChatWidgetInit {
pub(crate) config: Config,
pub(crate) frame_requester: FrameRequester,
pub(crate) app_event_tx: AppEventSender,
pub(crate) workspace_command_runner: Option<WorkspaceCommandRunner>,
pub(crate) initial_user_message: Option<UserMessage>,
pub(crate) enhanced_keys_supported: bool,
pub(crate) has_chatgpt_account: bool,
pub(crate) has_codex_backend_auth: bool,
pub(crate) model_catalog: Arc<ModelCatalog>,
pub(crate) feedback: lemurclaw_core::feedback::CodexFeedback,
pub(crate) is_first_run: bool,
pub(crate) status_account_display: Option<StatusAccountDisplay>,
pub(crate) runtime_model_provider_base_url: Option<String>,
pub(crate) initial_plan_type: Option<PlanType>,
pub(crate) model: Option<String>,
pub(crate) startup_tooltip_override: Option<String>,
pub(crate) status_line_invalid_items_warned: Arc<AtomicBool>,
pub(crate) terminal_title_invalid_items_warned: Arc<AtomicBool>,
pub(crate) session_telemetry: SessionTelemetry,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum ExternalEditorState {
#[default]
Closed,
Requested,
Active,
}
pub(crate) struct ChatWidget {
app_event_tx: AppEventSender,
codex_op_target: CodexOpTarget,
bottom_pane: BottomPane,
transcript: TranscriptState,
config: Config,
raw_output_mode: bool,
effective_service_tier: Option<String>,
current_collaboration_mode: CollaborationMode,
active_collaboration_mask: Option<CollaborationModeMask>,
has_chatgpt_account: bool,
has_codex_backend_auth: bool,
model_catalog: Arc<ModelCatalog>,
session_telemetry: SessionTelemetry,
session_header: SessionHeader,
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)>,
next_status_refresh_request_id: u64,
refreshing_token_activity_output: Option<tokens::PendingTokenActivityOutput>,
completed_token_activity_output: Option<history_cell::CompositeHistoryCell>,
next_token_activity_request_id: u64,
pending_rate_limit_reset_request_id: Option<u64>,
pending_rate_limit_reset_idempotency_key: Option<String>,
rate_limit_reset_picker_request_id: Option<u64>,
pending_rate_limit_reset_hint_request_id: Option<u64>,
pending_usage_menu_rate_limit_request_id: Option<u64>,
pending_rate_limit_reset_hint: Option<PlainHistoryCell>,
available_rate_limit_reset_credits: Option<i64>,
next_rate_limit_reset_request_id: u64,
plan_type: Option<PlanType>,
codex_rate_limit_reached_type: Option<RateLimitReachedType>,
codex_spend_control_reached: Option<bool>,
rate_limit_warnings: RateLimitWarningState,
warning_display_state: WarningDisplayState,
rate_limit_switch_prompt: RateLimitSwitchPromptState,
add_credits_nudge_email_in_flight: Option<AddCreditsNudgeCreditType>,
adaptive_chunking: AdaptiveChunkingPolicy,
stream_controller: Option<StreamController>,
plan_stream_controller: Option<PlanStreamController>,
pending_stream_consolidations: usize,
clipboard_lease: Option<crate::tui_internal::clipboard_copy::ClipboardLease>,
copy_last_response_binding: Vec<KeyBinding>,
running_commands: HashMap<String, RunningCommand>,
collab_agent_metadata: HashMap<ThreadId, AgentMetadata>,
pending_collab_spawn_requests: HashMap<String, multi_agents::SpawnRequestSummary>,
suppressed_exec_calls: HashSet<String>,
skills_all: Vec<ProtocolSkillMetadata>,
skills_initial_state: Option<HashMap<AbsolutePathBuf, bool>>,
last_unified_wait: Option<UnifiedExecWaitState>,
unified_exec_wait_streak: Option<UnifiedExecWaitStreak>,
turn_lifecycle: TurnLifecycleState,
safety_buffering: SafetyBufferingState,
task_complete_pending: bool,
unified_exec_processes: Vec<UnifiedExecProcessSummary>,
mcp_startup_status: Option<HashMap<String, McpStartupStatus>>,
mcp_startup_expected_servers: Option<HashSet<String>>,
mcp_startup_ignore_updates_until_next_start: bool,
mcp_startup_allow_terminal_only_next_round: bool,
mcp_startup_pending_next_round: HashMap<String, McpStartupStatus>,
mcp_startup_pending_next_round_saw_starting: bool,
connectors: ConnectorsState,
ide_context: IdeContextState,
plugins_cache: PluginsCacheState,
plugins_fetch_state: PluginListFetchState,
plugin_remote_sections_loading: bool,
plugin_remote_sections_loaded: bool,
plugin_remote_section_errors: Vec<crate::tui_internal::app_event::PluginRemoteSectionError>,
plugin_install_apps_needing_auth: Vec<AppSummary>,
plugin_install_auth_flow: Option<PluginInstallAuthFlowState>,
plugins_active_tab_id: Option<String>,
newly_installed_marketplace_tab_id: Option<String>,
interrupts: InterruptManager,
reasoning_buffer: String,
reasoning_header: Option<String>,
reasoning_summary_parts: Vec<String>,
status_state: StatusState,
review: ReviewState,
active_hook_cell: Option<HookCell>,
ambient_pet: Option<crate::tui_internal::pets::AmbientPet>,
pet_picker_preview_state: crate::tui_internal::pets::PetPickerPreviewState,
pet_picker_preview_pet: Option<crate::tui_internal::pets::AmbientPet>,
pet_picker_preview_request_id: u64,
pet_picker_preview_image_visible: std::cell::Cell<bool>,
pet_selection_load_request_id: u64,
#[cfg(test)]
pet_image_support_override: Option<crate::tui_internal::pets::PetImageSupport>,
thread_id: Option<ThreadId>,
dismissed_plan_mode_nudge_scopes: HashSet<PlanModeNudgeScope>,
thread_name: Option<String>,
thread_rename_block_message: Option<String>,
active_side_conversation: bool,
blocks_direct_input: bool,
normal_placeholder_text: String,
side_placeholder_text: String,
forked_from: Option<ThreadId>,
interrupted_turn_notice_mode: InterruptedTurnNoticeMode,
frame_requester: FrameRequester,
show_welcome_banner: bool,
startup_tooltip_override: Option<String>,
suppress_session_configured_redraw: bool,
suppress_initial_user_message_submit: bool,
input_queue: InputQueueState,
safety_buffering_prompt: Option<UserMessage>,
chat_keymap: ChatKeymap,
queued_message_edit_hint_binding: Option<KeyBinding>,
pending_notification: Option<Notification>,
quit_shortcut_expires_at: Option<Instant>,
quit_shortcut_key: Option<KeyBinding>,
turn_runtime_metrics: RuntimeMetricsSummary,
last_rendered_width: std::cell::Cell<Option<usize>>,
feedback: lemurclaw_core::feedback::CodexFeedback,
current_rollout_path: Option<PathBuf>,
current_cwd: Option<PathBuf>,
workspace_command_runner: Option<WorkspaceCommandRunner>,
instruction_source_paths: Vec<PathUri>,
session_network_proxy: Option<SessionNetworkProxyRuntime>,
status_line_invalid_items_warned: Arc<AtomicBool>,
terminal_title_invalid_items_warned: Arc<AtomicBool>,
pub(crate) last_terminal_title: Option<String>,
last_terminal_title_requires_action: bool,
terminal_title_setup_original_items: Option<Option<Vec<String>>>,
terminal_title_animation_origin: Instant,
status_line_project_root_name_cache: Option<CachedProjectRootName>,
status_line_branch: Option<String>,
status_line_branch_cwd: Option<PathBuf>,
status_line_branch_pending: bool,
status_line_branch_lookup_complete: bool,
status_line_git_summary: Option<StatusLineGitSummary>,
status_line_git_summary_cwd: Option<PathBuf>,
status_line_git_summary_pending: bool,
status_line_git_summary_lookup_complete: bool,
status_line_workspace_headline: Option<String>,
status_line_workspace_headline_pending_request_id: Option<u64>,
next_status_line_workspace_headline_request_id: u64,
status_line_workspace_headline_last_requested_at: Option<Instant>,
status_line_workspace_messages_disabled: bool,
current_goal_status_indicator: Option<GoalStatusIndicator>,
current_goal_status: Option<GoalStatusState>,
external_editor_state: ExternalEditorState,
last_rendered_user_message_display: Option<UserMessageDisplay>,
last_non_retry_error: Option<(String, String)>,
}
#[cfg_attr(not(test), allow(dead_code))]
enum CodexOpTarget {
Direct(UnboundedSender<AppCommand>),
AppEvent,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ActiveCellTranscriptKey {
pub(crate) revision: u64,
pub(crate) is_stream_continuation: bool,
pub(crate) animation_tick: Option<u64>,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub(crate) enum InterruptedTurnNoticeMode {
#[default]
Default,
Suppress,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum ReplayKind {
ResumeInitialMessages,
ThreadSnapshot,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum SessionConfiguredDisplay {
Normal,
PromptEdit,
Quiet,
SideConversation,
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
enum PlanModeNudgeScope {
NewThread,
Thread(ThreadId),
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub(crate) enum TurnAbortReason {
Interrupted,
BudgetLimited,
}
fn contains_plan_keyword(text: &str) -> bool {
text.split(|ch: char| !ch.is_alphanumeric() && ch != '_')
.any(|word| word.eq_ignore_ascii_case("plan"))
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum ThreadItemRenderSource {
Live,
Replay(ReplayKind),
}
impl ThreadItemRenderSource {
fn is_replay(self) -> bool {
matches!(self, Self::Replay(_))
}
fn replay_kind(self) -> Option<ReplayKind> {
match self {
Self::Live => None,
Self::Replay(replay_kind) => Some(replay_kind),
}
}
}
fn exec_approval_request_from_params(
params: CommandExecutionRequestApprovalParams,
fallback_cwd: &AbsolutePathBuf,
) -> ExecApprovalRequestEvent {
let cwd = params
.cwd
.and_then(|cwd| cwd.to_inferred_abs_path())
.unwrap_or_else(|| fallback_cwd.clone());
ExecApprovalRequestEvent {
call_id: params.item_id,
command: params
.command
.as_deref()
.map(split_command_string)
.unwrap_or_default(),
cwd,
reason: params.reason,
network_approval_context: params.network_approval_context,
additional_permissions: params.additional_permissions,
turn_id: params.turn_id,
approval_id: params.approval_id,
environment_id: params.environment_id,
proposed_execpolicy_amendment: params.proposed_execpolicy_amendment,
proposed_network_policy_amendments: params.proposed_network_policy_amendments,
available_decisions: params.available_decisions,
}
}
fn patch_approval_request_from_params(
params: FileChangeRequestApprovalParams,
) -> ApplyPatchApprovalRequestEvent {
ApplyPatchApprovalRequestEvent {
call_id: params.item_id,
turn_id: params.turn_id,
changes: HashMap::new(),
reason: params.reason,
grant_root: params.grant_root,
}
}
fn request_permissions_from_params(
params: lemurclaw_core::app_server_protocol::PermissionsRequestApprovalParams,
) -> std::io::Result<RequestPermissionsEvent> {
Ok(RequestPermissionsEvent {
turn_id: params.turn_id,
call_id: params.item_id,
environment_id: params.environment_id,
started_at_ms: params.started_at_ms,
reason: params.reason,
permissions: params.permissions.try_into()?,
cwd: Some(params.cwd),
})
}
fn token_usage_info_from_app_server(token_usage: ThreadTokenUsage) -> TokenUsageInfo {
TokenUsageInfo {
total_token_usage: TokenUsage {
total_tokens: token_usage.total.total_tokens,
input_tokens: token_usage.total.input_tokens,
cached_input_tokens: token_usage.total.cached_input_tokens,
output_tokens: token_usage.total.output_tokens,
reasoning_output_tokens: token_usage.total.reasoning_output_tokens,
},
last_token_usage: TokenUsage {
total_tokens: token_usage.last.total_tokens,
input_tokens: token_usage.last.input_tokens,
cached_input_tokens: token_usage.last.cached_input_tokens,
output_tokens: token_usage.last.output_tokens,
reasoning_output_tokens: token_usage.last.reasoning_output_tokens,
},
model_context_window: token_usage.model_context_window,
}
}
impl ChatWidget {
pub(crate) fn set_collab_agent_metadata(
&mut self,
thread_id: ThreadId,
agent_nickname: Option<String>,
agent_role: Option<String>,
) {
self.collab_agent_metadata.insert(
thread_id,
AgentMetadata {
agent_nickname,
agent_role,
},
);
}
fn collab_agent_metadata(&self, thread_id: ThreadId) -> AgentMetadata {
self.collab_agent_metadata
.get(&thread_id)
.cloned()
.unwrap_or_default()
}
fn restore_retry_status_header_if_present(&mut self) {
if let Some(header) = self.status_state.take_retry_status_header() {
self.set_status_header(header);
}
}
fn record_agent_markdown(&mut self, message: &str) {
if !message.is_empty() {
self.transcript.record_agent_markdown(message.to_string());
}
}
pub(crate) fn open_feedback_note(
&mut self,
category: crate::tui_internal::app_event::FeedbackCategory,
include_logs: bool,
) {
self.show_feedback_note(category, include_logs);
}
fn show_feedback_note(
&mut self,
category: crate::tui_internal::app_event::FeedbackCategory,
include_logs: bool,
) {
let view = crate::tui_internal::bottom_pane::FeedbackNoteView::new(
category,
self.turn_lifecycle.last_turn_id.clone(),
self.app_event_tx.clone(),
include_logs,
);
self.bottom_pane.show_view(Box::new(view));
self.request_redraw();
}
pub(crate) fn open_app_link_view(&mut self, params: crate::tui_internal::bottom_pane::AppLinkViewParams) {
let view = crate::tui_internal::bottom_pane::AppLinkView::new_with_keymap(
params,
self.app_event_tx.clone(),
self.bottom_pane.list_keymap(),
);
self.bottom_pane.show_view(Box::new(view));
self.request_redraw();
}
pub(crate) fn dismiss_app_server_request(&mut self, request: &ResolvedAppServerRequest) {
let removed_deferred = self.interrupts.remove_resolved_prompt(request);
let removed_visible = self.bottom_pane.dismiss_app_server_request(request);
if removed_deferred || removed_visible {
self.request_redraw();
}
}
pub(crate) fn open_feedback_consent(&mut self, category: crate::tui_internal::app_event::FeedbackCategory) {
let snapshot = self.feedback.snapshot(self.thread_id);
#[cfg(target_os = "windows")]
let include_windows_sandbox_log =
lemurclaw_core::windows_sandbox::current_log_file_path_for_codex_home(&self.config.codex_home)
.is_file();
#[cfg(not(target_os = "windows"))]
let include_windows_sandbox_log = false;
let params = crate::tui_internal::bottom_pane::feedback_upload_consent_params(
self.app_event_tx.clone(),
category,
self.current_rollout_path.clone(),
self.thread_id
.map(|thread_id| format!("auto-review-rollout-{thread_id}.jsonl")),
include_windows_sandbox_log,
snapshot.feedback_diagnostics(),
);
self.bottom_pane.show_selection_view(params);
self.request_redraw();
}
pub(crate) fn open_multi_agent_enable_prompt(&mut self) {
let items = vec![
SelectionItem {
name: MULTI_AGENT_ENABLE_YES.to_string(),
description: Some(
"Save the setting now. You will need a new session to use it.".to_string(),
),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::UpdateFeatureFlags {
updates: vec![(Feature::Collab, true)],
});
tx.send(AppEvent::InsertHistoryCell(Box::new(
history_cell::new_warning_event(MULTI_AGENT_ENABLE_NOTICE.to_string()),
)));
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: MULTI_AGENT_ENABLE_NO.to_string(),
description: Some("Keep subagents disabled.".to_string()),
dismiss_on_select: true,
..Default::default()
},
];
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some(MULTI_AGENT_ENABLE_TITLE.to_string()),
subtitle: Some("Subagents are currently disabled in your config.".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
}
pub(crate) fn open_memories_popup(&mut self) {
if !self.config.features.enabled(Feature::MemoryTool) {
self.open_memories_enable_prompt();
return;
}
let view = MemoriesSettingsView::new(
self.config.memories.use_memories,
self.config.memories.generate_memories,
self.app_event_tx.clone(),
self.bottom_pane.list_keymap(),
);
self.bottom_pane.show_view(Box::new(view));
}
pub(crate) fn open_memories_enable_prompt(&mut self) {
let items = vec![
SelectionItem {
name: MEMORIES_ENABLE_YES.to_string(),
description: Some(
"Save the setting now. You will need a new session to use it.".to_string(),
),
actions: vec![Box::new(|tx| {
tx.send(AppEvent::UpdateFeatureFlags {
updates: vec![(Feature::MemoryTool, true)],
});
})],
dismiss_on_select: true,
..Default::default()
},
SelectionItem {
name: MEMORIES_ENABLE_NO.to_string(),
description: Some("Keep memories disabled.".to_string()),
dismiss_on_select: true,
..Default::default()
},
];
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some(MEMORIES_ENABLE_TITLE.to_string()),
subtitle: Some("Memories are currently disabled in your config.".to_string()),
footer_note: Some(Line::from(vec![
"Learn more: ".dim(),
MEMORIES_DOC_URL.cyan().underlined(),
])),
footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
}
pub(crate) fn set_memory_settings(&mut self, use_memories: bool, generate_memories: bool) {
self.config.memories.use_memories = use_memories;
self.config.memories.generate_memories = generate_memories;
}
pub(crate) fn set_token_info(&mut self, info: Option<TokenUsageInfo>) {
match info {
Some(info) => self.apply_token_info(info),
None => {
self.bottom_pane
.set_context_window( None, None);
self.token_info = None;
}
}
}
fn apply_token_info(&mut self, info: TokenUsageInfo) {
let percent = self.context_remaining_percent(&info);
let used_tokens = self.context_used_tokens(&info, percent.is_some());
self.bottom_pane.set_context_window(percent, used_tokens);
self.token_info = Some(info);
}
fn context_remaining_percent(&self, info: &TokenUsageInfo) -> Option<i64> {
info.model_context_window.map(|window| {
info.last_token_usage
.percent_of_context_window_remaining(window)
})
}
fn context_used_tokens(&self, info: &TokenUsageInfo, percent_known: bool) -> Option<i64> {
if percent_known {
return None;
}
Some(info.total_token_usage.tokens_in_context_window())
}
fn restore_pre_review_token_info(&mut self) {
if let Some(saved) = self.review.pre_review_token_info.take() {
match saved {
Some(info) => self.apply_token_info(info),
None => {
self.bottom_pane
.set_context_window( None, None);
self.token_info = None;
}
}
}
}
pub(crate) fn handle_history_entry_response(&mut self, event: HistoryLookupResponse) {
self.bottom_pane.on_history_lookup_response(event);
}
pub(crate) fn pre_draw_tick(&mut self) {
self.update_due_hook_visibility();
self.schedule_hook_timer_if_needed();
self.bottom_pane.pre_draw_tick();
if let Some(pet) = self.ambient_pet.as_ref() {
pet.schedule_next_frame();
}
self.refresh_plan_mode_nudge();
self.refresh_goal_status_indicator_for_time_tick();
if self.terminal_title_shows_action_required() != self.last_terminal_title_requires_action {
self.refresh_terminal_title();
}
if self.should_animate_terminal_title_spinner()
|| self.should_animate_terminal_title_action_required()
{
self.refresh_terminal_title();
}
self.refresh_status_line_if_workspace_headline_due();
}
fn flush_active_cell(&mut self) {
if let Some(active) = self.transcript.active_cell.take() {
self.transcript.needs_final_message_separator = true;
self.app_event_tx.send(AppEvent::InsertHistoryCell(active));
self.request_pending_usage_output_insertion();
}
}
pub(crate) fn add_to_history(&mut self, cell: impl HistoryCell + 'static) {
self.add_boxed_history(Box::new(cell));
}
fn add_boxed_history(&mut self, cell: Box<dyn HistoryCell>) {
let keep_placeholder_header_active = !self.is_session_configured()
&& self
.transcript
.active_cell
.as_ref()
.is_some_and(|c| c.as_any().is::<history_cell::SessionHeaderHistoryCell>());
if !keep_placeholder_header_active && !cell.display_lines(u16::MAX).is_empty() {
if !self.has_active_stream_tail() {
self.flush_active_cell();
}
self.transcript.needs_final_message_separator = true;
}
self.app_event_tx.send(AppEvent::InsertHistoryCell(cell));
}
fn enter_review_mode_with_hint(&mut self, hint: String, from_replay: bool) {
if self.review.pre_review_token_info.is_none() {
self.review.pre_review_token_info = Some(self.token_info.clone());
}
if !from_replay && !self.bottom_pane.is_task_running() {
self.bottom_pane.set_task_running( true);
}
self.review.is_review_mode = true;
let banner = format!(">> Code review started: {hint} <<");
self.add_to_history(history_cell::new_review_status_line(banner));
self.request_redraw();
}
fn exit_review_mode_after_item(&mut self) {
self.flush_answer_stream_with_separator();
self.flush_interrupt_queue();
self.flush_active_cell();
self.review.is_review_mode = false;
self.restore_pre_review_token_info();
self.add_to_history(history_cell::new_review_status_line(
"<< Code review finished >>".to_string(),
));
self.request_redraw();
}
fn on_committed_user_message(&mut self, items: &[UserInput], from_replay: bool) {
let display = Self::user_message_display_from_inputs(items);
if from_replay {
if self.review.is_review_mode {
return;
}
self.bottom_pane
.record_replayed_user_message_history(HistoryEntry {
text: display.message.clone(),
text_elements: display.text_elements.clone(),
local_image_paths: display.local_images.clone(),
remote_image_urls: display.remote_image_urls.clone(),
mention_bindings: mention_bindings_from_user_inputs(items, &display.message),
pending_pastes: Vec::new(),
});
self.on_user_message_display(display);
return;
}
let compare_key = Self::pending_steer_compare_key_from_items(items);
if self
.input_queue
.pending_steers
.front()
.is_some_and(|pending| pending.compare_key == compare_key)
{
if let Some(pending) = self.input_queue.pending_steers.pop_front() {
self.refresh_pending_input_preview();
let pending_display =
user_message_display_for_history(pending.user_message, &pending.history_record);
self.on_user_message_display(pending_display);
} else if self.last_rendered_user_message_display.as_ref() != Some(&display) {
tracing::warn!(
"pending steer matched compare key but queue was empty when rendering committed user message"
);
self.on_user_message_display(display);
}
} else if !self.review.is_review_mode
&& self.last_rendered_user_message_display.as_ref() != Some(&display)
{
self.on_user_message_display(display);
}
}
fn on_user_message_display(&mut self, display: UserMessageDisplay) {
self.last_rendered_user_message_display = Some(display.clone());
if !display.message.trim().is_empty()
|| !display.text_elements.is_empty()
|| !display.local_images.is_empty()
|| !display.remote_image_urls.is_empty()
{
self.add_to_history(history_cell::new_user_prompt(
display.message,
display.text_elements,
display.local_images,
display.remote_image_urls,
));
}
self.transcript.needs_final_message_separator = false;
}
fn request_immediate_exit(&self) {
self.app_event_tx.send(AppEvent::Exit(ExitMode::Immediate));
}
fn request_quit_without_confirmation(&self) {
self.app_event_tx
.send(AppEvent::Exit(ExitMode::ShutdownFirst));
}
pub(crate) fn show_shutdown_in_progress(&mut self) {
self.bottom_pane.show_shutdown_in_progress();
}
fn request_redraw(&mut self) {
self.frame_requester.schedule_frame();
}
fn bump_active_cell_revision(&mut self) {
self.transcript.bump_active_cell_revision();
}
fn finalize_active_cell_as_failed(&mut self) {
if let Some(mut cell) = self.transcript.active_cell.take() {
if let Some(exec) = cell.as_any_mut().downcast_mut::<ExecCell>() {
exec.mark_failed();
} else if let Some(tool) = cell.as_any_mut().downcast_mut::<McpToolCallCell>() {
tool.mark_failed();
}
self.add_boxed_history(cell);
self.request_pending_usage_output_insertion();
}
}
pub(crate) fn set_pending_thread_approvals(&mut self, threads: Vec<String>) {
self.bottom_pane.set_pending_thread_approvals(threads);
}
pub(crate) fn clear_thread_rename_block(&mut self) {
self.thread_rename_block_message = None;
}
pub(crate) fn set_thread_rename_block_message(&mut self, message: impl Into<String>) {
self.thread_rename_block_message = Some(message.into());
}
pub(crate) fn set_interrupted_turn_notice_mode(&mut self, mode: InterruptedTurnNoticeMode) {
self.interrupted_turn_notice_mode = mode;
}
pub(crate) fn add_diff_in_progress(&mut self) {
self.request_redraw();
}
pub(crate) fn on_diff_complete(&mut self) {
self.request_redraw();
}
pub(crate) fn add_debug_config_output(&mut self) {
self.add_to_history(crate::tui_internal::debug_config::new_debug_config_output(
&self.config,
self.session_network_proxy.as_ref(),
));
}
pub(crate) fn add_ps_output(&mut self) {
let processes = self
.unified_exec_processes
.iter()
.map(|process| history_cell::UnifiedExecProcessDetails {
command_display: process.command_display.clone(),
recent_chunks: process.recent_chunks.clone(),
})
.collect();
self.add_to_history(history_cell::new_unified_exec_processes_output(processes));
}
fn clean_background_terminals(&mut self) {
self.submit_op(AppCommand::clean_background_terminals());
self.unified_exec_processes.clear();
self.sync_unified_exec_footer();
self.add_info_message(
"Stopping all background terminals.".to_string(),
None,
);
}
fn plugins_for_mentions(&self) -> Option<&[PluginCapabilitySummary]> {
if !self.config.features.enabled(Feature::Plugins) {
return None;
}
self.bottom_pane.plugins().map(Vec::as_slice)
}
fn placeholder_session_header_cell(config: &Config) -> Box<dyn HistoryCell> {
let placeholder_style = Style::default().add_modifier(Modifier::DIM | Modifier::ITALIC);
Box::new(
history_cell::SessionHeaderHistoryCell::new_with_style(
DEFAULT_MODEL_DISPLAY_NAME.to_string(),
placeholder_style,
None,
false,
config.cwd.to_path_buf(),
CODEX_CLI_VERSION,
)
.with_yolo_mode(history_cell::is_yolo_mode(config)),
)
}
fn apply_session_info_cell(&mut self, cell: history_cell::SessionInfoCell) {
let mut session_info_cell = Some(Box::new(cell) as Box<dyn HistoryCell>);
let merged_header = if let Some(active) = self.transcript.active_cell.take() {
if active
.as_any()
.is::<history_cell::SessionHeaderHistoryCell>()
{
if let Some(cell) = session_info_cell.take() {
self.transcript.active_cell = Some(cell);
}
true
} else {
self.transcript.active_cell = Some(active);
false
}
} else {
false
};
self.flush_active_cell();
if !merged_header && let Some(cell) = session_info_cell {
self.add_boxed_history(cell);
}
}
pub(crate) fn add_info_message(&mut self, message: String, hint: Option<String>) {
self.add_to_history(history_cell::new_info_event(message, hint));
self.request_redraw();
}
pub(crate) fn add_memories_enable_notice(&mut self) {
self.add_to_history(history_cell::new_warning_event(
MEMORIES_ENABLE_NOTICE.to_string(),
));
self.request_redraw();
}
pub(crate) fn add_plain_history_lines(&mut self, lines: Vec<Line<'static>>) {
self.add_boxed_history(Box::new(PlainHistoryCell::new(lines)));
self.request_redraw();
}
pub(crate) fn add_warning_message(&mut self, message: String) {
self.add_to_history(history_cell::new_warning_event(message));
self.request_redraw();
}
pub(crate) fn add_error_message(&mut self, message: String) {
self.add_to_history(history_cell::new_error_event(message));
self.request_redraw();
}
fn add_app_server_stub_message(&mut self, feature: &str) {
warn!(feature, "stubbed unsupported TUI feature");
self.add_error_message(format!("{feature}: {TUI_STUB_MESSAGE}"));
}
fn rename_confirmation_cell(name: &str, thread_id: Option<ThreadId>) -> PlainHistoryCell {
let mut line = vec![
"• ".into(),
"Session renamed to ".into(),
name.to_string().cyan(),
];
if let Some(hint) = resume_hint(Some(name), thread_id) {
line.extend([". To resume this session run ".into(), hint.cyan()]);
}
PlainHistoryCell::new(vec![line.into()])
}
pub(crate) fn add_mcp_output(&mut self, detail: McpServerStatusDetail) {
self.flush_answer_stream_with_separator();
self.flush_active_cell();
self.transcript.active_cell = Some(Box::new(history_cell::new_mcp_inventory_loading(
self.config.animations,
)));
self.bump_active_cell_revision();
self.request_redraw();
self.app_event_tx.send(AppEvent::FetchMcpInventory {
detail,
thread_id: self.thread_id(),
});
}
pub(crate) fn clear_mcp_inventory_loading(&mut self) {
let Some(active) = self.transcript.active_cell.as_ref() else {
return;
};
if !active
.as_any()
.is::<history_cell::McpInventoryLoadingCell>()
{
return;
}
self.transcript.active_cell = None;
self.bump_active_cell_revision();
self.request_redraw();
}
pub(crate) fn apply_file_search_result(&mut self, query: String, matches: Vec<FileMatch>) {
self.bottom_pane.on_file_search_result(query, matches);
}
fn current_stream_width(&self, reserved_cols: usize) -> Option<usize> {
self.last_rendered_width.get().and_then(|width| {
if width == 0 {
None
} else {
let width = u16::try_from(width).unwrap_or(u16::MAX);
let width = usize::from(self.history_wrap_width(width));
Some(crate::tui_internal::width::usable_content_width(width, reserved_cols).unwrap_or(1))
}
})
}
pub(crate) fn raw_output_mode(&self) -> bool {
self.raw_output_mode
}
pub(crate) fn history_render_mode(&self) -> HistoryRenderMode {
if self.raw_output_mode {
HistoryRenderMode::Raw
} else {
HistoryRenderMode::Rich
}
}
pub(crate) fn set_raw_output_mode(&mut self, enabled: bool) {
self.raw_output_mode = enabled;
self.config.tui_raw_output_mode = enabled;
let render_mode = self.history_render_mode();
if let Some(controller) = self.stream_controller.as_mut() {
controller.set_render_mode(render_mode);
}
if let Some(controller) = self.plan_stream_controller.as_mut() {
controller.set_render_mode(render_mode);
}
self.refresh_status_surfaces();
}
pub(crate) fn raw_output_mode_notice(enabled: bool) -> &'static str {
if enabled {
"Raw output mode on: transcript text is shown for clean terminal selection."
} else {
"Raw output mode off: rich transcript rendering restored."
}
}
pub(crate) fn set_raw_output_mode_and_notify(&mut self, enabled: bool) {
self.set_raw_output_mode(enabled);
self.add_info_message(
Self::raw_output_mode_notice(enabled).to_string(),
None,
);
}
pub(crate) fn toggle_raw_output_mode_and_notify(&mut self) -> bool {
let enabled = !self.raw_output_mode;
self.set_raw_output_mode_and_notify(enabled);
enabled
}
pub(crate) fn on_terminal_resize(&mut self, width: u16) {
let had_rendered_width = self.last_rendered_width.get().is_some();
self.last_rendered_width.set(Some(width as usize));
let stream_width = self.current_stream_width( 2);
let plan_stream_width = self.current_stream_width( 4);
if let Some(controller) = self.stream_controller.as_mut() {
controller.set_width(stream_width);
}
if let Some(controller) = self.plan_stream_controller.as_mut() {
controller.set_width(plan_stream_width);
}
self.sync_active_stream_tail();
if !had_rendered_width {
self.request_redraw();
}
}
pub(crate) fn has_active_agent_stream(&self) -> bool {
self.stream_controller.is_some()
}
pub(crate) fn has_active_plan_stream(&self) -> bool {
self.plan_stream_controller.is_some()
}
fn is_plan_streaming_in_tui(&self) -> bool {
self.plan_stream_controller.is_some()
}
pub(crate) fn composer_is_empty(&self) -> bool {
self.bottom_pane.composer_is_empty()
}
#[cfg(test)]
pub(crate) fn is_task_running_for_test(&self) -> bool {
self.bottom_pane.is_task_running()
}
pub(crate) fn toggle_vim_mode_and_notify(&mut self) {
let enabled = self.bottom_pane.toggle_vim_enabled();
let message = if enabled {
"Vim mode enabled."
} else {
"Vim mode disabled."
};
self.add_info_message(message.to_string(), None);
}
pub(crate) fn is_normal_backtrack_mode(&self) -> bool {
self.bottom_pane.is_normal_backtrack_mode()
}
pub(crate) fn should_handle_vim_insert_escape(&self, key_event: KeyEvent) -> bool {
self.bottom_pane
.composer_should_handle_vim_insert_escape(key_event)
}
pub(crate) fn insert_str(&mut self, text: &str) {
self.bottom_pane.insert_str(text);
}
pub(crate) fn set_remote_image_urls(&mut self, remote_image_urls: Vec<String>) {
self.bottom_pane.set_remote_image_urls(remote_image_urls);
}
fn take_remote_image_urls(&mut self) -> Vec<String> {
self.bottom_pane.take_remote_image_urls()
}
#[cfg(test)]
pub(crate) fn remote_image_urls(&self) -> Vec<String> {
self.bottom_pane.remote_image_urls()
}
#[cfg(test)]
pub(crate) fn pending_thread_approvals(&self) -> &[String] {
self.bottom_pane.pending_thread_approvals()
}
#[cfg(test)]
pub(crate) fn has_active_view(&self) -> bool {
self.bottom_pane.has_active_view()
}
pub(crate) fn show_esc_backtrack_hint(&mut self) {
self.bottom_pane.show_esc_backtrack_hint();
}
pub(crate) fn clear_esc_backtrack_hint(&mut self) {
self.bottom_pane.clear_esc_backtrack_hint();
}
fn refresh_skills_for_current_cwd(&mut self, force_reload: bool) {
self.submit_op(AppCommand::list_skills(
vec![self.config.cwd.to_path_buf()],
force_reload,
));
}
pub(crate) fn submit_op<T>(&mut self, op: T) -> bool
where
T: Into<AppCommand>,
{
let op: AppCommand = op.into();
if self.blocks_direct_input
&& matches!(
&op,
AppCommand::UserTurn { .. } | AppCommand::Review { .. } | AppCommand::Compact
)
{
self.add_error_message(PARENT_OWNED_INPUT_MESSAGE.to_string());
return false;
}
self.prepare_local_op_submission(&op);
if op.is_review() && !self.bottom_pane.is_task_running() {
self.bottom_pane.set_task_running( true);
}
match &self.codex_op_target {
CodexOpTarget::Direct(codex_op_tx) => {
crate::tui_internal::session_log::log_outbound_op(&op);
if let Err(e) = codex_op_tx.send(op) {
tracing::error!("failed to submit op: {e}");
return false;
}
}
CodexOpTarget::AppEvent => {
self.app_event_tx.send(AppEvent::CodexOp(op));
}
}
true
}
fn append_message_history_entry(&self, text: String) {
let Some(thread_id) = self.thread_id else {
tracing::warn!("failed to append to message history: no active thread id");
return;
};
self.app_event_tx
.send(AppEvent::AppendMessageHistoryEntry { thread_id, text });
}
pub(crate) fn prepare_local_op_submission(&mut self, op: &AppCommand) {
if matches!(op, AppCommand::Interrupt) && self.turn_lifecycle.agent_turn_running {
if let Some(controller) = self.stream_controller.as_mut() {
controller.clear_queue();
}
if let Some(controller) = self.plan_stream_controller.as_mut() {
controller.clear_queue();
}
self.clear_active_stream_tail();
self.request_redraw();
}
}
fn on_list_skills(&mut self, ev: SkillsListResponse) {
self.set_skills_from_response(&ev);
self.refresh_plugin_mentions();
}
pub(crate) fn refresh_plugin_mentions(&mut self) {
if !self.config.features.enabled(Feature::Plugins) {
self.bottom_pane.set_plugin_mentions( None);
return;
}
self.app_event_tx.send(AppEvent::RefreshPluginMentions);
}
pub(crate) fn on_plugin_mentions_loaded(
&mut self,
plugins: Option<Vec<PluginCapabilitySummary>>,
) {
if self.bottom_pane.plugins() == plugins.as_ref() {
return;
}
self.bottom_pane.set_plugin_mentions(plugins);
}
pub(crate) fn sync_plugin_mentions_config(&mut self, config: &Config) {
self.config.features = config.features.clone();
self.config.config_layer_stack = config.config_layer_stack.clone();
self.config.memories = config.memories.clone();
self.config.terminal_resize_reflow = config.terminal_resize_reflow;
self.sync_mentions_v2_enabled();
}
pub(crate) fn token_usage(&self) -> TokenUsage {
self.token_info
.as_ref()
.map(|ti| ti.total_token_usage.clone())
.unwrap_or_default()
}
pub(crate) fn thread_id(&self) -> Option<ThreadId> {
self.thread_id
}
pub(crate) fn thread_name(&self) -> Option<String> {
self.thread_name.clone()
}
pub(crate) fn rollout_path(&self) -> Option<PathBuf> {
self.current_rollout_path.clone()
}
pub(crate) fn active_cell_transcript_key(&self) -> Option<ActiveCellTranscriptKey> {
let cell = self.transcript.active_cell.as_ref();
let hook_cell = self.active_hook_cell.as_ref();
let token_activity_cell = self.pending_token_activity_output();
let rate_limit_reset_hint = self.pending_rate_limit_reset_hint();
if cell.is_none()
&& hook_cell.is_none()
&& token_activity_cell.is_none()
&& rate_limit_reset_hint.is_none()
{
return None;
}
Some(ActiveCellTranscriptKey {
revision: self.transcript.active_cell_revision,
is_stream_continuation: cell
.map(|cell| cell.is_stream_continuation())
.unwrap_or(false),
animation_tick: cell
.and_then(|cell| cell.transcript_animation_tick())
.or_else(|| {
hook_cell.and_then(super::history_cell::HistoryCell::transcript_animation_tick)
}),
})
}
pub(crate) fn active_cell_transcript_hyperlink_lines(
&self,
width: u16,
) -> Option<Vec<HyperlinkLine>> {
let mut lines = Vec::new();
if let Some(cell) = self.transcript.active_cell.as_ref() {
lines.extend(cell.transcript_hyperlink_lines(width));
}
if let Some(hook_cell) = self.active_hook_cell.as_ref() {
let hook_lines = hook_cell.transcript_hyperlink_lines(width);
if !hook_lines.is_empty() && !lines.is_empty() {
lines.push(HyperlinkLine::from(""));
}
lines.extend(hook_lines);
}
if let Some(token_activity_cell) = self.pending_token_activity_output() {
let token_activity_lines = token_activity_cell.transcript_hyperlink_lines(width);
if !token_activity_lines.is_empty() && !lines.is_empty() {
lines.push(HyperlinkLine::from(""));
}
lines.extend(token_activity_lines);
}
if let Some(rate_limit_reset_hint) = self.pending_rate_limit_reset_hint() {
let hint_lines = rate_limit_reset_hint.transcript_hyperlink_lines(width);
if !hint_lines.is_empty() && !lines.is_empty() {
lines.push(HyperlinkLine::from(""));
}
lines.extend(hint_lines);
}
(!lines.is_empty()).then_some(lines)
}
#[cfg(test)]
pub(crate) fn active_cell_transcript_lines(&self, width: u16) -> Option<Vec<Line<'static>>> {
self.active_cell_transcript_hyperlink_lines(width)
.map(crate::tui_internal::terminal_hyperlinks::visible_lines)
}
pub(crate) fn config_ref(&self) -> &Config {
&self.config
}
#[cfg(test)]
pub(crate) fn status_line_text(&self) -> Option<String> {
self.bottom_pane.status_line_text()
}
pub(crate) fn clear_token_usage(&mut self) {
self.token_info = None;
}
}
fn has_websocket_timing_metrics(summary: RuntimeMetricsSummary) -> bool {
summary.responses_api_overhead_ms > 0
|| summary.responses_api_inference_time_ms > 0
|| summary.responses_api_engine_iapi_ttft_ms > 0
|| summary.responses_api_engine_service_ttft_ms > 0
|| summary.responses_api_engine_iapi_tbt_ms > 0.0
|| summary.responses_api_engine_service_tbt_ms > 0.0
}
impl Drop for ChatWidget {
fn drop(&mut self) {
self.stop_rate_limit_poller();
}
}
const PLACEHOLDERS: [&str; 8] = [
"Explain this codebase",
"Summarize recent commits",
"Implement {feature}",
"Find and fix a bug in @filename",
"Write tests for @filename",
"Improve documentation in @filename",
"Run /review on my current changes",
"Use /skills to list available skills",
];
const SIDE_PLACEHOLDERS: [&str; 3] = [
"Check recently modified functions for compatibility",
"How many files have been modified?",
"Will this algorithm scale well?",
];
fn extract_first_bold(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut i = 0usize;
while i + 1 < bytes.len() {
if bytes[i] == b'*' && bytes[i + 1] == b'*' {
let start = i + 2;
let mut j = start;
while j + 1 < bytes.len() {
if bytes[j] == b'*' && bytes[j + 1] == b'*' {
let inner = &s[start..j];
let trimmed = inner.trim();
if !trimmed.is_empty() {
return Some(trimmed.to_string());
} else {
return None;
}
}
j += 1;
}
return None;
}
i += 1;
}
None
}
#[cfg(test)]
pub(crate) mod tests;