use crate::event::AppEvent;
use crate::widgets::{TodoDisplayItem, TodoDisplayStatus};
use super::{App, DisplayRole};
pub(super) fn map_todo_items(mgr: &opendev_runtime::TodoManager) -> Vec<TodoDisplayItem> {
mgr.all()
.iter()
.map(|item| TodoDisplayItem {
id: item.id,
title: item.title.clone(),
status: match item.status {
opendev_runtime::TodoStatus::Pending => TodoDisplayStatus::Pending,
opendev_runtime::TodoStatus::InProgress => TodoDisplayStatus::InProgress,
opendev_runtime::TodoStatus::Completed => TodoDisplayStatus::Completed,
},
active_form: if item.active_form.is_empty() {
None
} else {
Some(item.active_form.clone())
},
})
.collect()
}
impl App {
pub(super) fn sync_todo_display(&mut self) {
if let Some(ref mgr) = self.state.todo_manager
&& let Ok(mgr) = mgr.lock()
{
self.state.todo_items = map_todo_items(&mgr);
}
}
pub(super) fn finalize_active_thinking(&mut self) {
if let Some(msg) = self
.state
.messages
.iter_mut()
.rev()
.find(|m| m.role == DisplayRole::Reasoning && m.thinking_duration_secs.is_none())
{
if let Some(started) = msg.thinking_started_at {
msg.thinking_duration_secs = Some(started.elapsed().as_secs());
} else {
msg.thinking_duration_secs = Some(0);
}
self.state.message_generation += 1;
}
}
pub(super) fn drain_next_pending(&mut self) {
if self.state.pending_queue.is_empty() {
return;
}
match self.state.pending_queue.front() {
Some(super::PendingItem::UserMessage(_)) => {
if let Some(super::PendingItem::UserMessage(msg)) =
self.state.pending_queue.pop_front()
{
self.message_controller
.handle_user_submit(&mut self.state, &msg);
self.state.message_generation += 1;
self.state.agent_active = true;
let _ = self.event_tx.send(AppEvent::UserSubmit(msg));
self.state.dirty = true;
}
}
Some(super::PendingItem::BackgroundResult { .. }) => {
if let Some(super::PendingItem::BackgroundResult {
task_id,
query,
result,
tool_call_count,
..
}) = self.state.pending_queue.pop_front()
{
let msg = format!(
"[Background task [{task_id}] completed ({tool_call_count} tools)]\n\
Task: {query}\n\n\
{result}"
);
self.message_controller
.handle_user_submit(&mut self.state, &msg);
self.state.agent_active = true;
self.state.message_generation += 1;
let _ = self.event_tx.send(AppEvent::UserSubmit(msg));
self.state.dirty = true;
}
}
None => {}
}
}
pub(super) fn handle_event(&mut self, event: AppEvent) {
let is_user_event = matches!(
event,
AppEvent::Key(_)
| AppEvent::ScrollUp
| AppEvent::ScrollDown
| AppEvent::MouseDown { .. }
| AppEvent::MouseDrag { .. }
| AppEvent::MouseUp { .. }
);
if is_user_event {
if let Some(last) = self.state.last_event_time
&& last.elapsed() > std::time::Duration::from_secs(1)
{
self.state.force_clear = true;
}
self.state.last_event_time = Some(std::time::Instant::now());
}
match event {
AppEvent::Key(key) => {
if self.state.selection.range.is_some() {
self.state.selection.clear();
}
self.handle_key(key);
self.state.dirty = true;
}
AppEvent::Resize(_, _) => {
self.state.selection.clear();
self.state.dirty = true;
}
AppEvent::FocusGained => {
self.state.force_clear = true;
self.state.dirty = true;
}
AppEvent::ScrollUp => {
let amount = self.accelerated_scroll(true);
self.state.scroll_offset = self.state.scroll_offset.saturating_add(amount);
self.state.user_scrolled = true;
self.state.dirty = true;
}
AppEvent::ScrollDown => {
if self.state.scroll_offset > 0 {
let amount = self.accelerated_scroll(false);
self.state.scroll_offset = self.state.scroll_offset.saturating_sub(amount);
} else {
self.state.user_scrolled = false;
}
self.state.dirty = true;
}
AppEvent::MouseDown { col, row } => {
self.handle_mouse_down(col, row);
self.state.dirty = true;
}
AppEvent::MouseDrag { col, row } => {
self.handle_mouse_drag(col, row);
self.state.dirty = true;
}
AppEvent::MouseUp { col, row } => {
self.handle_mouse_up(col, row);
self.state.dirty = true;
}
AppEvent::Tick => {
self.handle_tick();
if self.state.agent_active
|| !self.state.active_tools.is_empty()
|| !self.state.active_subagents.is_empty()
|| self.state.task_progress.is_some()
|| !self.state.welcome_panel.fade_complete
|| self.state.task_watcher_open
|| self.state.background_task_count > 0
|| self.state.last_task_completion.is_some()
|| self.state.backgrounded_task_info.is_some()
|| !self.state.toasts.is_empty()
|| self.state.leader_pending
|| self.state.selection.active
{
self.state.dirty = true;
}
}
AppEvent::BudgetExhausted {
cost_usd,
budget_usd,
} => self.handle_budget_exhausted(cost_usd, budget_usd),
AppEvent::FileChangeSummary {
files,
additions,
deletions,
} => self.handle_file_change_summary(files, additions, deletions),
AppEvent::ContextUsage(pct) => self.handle_context_usage(pct),
AppEvent::AgentStarted => self.handle_agent_started(),
AppEvent::AgentChunk(text) => self.handle_agent_chunk(text),
AppEvent::AgentMessage(msg) => self.handle_agent_message(msg),
AppEvent::AgentFinished => self.handle_agent_finished(),
AppEvent::AgentError(err) => self.handle_agent_error(err),
AppEvent::ReasoningBlockStart => self.handle_reasoning_block_start(),
AppEvent::ReasoningContent(content) => self.handle_reasoning_content(content),
AppEvent::ToolStarted {
tool_id,
tool_name,
args,
} => self.handle_tool_started(tool_id, tool_name, args),
AppEvent::ToolOutput { tool_id, output } => self.handle_tool_output(tool_id, output),
AppEvent::ToolResult {
tool_id,
tool_name,
output,
success,
args: result_args,
} => self.handle_tool_result(tool_id, tool_name, output, success, result_args),
AppEvent::ToolFinished { tool_id, success } => {
self.handle_tool_finished(tool_id, success)
}
AppEvent::ToolApprovalRequired {
tool_id: _,
tool_name: _,
description,
} => self.handle_tool_approval_required(description),
AppEvent::ToolApprovalRequested {
command,
working_dir,
response_tx,
} => self.handle_tool_approval_requested(command, working_dir, response_tx),
AppEvent::AskUserRequested {
question,
options,
default,
response_tx,
} => self.handle_ask_user_requested(question, options, default, response_tx),
AppEvent::SubagentStarted {
subagent_id,
subagent_name,
task,
cancel_token,
} => self.handle_subagent_started(subagent_id, subagent_name, task, cancel_token),
AppEvent::SubagentToolCall {
subagent_id,
tool_name,
tool_id,
args,
..
} => self.handle_subagent_tool_call(subagent_id, tool_name, tool_id, args),
AppEvent::SubagentToolComplete {
subagent_id,
tool_name,
tool_id,
success,
..
} => self.handle_subagent_tool_complete(subagent_id, tool_name, tool_id, success),
AppEvent::SubagentFinished {
subagent_id,
success,
result_summary,
tool_call_count,
shallow_warning,
..
} => self.handle_subagent_finished(
subagent_id,
success,
result_summary,
tool_call_count,
shallow_warning,
),
AppEvent::SubagentTokenUpdate {
subagent_id,
input_tokens,
output_tokens,
..
} => self.handle_subagent_token_update(subagent_id, input_tokens, output_tokens),
AppEvent::TaskProgressStarted { description } => {
self.handle_task_progress_started(description)
}
AppEvent::TaskProgressFinished => self.handle_task_progress_finished(),
AppEvent::PlanApprovalRequested {
plan_content,
response_tx,
} => self.handle_plan_approval_requested(plan_content, response_tx),
AppEvent::UserSubmit(ref msg) => self.handle_user_submit(msg),
AppEvent::Interrupt => self.handle_interrupt(),
AppEvent::SetInterruptToken(token) => self.handle_set_interrupt_token(token),
AppEvent::AgentInterrupted => self.handle_agent_interrupted(),
AppEvent::ModeChanged(mode) => self.handle_mode_changed(mode),
AppEvent::KillTask(id) => self.handle_kill_task(id),
AppEvent::CompactionStarted => self.handle_compaction_started(),
AppEvent::CompactionFinished { success, message } => {
self.handle_compaction_finished(success, message)
}
AppEvent::AgentBackgrounded {
task_id,
query_summary: _,
} => self.handle_agent_backgrounded(task_id),
AppEvent::BackgroundNudge { content } => self.handle_background_nudge(content),
AppEvent::BackgroundAgentCompleted {
task_id,
success,
result_summary,
full_result,
cost_usd,
tool_call_count,
} => self.handle_background_agent_completed(
task_id,
success,
result_summary,
full_result,
cost_usd,
tool_call_count,
),
AppEvent::BackgroundAgentProgress {
task_id,
tool_name,
tool_count,
} => self.handle_background_agent_progress(task_id, tool_name, tool_count),
AppEvent::BackgroundAgentActivity { task_id, line } => {
self.handle_background_agent_activity(task_id, line)
}
AppEvent::BackgroundAgentKilled { task_id } => {
self.handle_background_agent_killed(task_id)
}
AppEvent::SetBackgroundAgentToken {
task_id,
query,
session_id,
interrupt_token,
} => {
self.handle_set_background_agent_token(task_id, query, session_id, interrupt_token)
}
AppEvent::SnapshotTaken { hash } => self.handle_snapshot_taken(hash),
AppEvent::UndoResult { success, message } => self.handle_undo_result(success, message),
AppEvent::RedoResult { success, message } => self.handle_redo_result(success, message),
AppEvent::ShareResult { path } => self.handle_share_result(path),
AppEvent::FileChanged { paths } => self.handle_file_changed(paths),
AppEvent::SessionTitleUpdated(title) => self.handle_session_title_updated(title),
AppEvent::Quit => {
self.state.running = false;
self.state.dirty = true;
}
_ => {}
}
}
fn handle_mouse_down(&mut self, col: u16, row: u16) {
if self.approval_controller.active()
|| self.ask_user_controller.active()
|| self.plan_approval_controller.active()
|| self
.model_picker_controller
.as_ref()
.is_some_and(|p| p.active())
|| self.state.task_watcher_open
|| self.state.debug_panel_open
{
return;
}
if self.state.selection.is_in_conversation_area(col, row) {
self.state.selection.start(col, row);
} else {
self.state.selection.clear();
}
}
fn handle_mouse_drag(&mut self, col: u16, row: u16) {
if !self.state.selection.active {
return;
}
self.state.selection.extend(col, row);
}
fn handle_mouse_up(&mut self, col: u16, row: u16) {
if !self.state.selection.active {
return;
}
self.state.selection.extend(col, row);
if self.state.selection.finalize() {
if let Some(text) = self.extract_selected_text() {
self.copy_to_clipboard(&text);
}
}
}
fn extract_selected_text(&self) -> Option<String> {
let range = self.state.selection.range?;
let (start, end) = range.ordered();
let lines = &self.state.cached_lines;
if lines.is_empty() || start.line_index >= lines.len() {
return None;
}
let end_line = end.line_index.min(lines.len().saturating_sub(1));
let mut result = String::new();
for (i, line) in lines[start.line_index..=end_line].iter().enumerate() {
let line_idx = start.line_index + i;
if i > 0 {
result.push('\n');
}
let full_text: String = line.spans.iter().map(|s| s.content.as_ref()).collect();
let col_start = if line_idx == start.line_index {
start.char_offset
} else {
0
};
let col_end = if line_idx == end.line_index {
end.char_offset
} else {
full_text.len()
};
let clamped_start = col_start.min(full_text.len());
let clamped_end = col_end.min(full_text.len());
if clamped_start < clamped_end {
let byte_start = full_text
.char_indices()
.nth(clamped_start)
.map(|(i, _)| i)
.unwrap_or(full_text.len());
let byte_end = full_text
.char_indices()
.nth(clamped_end)
.map(|(i, _)| i)
.unwrap_or(full_text.len());
result.push_str(&full_text[byte_start..byte_end]);
}
}
if result.is_empty() {
None
} else {
Some(result)
}
}
fn copy_to_clipboard(&mut self, text: &str) {
match arboard::Clipboard::new() {
Ok(mut clipboard) => {
if let Err(e) = clipboard.set_text(text) {
tracing::warn!("Failed to copy to clipboard: {e}");
}
}
Err(e) => {
tracing::warn!("Failed to access clipboard: {e}");
}
}
}
}