mod fast_mode;
mod transcript_cache;
pub(crate) use fast_mode::FastDisplayState;
pub(crate) use fast_mode::StoredFastObservation;
mod activity_motion;
pub(crate) use super::prompt_editor::{
PROMPT_MOVEMENT_FAILED_STATUS, PROMPT_TOO_LARGE_STATUS, PromptEditResult, PromptEditor,
PromptMovementOutcome, PromptTextNormalizer, normalize_prompt_text_bounded,
};
use super::{
activity::{self, ActivityNode},
autocomplete,
selection::{ActiveSelection, TextPosition},
toast::{COPIED_TO_CLIPBOARD, ToastSeverity, ToastState},
transcript,
};
use crate::output::{
ActivityId, ActivityKind, ActivityMetadata, ActivityStatus, ContextUsageSource, OutputEvent,
StreamingRedactor, redact_sensitive_text,
};
use crate::rendering::DisplayLine;
use crate::tui::{
PADDED_INLINE_SEPARATOR, layout::TuiPane, normalize_inline_separators,
theme::MissionControlTheme,
};
use chrono::{DateTime, Local};
use ratatui::layout::Position;
use std::cell::{Cell, Ref, RefCell};
use std::collections::{HashMap, HashSet, VecDeque};
use std::time::Instant;
pub(crate) use super::activity::activity_status_mark;
pub(crate) use super::transcript::{TranscriptEntries, TranscriptRenderCache};
pub(crate) use super::transcript_cards::TranscriptActivityLink;
pub(crate) const MOUSE_WHEEL_SCROLL_ROWS: u16 = 3;
pub(crate) use super::autocomplete::{AutocompleteCandidate, AutocompleteKind, AutocompleteState};
mod activity_state;
mod layout;
mod modal;
mod output_events;
mod primary_agent_state;
mod prompt_actions;
pub(in crate::tui) mod prompt_state;
mod provider_context;
mod subagent_viewer;
mod transcript_state;
use activity_state::ActiveSubagentTranscript;
pub(crate) use activity_state::ActiveToolTranscript;
use modal::ModalState;
pub(crate) use modal::{
ConnectProviderField, ConnectProviderStage, ConnectProviderState, LoginProviderEntry,
McpServerToggleRow, ModalKind, OAuthControl, PreviewMessage, SessionPickerFocus,
SessionPickerRow, SessionPreview, SessionPreviewPartial, SessionPreviewState, SkillToggleRow,
SubagentProfileToggleRow, SystemPromptCard, SystemPromptModalState, ThemeSaveState,
ToolToggleRow,
};
pub(crate) use primary_agent_state::PrimaryAgentEntry;
pub(crate) use prompt_state::RunningPromptState;
pub(crate) use transcript_state::ContextUsageState;
use transcript_state::{
is_transient_status_error, merge_context_usage, transcript_tool_metadata, transcript_tool_row,
};
pub(crate) const MAX_AUTOCOMPLETE_VISIBLE_ROWS: usize = 6;
pub(crate) mod session_files;
mod summary;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub(crate) enum RightColumnTab {
#[default]
Activity,
Diff,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum TuiFocusPane {
Transcript,
#[default]
Prompt,
}
#[derive(Debug, Default)]
pub(crate) struct ActivityRenderCache {
visible_nodes: Option<Vec<(usize, ActivityId)>>,
selected_detail: Option<ActivityDetailCache>,
}
#[derive(Debug, Clone)]
struct ActivityBodyWrapCache {
total_rows: usize,
visual_starts: Vec<(usize, usize)>,
}
#[derive(Debug, Clone)]
struct ActivityDetailCache {
id: Option<ActivityId>,
summary_lines: Vec<DisplayLine>,
body_lines: Vec<DisplayLine>,
metadata_header: Option<usize>,
body_wrap_cache: HashMap<u16, ActivityBodyWrapCache>,
body_wrap_widths: VecDeque<u16>,
}
impl ActivityRenderCache {
fn clear(&mut self) {
self.visible_nodes = None;
self.selected_detail = None;
}
fn invalidate_visible_nodes(&mut self) {
self.visible_nodes = None;
}
fn invalidate_selected_detail(&mut self) {
self.selected_detail = None;
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct ThinkingTranscriptIdentity {
pub(crate) item_id: String,
pub(crate) turn_id: Option<String>,
}
#[derive(Debug, Default)]
pub(crate) struct ScrollViewStates {
pub(crate) transcript: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) activity_tree: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) detail: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) help: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) system_prompt: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) session_list: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) session_preview: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) connect_provider: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) logout_picker: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) model_picker: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) skills_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) mcp_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) tools_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) subagents_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) rewind_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) usage_modal: RefCell<tui_scrollview::ScrollViewState>,
pub(crate) theme_modal: RefCell<tui_scrollview::ScrollViewState>,
}
#[derive(Debug)]
pub(crate) struct WelcomeRenderCache {
pub(crate) key: (MissionControlTheme, u16, u16),
pub(crate) update_version: Option<String>,
pub(crate) update_notice: Option<ratatui::widgets::Paragraph<'static>>,
pub(crate) update_height: u16,
pub(crate) compact: bool,
pub(crate) logo_phase: Option<usize>,
pub(crate) logo: ratatui::widgets::Paragraph<'static>,
pub(crate) body: ratatui::widgets::Paragraph<'static>,
pub(crate) height: u16,
pub(crate) logo_height: u16,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct SubagentCardSelection {
pub(crate) task_id: Option<ActivityId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LastSessionHeader {
pub(crate) session_id: Option<String>,
pub(crate) model: String,
}
#[derive(Debug, Default)]
pub(crate) struct MissionControlState {
pub(crate) conversation_modal: Option<Box<super::render::TranscriptModal>>,
pub(crate) subagent_controls: crate::subagents::control::SubagentControls,
pub(crate) subagent_control_namespace: String,
pub(crate) subagent_prompt_drafts: HashMap<ActivityId, subagent_viewer::SubagentPromptDraft>,
pub(crate) transcript_only_layout: bool,
pub(crate) right_column_tab: RightColumnTab,
pub(crate) summary: summary::SummaryDisplay,
pub(crate) session_files: session_files::SessionFiles,
pub(crate) diff: super::diff::DiffPage,
pub(crate) nodes: HashMap<ActivityId, ActivityNode>,
pub(crate) roots: Vec<ActivityId>,
pub(crate) children: HashMap<ActivityId, Vec<ActivityId>>,
pub(crate) expanded: HashSet<ActivityId>,
pub(crate) transcript: TranscriptEntries,
pub(crate) images: RefCell<super::images::TranscriptImages>,
pub(crate) transcript_timestamp_context: Option<DateTime<Local>>,
pub(crate) subagent_card_selection: HashMap<ActivityId, SubagentCardSelection>,
pub(crate) activity_update_sequence: u64,
pub(crate) subagent_card_rows: usize,
pub(super) transcript_cache: RefCell<TranscriptRenderCache>,
pub(super) subagent_viewer_timeline_cache:
RefCell<subagent_viewer::SubagentViewerTimelineCache>,
pub(super) final_assistant_plain_first_entries: RefCell<HashSet<usize>>,
pub(super) final_assistant_rich_refresh_entries: RefCell<HashSet<usize>>,
pub(super) final_assistant_plain_first_enabled: Cell<bool>,
pub(super) transcript_visual_generation: Cell<u64>,
pub(crate) theme: MissionControlTheme,
pub(crate) reduced_motion: bool,
#[cfg(any(test, debug_assertions))]
pub(crate) perf: super::perf::TuiPerfCounters,
pub(super) activity_render_cache: RefCell<ActivityRenderCache>,
pub(crate) scroll_views: ScrollViewStates,
pub(crate) detail_scroll_offset: Cell<usize>,
pub(crate) detail_metadata_expanded: bool,
pub(crate) transcript_tail_distance: Cell<usize>,
pub(crate) transcript_scroll_anchor: Cell<Option<transcript::TranscriptScrollAnchor>>,
pub(crate) active_tool_transcripts: HashMap<ActivityId, ActiveToolTranscript>,
pub(crate) assistant_streaming: bool,
pub(crate) active_assistant_text: String,
pub(crate) active_assistant_redactor: StreamingRedactor,
pub(crate) active_assistant_transcript_index: Option<usize>,
pub(crate) pending_assistant_completion_index: Option<usize>,
pub(crate) pending_compaction_transcript_index: Option<usize>,
pub(crate) active_thinking_text: String,
pub(crate) thinking_transcript_identities: HashMap<ThinkingTranscriptIdentity, usize>,
#[cfg(test)]
pub(crate) thinking_append_work_bytes: usize,
pub(crate) active_thinking_redactor: StreamingRedactor,
pub(crate) active_thinking_control_sanitizer: transcript::StreamingControlSanitizer,
pub(crate) active_thinking_transcript_index: Option<usize>,
pub(crate) context_usage: Option<ContextUsageState>,
pub(crate) session_usage: super::session_usage::SessionUsageLedger,
pub(crate) session_active_time: crate::sessions::active_time::SessionActiveTime,
pub(crate) displayed_session_active_seconds: u64,
pub(crate) last_known_session_cache_percent: Option<u64>,
pub(crate) compaction_settings: crate::config::CompactionSettings,
pub(crate) selected: usize,
pub(crate) prompt_editor: PromptEditor,
pub(crate) prompt_history: crate::tui::prompt_history::PromptHistory,
pub(crate) prompt_history_loaded: bool,
pub(crate) dismissed_prompt_history_revision: Option<u64>,
pub(crate) visible_prompt_history_revision: Cell<Option<u64>>,
pub(crate) focus_pane: TuiFocusPane,
pub(crate) last_standalone_ctrl: Option<Instant>,
pub(crate) fast_observations: HashMap<(String, String), StoredFastObservation>,
pub(crate) fast_warning_keys: HashSet<crate::fast::FastWarningKey>,
pub(crate) fast_observation_accept_sequence: u64,
pub(crate) fast_mode_enabled: bool,
pub(crate) fast_mode_request_state: crate::fast::FastRequestState,
pub(crate) fast_mode_capable: bool,
pub(crate) fast_mode_effective: bool,
pub(crate) color_enabled: bool,
pub(crate) fast_mode_rainbow_phase: usize,
pub(crate) welcome_animation_phase: usize,
pub(crate) welcome_render_cache: RefCell<Option<WelcomeRenderCache>>,
pub(crate) release_notes: Option<&'static str>,
pub(crate) update_restart_welcome: bool,
pub(crate) release_notes_drawn: std::cell::Cell<bool>,
pub(crate) release_notes_displayed: std::cell::Cell<bool>,
pub(crate) release_notes_viewport: std::cell::Cell<(u16, u16)>,
pub(crate) release_notes_area: std::cell::Cell<ratatui::layout::Rect>,
pub(crate) release_notes_offset: std::cell::Cell<usize>,
pub(crate) release_notes_page_end: std::cell::Cell<usize>,
pub(crate) release_notes_viewed: std::cell::Cell<usize>,
pub(crate) release_notes_total: std::cell::Cell<usize>,
pub(crate) available_update_version: Option<String>,
pub(crate) activity_motion: activity_motion::ActivityMotion,
pub(crate) ignore_next_standalone_ctrl_release: bool,
pub(crate) activity_follow_tail: bool,
pub(crate) status: String,
pub(crate) footer_session: String,
pub(crate) footer_session_id: Option<String>,
pub(crate) footer_cwd: String,
pub(crate) footer_git_branch: Option<String>,
pub(crate) provider: String,
pub(crate) model: String,
pub(crate) last_session_header: Option<LastSessionHeader>,
pub(crate) provider_ready: bool,
pub(crate) thinking_level: crate::thinking::ThinkingLevel,
pub(crate) effective_thinking_level: crate::thinking::ThinkingLevel,
pub(crate) thinking_levels: Vec<crate::thinking::ThinkingLevel>,
pub(crate) running_prompt: Option<RunningPromptState>,
pub(crate) active_worker_id: Option<u64>,
pub(crate) last_run_finished_worker_id: Option<u64>,
pub(crate) pending_steering_count: usize,
pub(crate) show_help: bool,
pub(crate) autocomplete: Option<AutocompleteState>,
pub(crate) autocomplete_dismissed_token: Option<String>,
pub(crate) selection: Option<ActiveSelection>,
pub(crate) toast: Option<ToastState>,
pub(crate) pending_model_catalog_request_id: Option<u64>,
pub(crate) pending_usage_request_id: Option<u64>,
pub(crate) codex_quota_label: Option<String>,
pub(crate) claude_quota_label: Option<String>,
pub(crate) last_agent_activity: Option<std::time::Instant>,
pub(crate) pending_startup_critical_request_id: Option<u64>,
pub(crate) pending_startup_decorative_request_id: Option<u64>,
pub(crate) startup_prompt_queued: bool,
pub(crate) modals: ModalState,
pub(crate) primary_agents: Vec<PrimaryAgentEntry>,
pub(crate) selected_primary_agent: Option<usize>,
}
impl MissionControlState {
pub(crate) fn subagent_card_rows(&self) -> usize {
if self.subagent_card_rows == 0 {
crate::config::DEFAULT_TUI_SUBAGENT_CARD_ROWS
} else {
self.subagent_card_rows
}
}
}
impl MissionControlState {
pub(crate) fn pin_subagent_card_task(&mut self, batch_id: ActivityId, task_id: ActivityId) {
self.subagent_card_selection.insert(
batch_id,
SubagentCardSelection {
task_id: Some(task_id),
},
);
self.invalidate_transcript_cache();
}
pub(crate) fn scroll_offset(
&self,
view: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
) -> usize {
usize::from(view.borrow().offset().y)
}
pub(crate) fn set_scroll_offset(
&self,
view: &std::cell::RefCell<tui_scrollview::ScrollViewState>,
offset: usize,
) {
if std::ptr::eq(view, &self.scroll_views.detail) {
self.detail_scroll_offset.set(offset);
return;
}
if std::ptr::eq(view, &self.scroll_views.transcript) {
self.transcript_tail_distance.set(offset);
if offset == 0 {
self.transcript_scroll_anchor.set(None);
}
}
let offset = offset.min(usize::from(u16::MAX));
view.borrow_mut()
.set_offset(Position::new(0, offset as u16));
}
pub(crate) fn transcript_scroll_offset(&self) -> usize {
self.transcript_tail_distance.get()
}
pub(crate) fn activity_scroll_offset(&self) -> usize {
self.scroll_offset(&self.scroll_views.activity_tree)
}
pub(crate) fn detail_offset(&self) -> usize {
self.detail_scroll_offset.get()
}
pub(crate) fn help_offset(&self) -> usize {
self.scroll_offset(&self.scroll_views.help)
}
pub(crate) fn reset_core_scroll_views(&mut self) {
self.set_scroll_offset(&self.scroll_views.transcript, 0);
self.set_scroll_offset(&self.scroll_views.activity_tree, 0);
self.detail_scroll_offset.set(0);
self.set_scroll_offset(&self.scroll_views.help, 0);
}
pub(crate) fn refresh_thinking_levels(
&mut self,
selected: crate::thinking::ThinkingLevel,
available: Vec<crate::thinking::ThinkingLevel>,
) {
self.thinking_level = selected;
self.thinking_levels = crate::thinking::normalize_thinking_levels(&available);
self.effective_thinking_level =
crate::thinking::resolve_thinking_level(&self.thinking_levels, selected);
}
pub(crate) fn set_theme(&mut self, theme: MissionControlTheme, reduced_motion: bool) -> bool {
let presentation_changed = !self.theme.presentation_eq(theme);
let newer_revision = theme.revision() > self.theme.revision();
let motion_changed = self.reduced_motion != reduced_motion;
if !presentation_changed && !newer_revision && !motion_changed {
return false;
}
self.theme = theme;
self.reduced_motion = reduced_motion;
self.fast_mode_rainbow_phase = 0;
self.invalidate_transcript_cache();
true
}
pub(crate) fn theme_revision(&self) -> u64 {
self.theme.revision()
}
pub(crate) fn welcome_animation_active(&self) -> bool {
!self.transcript_only_layout
&& self.transcript.is_empty()
&& self.color_enabled
&& !self.reduced_motion
}
pub(crate) fn next_thinking_level(&self) -> Option<crate::thinking::ThinkingLevel> {
crate::thinking::next_thinking_level(&self.thinking_levels, self.thinking_level)
}
pub(crate) fn thinking_label(&self) -> &'static str {
self.effective_thinking_level.label()
}
pub(crate) fn refresh_compaction_settings(&mut self, settings: &crate::config::Settings) {
self.compaction_settings = settings.compaction.clone();
if self.provider == crate::providers::CLAUDE_SUBSCRIPTION_PROVIDER {
let mut budget = settings.context.clone().unwrap_or_default();
budget.apply_model_limits(&self.provider, &self.model);
settings
.compaction
.apply_conversation_limit(&mut budget, &self.provider);
self.update_context_window(budget.max_tokens);
}
}
pub(crate) fn update_context_window(&mut self, max_tokens: usize) {
let current_tokens = self
.context_usage
.map(|usage| usage.current_tokens)
.unwrap_or(0);
let reasoning_tokens = self.context_usage.and_then(|usage| usage.reasoning_tokens);
let source = self
.context_usage
.map(|usage| usage.source)
.unwrap_or(ContextUsageSource::FallbackEstimate);
let request_sequence = self
.context_usage
.map(|usage| usage.request_sequence)
.unwrap_or(0);
self.context_usage = Some(ContextUsageState {
current_tokens,
max_tokens,
reasoning_tokens,
source,
request_sequence,
});
}
pub(crate) fn reset_context_for_model_switch(&mut self) {
self.context_usage = None;
}
pub(crate) fn invalidate_activity_render_cache(&self) {
self.activity_render_cache.borrow_mut().clear();
}
pub(crate) fn invalidate_activity_visible_nodes_cache(&self) {
self.activity_render_cache
.borrow_mut()
.invalidate_visible_nodes();
}
pub(crate) fn invalidate_selected_activity_detail_cache(&self) {
self.activity_render_cache
.borrow_mut()
.invalidate_selected_detail();
}
pub(crate) fn selected_activity_summary_lines(&self) -> Vec<DisplayLine> {
self.ensure_selected_activity_detail_cache();
self.activity_render_cache
.borrow()
.selected_detail
.as_ref()
.map(|detail| detail.summary_lines.clone())
.unwrap_or_else(|| vec![DisplayLine::plain(no_activity_selected_text())])
}
fn ensure_selected_activity_detail_cache(&self) {
let selected_id = self
.modals
.activity_detail
.clone()
.or_else(|| self.selected_activity_id());
if self
.activity_render_cache
.borrow()
.selected_detail
.as_ref()
.is_some_and(|detail| detail.id == selected_id)
{
return;
}
let lines = selected_id
.as_ref()
.and_then(|id| self.nodes.get(id))
.map(|node| {
let mut detail =
activity::activity_detail_lines(node, self.detail_metadata_expanded);
if node.kind == ActivityKind::SubagentTask {
let descendants = self.safe_subagent_descendant_lines(&node.id);
if !descendants.is_empty() {
if !detail.body.is_empty() {
detail.body.push(DisplayLine::plain(""));
}
detail.body.push(DisplayLine::plain("Recent activity"));
detail.body.extend(descendants);
}
}
detail
})
.unwrap_or_else(|| activity::ActivityDetailLines {
summary: vec![DisplayLine::plain(no_activity_selected_text())],
body: Vec::new(),
metadata_header: None,
});
self.activity_render_cache.borrow_mut().selected_detail = Some(ActivityDetailCache {
id: selected_id,
summary_lines: lines.summary,
body_lines: lines.body,
metadata_header: lines.metadata_header,
body_wrap_cache: HashMap::new(),
body_wrap_widths: VecDeque::new(),
});
}
fn safe_subagent_descendant_lines(&self, task_id: &ActivityId) -> Vec<DisplayLine> {
let mut nodes = self
.nodes
.values()
.filter(|node| self.activity_is_descendant_of(&node.id, task_id))
.collect::<Vec<_>>();
nodes.sort_by(|left, right| {
right
.own_update_sequence
.cmp(&left.own_update_sequence)
.then_with(|| right.id.as_str().cmp(left.id.as_str()))
});
let mut lines = Vec::new();
for node in nodes {
let source = if node.preview.trim().is_empty() {
node.metadata.label.as_str()
} else {
node.preview.as_str()
};
let Some(line) = source.lines().rev().find(|line| !line.trim().is_empty()) else {
continue;
};
let line = normalize_inline_separators(&transcript::sanitize_preview(
&redact_sensitive_text(line),
));
if !line.is_empty()
&& lines
.last()
.is_none_or(|last: &DisplayLine| last.plain_text() != line)
{
lines.push(DisplayLine::plain(line));
}
if lines.len() == 6 {
break;
}
}
lines
}
pub(crate) fn toggle_detail_metadata(&mut self, visible_rows: u16, wrap_width: u16) -> bool {
self.ensure_selected_activity_detail_cache();
if self
.activity_render_cache
.borrow()
.selected_detail
.as_ref()
.is_none_or(|detail| detail.metadata_header.is_none())
{
return false;
}
let offset = self.detail_offset();
self.detail_metadata_expanded = !self.detail_metadata_expanded;
self.invalidate_selected_activity_detail_cache();
let body_rows = self.detail_body_viewport_rows(visible_rows, wrap_width);
let clamped = offset.min(self.detail_overflow(body_rows, wrap_width));
self.set_scroll_offset(&self.scroll_views.detail, clamped);
true
}
pub(crate) fn detail_metadata_header_visual_rows(
&self,
wrap_width: u16,
) -> Option<std::ops::Range<usize>> {
self.ensure_selected_activity_body_wrap_cache(wrap_width);
let cache = self.activity_render_cache.borrow();
let detail = cache.selected_detail.as_ref()?;
let header = detail.metadata_header?;
let starts = &detail
.body_wrap_cache
.get(&wrap_width.max(1))?
.visual_starts;
let start = starts.iter().position(|(line, _)| *line == header)?;
let end = starts[start..]
.iter()
.position(|(line, _)| *line != header)
.map_or(starts.len(), |offset| start + offset);
Some(start..end)
}
pub(crate) fn start_prompt_selection_at(&mut self, byte: usize) {
self.selection = None;
if self.prompt_editor.start_selection_at(byte) == PromptMovementOutcome::Failed {
self.status = PROMPT_MOVEMENT_FAILED_STATUS.to_string();
}
}
pub(crate) fn update_prompt_selection_at(&mut self, byte: usize) -> bool {
let outcome = self.prompt_editor.update_selection_at(byte);
if outcome == PromptMovementOutcome::Failed {
self.status = PROMPT_MOVEMENT_FAILED_STATUS.to_string();
}
!matches!(outcome, PromptMovementOutcome::Unchanged)
}
pub(crate) fn start_selection(&mut self, pane: TuiPane, position: TextPosition) {
match pane {
TuiPane::Prompt => self.start_prompt_selection_at(position.byte),
TuiPane::Transcript => {
self.prompt_editor.cancel_selection();
self.selection = Some(ActiveSelection::new(pane, position));
}
_ => {
self.clear_selection();
}
}
}
pub(crate) fn update_selection(&mut self, pane: TuiPane, position: TextPosition) -> bool {
match pane {
TuiPane::Prompt => self.update_prompt_selection_at(position.byte),
TuiPane::Transcript => {
let Some(selection) = &mut self.selection else {
return false;
};
if selection.pane != pane {
self.selection = None;
return true;
}
let before = selection.current;
selection.set_current(position);
before != position
}
_ => false,
}
}
pub(crate) fn finish_selection(
&mut self,
pane: TuiPane,
transcript_wrap_width: u16,
) -> Option<String> {
match pane {
TuiPane::Prompt => self.prompt_editor.finish_selection(),
TuiPane::Transcript => {
let selection = self.selection.take()?;
if selection.pane != pane {
return None;
}
let (start, end) = selection.byte_range()?;
super::transcript_cards::visual_text_for_width(self, transcript_wrap_width.max(1))
.get(start..end)
.map(ToOwned::to_owned)
.filter(|text| !text.is_empty())
}
_ => None,
}
}
pub(crate) fn clear_selection(&mut self) -> bool {
self.prompt_editor.cancel_selection() || self.selection.take().is_some()
}
pub(crate) fn transcript_text_position_for_cell(
&self,
row: u16,
column: u16,
visible_rows: u16,
wrap_width: u16,
) -> Option<TextPosition> {
let render_scroll = self.transcript_render_scroll(visible_rows, wrap_width);
super::transcript_cards::text_position_for_visual_cell(
self,
render_scroll.saturating_add(usize::from(row)),
usize::from(column),
wrap_width,
)
.map(|position| TextPosition::new(position.byte, usize::from(row), position.col))
}
pub(crate) fn show_copy_success_toast(&mut self, now: Instant) {
self.toast = Some(ToastState::new(
COPIED_TO_CLIPBOARD,
ToastSeverity::Success,
now,
));
}
pub(crate) fn show_copy_failure_toast(&mut self, now: Instant) {
self.toast = Some(ToastState::new(
"Clipboard copy failed",
ToastSeverity::Error,
now,
));
}
pub(crate) fn set_pending_steering_count(&mut self, count: usize) -> bool {
let changed = self.pending_steering_count != count;
self.pending_steering_count = count;
changed
}
pub(crate) fn show_steering_injected_feedback(&mut self, now: Instant) {
self.status = "Steering injected".to_string();
self.toast = Some(ToastState::new(
"Steering injected",
ToastSeverity::Success,
now,
));
}
pub(crate) fn expire_toast(&mut self, now: Instant) -> bool {
if self.toast.as_ref().is_some_and(|toast| toast.expired(now)) {
self.toast = None;
true
} else {
false
}
}
}
fn no_activity_selected_text() -> &'static str {
"No activity selected."
}
fn short_session_id(session_id: &str) -> String {
let char_count = session_id.chars().count();
if char_count <= 8 {
return session_id.to_string();
}
let prefix: String = session_id.chars().take(4).collect();
let suffix: String = session_id
.chars()
.rev()
.take(4)
.collect::<Vec<_>>()
.into_iter()
.rev()
.collect();
format!("{prefix}...{suffix}")
}
#[cfg(test)]
mod profiling_support;