summaryrefslogblamecommitdiff
path: root/makima/src/daemon/tui/app.rs
blob: 3eff99835506fb2d9fda61226048155b0f02649b (plain) (tree)
1
2
3
4
5
6
7
8
9

                                    

                               




                                       


                                                    


                                            
                         
              



                                     




                                          
                                               

                                             



         


















                                              








                                              







































































































































                                                                       










                                  
                                                     
           




                                                                           


























                                                             























                                                                                       









































































































                                                                       




















                                                                                                                                                                   













                                                                                                                                       
                                                                                                    

                                                                      
                                                                                              

                                                                

                                                                      

                                                                                                                                                                   



                                                                                                                                         











                                                                                                                                                                   









                                                

















                                                 

                                     





                                                                    



                                          





                                             




                                   








                                              
                                             


                                 

                                                      


         





































































                                                                                        

























































                                                                                          



































                                                                                    
                             
                                                    
                                                          



















                                                                                   
                     




                                                                                      


                            





















































                                                                                       

                                                  























                                                                                                 






                                                                         





















































                                                                               







































                                                                           












                                                                        
//! TUI application state and logic.

use std::collections::VecDeque;

use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use serde_json::Value;
use uuid::Uuid;

/// Maximum number of output lines to keep in buffer
const MAX_OUTPUT_LINES: usize = 10000;

/// Available views/resource types
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ViewType {
    /// List of contracts
    Contracts,
    /// Tasks for a specific contract
    Tasks,
    /// Task output streaming view
    TaskOutput,
}

impl ViewType {
    pub fn as_str(&self) -> &'static str {
        match self {
            ViewType::Contracts => "contracts",
            ViewType::Tasks => "tasks",
            ViewType::TaskOutput => "output",
        }
    }
}

/// A saved view state for navigation stack
#[derive(Debug, Clone)]
pub struct ViewState {
    /// The type of view
    pub view_type: ViewType,
    /// Contract ID (for Tasks view)
    pub contract_id: Option<Uuid>,
    /// Contract name (for breadcrumb display)
    pub contract_name: Option<String>,
    /// Task ID (for TaskOutput view)
    pub task_id: Option<Uuid>,
    /// Task name (for breadcrumb display)
    pub task_name: Option<String>,
    /// Selected index at time of navigation
    pub selected_index: usize,
    /// Scroll offset at time of navigation
    pub scroll_offset: usize,
}

/// Input mode for the TUI
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InputMode {
    /// Normal navigation mode
    Normal,
    /// Fuzzy search mode
    Search,
    /// Confirmation dialog (e.g., for delete)
    Confirm,
    /// Edit mode - editing name
    EditName,
    /// Edit mode - editing description/plan
    EditDescription,
}

/// Edit state for inline editing
#[derive(Debug, Clone, Default)]
pub struct EditState {
    /// ID of the item being edited
    pub item_id: Option<Uuid>,
    /// Original name
    pub original_name: String,
    /// Original description
    pub original_description: String,
    /// Current name value
    pub name: String,
    /// Current description value
    pub description: String,
    /// Cursor position in current field
    pub cursor: usize,
}

/// Output line type for rendering
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum OutputMessageType {
    /// Assistant text response
    Assistant,
    /// Tool being called
    ToolUse,
    /// Result from tool
    ToolResult,
    /// Final result/summary
    Result,
    /// System message
    System,
    /// Error message
    Error,
    /// Raw/unformatted output
    Raw,
}

impl OutputMessageType {
    pub fn from_str(s: &str) -> Self {
        match s.to_lowercase().as_str() {
            "assistant" => Self::Assistant,
            "tool_use" => Self::ToolUse,
            "tool_result" => Self::ToolResult,
            "result" => Self::Result,
            "system" => Self::System,
            "error" => Self::Error,
            _ => Self::Raw,
        }
    }
}

/// A single line of task output
#[derive(Debug, Clone)]
pub struct OutputLine {
    /// The type of message
    pub message_type: OutputMessageType,
    /// The content text
    pub content: String,
    /// Tool name (for tool_use messages)
    pub tool_name: Option<String>,
    /// Whether this is an error (for tool_result)
    pub is_error: bool,
    /// Cost in USD (for result messages)
    pub cost_usd: Option<f64>,
    /// Duration in ms (for result messages)
    pub duration_ms: Option<u64>,
}

/// Output buffer for task output view
#[derive(Debug, Clone, Default)]
pub struct OutputBuffer {
    /// Lines of output
    pub lines: VecDeque<OutputLine>,
    /// Current scroll offset (0 = bottom, auto-scroll)
    pub scroll_offset: usize,
    /// Auto-scroll enabled
    pub auto_scroll: bool,
}

impl OutputBuffer {
    pub fn new() -> Self {
        Self {
            lines: VecDeque::new(),
            scroll_offset: 0,
            auto_scroll: true,
        }
    }

    pub fn add_line(&mut self, line: OutputLine) {
        self.lines.push_back(line);
        // Trim to max size
        while self.lines.len() > MAX_OUTPUT_LINES {
            self.lines.pop_front();
        }
        // Auto-scroll to bottom
        if self.auto_scroll {
            self.scroll_offset = 0;
        }
    }

    pub fn clear(&mut self) {
        self.lines.clear();
        self.scroll_offset = 0;
        self.auto_scroll = true;
    }

    pub fn scroll_up(&mut self, amount: usize) {
        self.scroll_offset = self.scroll_offset.saturating_add(amount);
        self.auto_scroll = false;
    }

    pub fn scroll_down(&mut self, amount: usize) {
        self.scroll_offset = self.scroll_offset.saturating_sub(amount);
        if self.scroll_offset == 0 {
            self.auto_scroll = true;
        }
    }

    pub fn scroll_to_bottom(&mut self) {
        self.scroll_offset = 0;
        self.auto_scroll = true;
    }
}

/// WebSocket connection state
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WsConnectionState {
    Disconnected,
    Connecting,
    Connected,
    Reconnecting,
}

/// Actions that can be performed
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
    /// Do nothing
    None,
    /// Move selection up
    Up,
    /// Move selection down
    Down,
    /// Select current item (show details in preview)
    Select,
    /// Drill down into selected item (contracts -> tasks, tasks -> output)
    DrillDown,
    /// Go back to previous view
    GoBack,
    /// Edit the selected item (inline editing)
    Edit,
    /// Delete the selected item
    Delete,
    /// Navigate to worktree (output path and exit)
    Navigate,
    /// Confirm pending action
    ConfirmYes,
    /// Cancel pending action
    ConfirmNo,
    /// Enter search mode
    EnterSearch,
    /// Exit search mode
    ExitSearch,
    /// Add character to search
    SearchChar(char),
    /// Backspace in search
    SearchBackspace,
    /// Clear search
    ClearSearch,
    /// Quit the application
    Quit,
    /// Output a path to stdout and exit (for cd integration)
    OutputPath(String),
    /// Launch editor with path
    LaunchEditor(String),
    /// Refresh data
    Refresh,
    /// Request to load tasks for a contract (internal)
    LoadTasks { contract_id: Uuid, contract_name: String },
    /// Request to load task output (internal)
    LoadTaskOutput { task_id: Uuid, task_name: String },
    /// Request to delete an item (internal)
    PerformDelete { id: Uuid, item_type: ViewType },
    /// Add character in edit mode
    EditChar(char),
    /// Backspace in edit mode
    EditBackspace,
    /// Switch to next edit field (Tab)
    EditNextField,
    /// Save edit changes
    EditSave,
    /// Cancel edit
    EditCancel,
    /// Request to perform update (internal)
    PerformUpdate { id: Uuid, item_type: ViewType, name: String, description: String },
    /// Scroll output up
    ScrollUp,
    /// Scroll output down
    ScrollDown,
    /// Scroll to bottom of output
    ScrollToBottom,
}

/// A displayable item in the TUI
#[derive(Debug, Clone)]
pub struct ListItem {
    pub id: Uuid,
    pub name: String,
    pub status: Option<String>,
    pub description: Option<String>,
    /// Extra data for actions (e.g., worktree path)
    pub extra: Value,
}

impl ListItem {
    pub fn from_task(value: &Value) -> Option<Self> {
        let id = value.get("id")
            .and_then(|v| v.as_str())
            .and_then(|s| Uuid::parse_str(s).ok())?;

        let name = value.get("name")
            .and_then(|v| v.as_str())
            .unwrap_or("Unnamed")
            .to_string();

        let status = value.get("status")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let description = value.get("plan")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        Some(Self {
            id,
            name,
            status,
            description,
            extra: value.clone(),
        })
    }

    pub fn from_contract(value: &Value) -> Option<Self> {
        let id = value.get("id")
            .and_then(|v| v.as_str())
            .and_then(|s| Uuid::parse_str(s).ok())?;

        let name = value.get("name")
            .and_then(|v| v.as_str())
            .unwrap_or("Unnamed")
            .to_string();

        let status = value.get("phase")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        let description = value.get("description")
            .and_then(|v| v.as_str())
            .map(|s| s.to_string());

        Some(Self {
            id,
            name,
            status,
            description,
            extra: value.clone(),
        })
    }

    pub fn from_file(value: &Value) -> Option<Self> {
        let id = value.get("id")
            .and_then(|v| v.as_str())
            .and_then(|s| Uuid::parse_str(s).ok())?;

        let name = value.get("name")
            .and_then(|v| v.as_str())
            .unwrap_or("Unnamed")
            .to_string();

        let description = value.get("template_name")
            .and_then(|v| v.as_str())
            .map(|s| format!("Template: {}", s));

        Some(Self {
            id,
            name,
            status: None,
            description,
            extra: value.clone(),
        })
    }

    /// Get the worktree path from task extra data
    pub fn get_worktree_path(&self) -> Option<String> {
        // Try various field names that might contain the worktree path
        self.extra.get("worktreePath")
            .or_else(|| self.extra.get("worktree_path"))
            .or_else(|| self.extra.get("workdir"))
            .and_then(|v| v.as_str())
            .map(|s| s.to_string())
    }

    /// Build a detailed view string for display
    pub fn build_detail_view(&self, view_type: ViewType) -> String {
        let mut lines = Vec::new();

        match view_type {
            ViewType::Contracts => {
                lines.push(format!("╭─ Contract Details ─────────────────────"));
                lines.push(format!("│ Name: {}", self.name));
                lines.push(format!("│ ID: {}", self.id));
                if let Some(ref status) = self.status {
                    lines.push(format!("│ Phase: {}", status));
                }
                if let Some(ref desc) = self.description {
                    lines.push(format!("│ Description: {}", desc));
                }
                // Show task count if available
                if let Some(count) = self.extra.get("taskCount").and_then(|v| v.as_i64()) {
                    lines.push(format!("│ Tasks: {}", count));
                }
                if let Some(count) = self.extra.get("fileCount").and_then(|v| v.as_i64()) {
                    lines.push(format!("│ Files: {}", count));
                }
                lines.push(format!("│"));
                lines.push(format!("│ Press Enter to view tasks"));
                lines.push(format!("╰────────────────────────────────────────"));
            }
            ViewType::Tasks => {
                lines.push(format!("╭─ Task Details ─────────────────────────"));
                lines.push(format!("│ Name: {}", self.name));
                lines.push(format!("│ ID: {}", self.id));
                if let Some(ref status) = self.status {
                    lines.push(format!("│ Status: {}", status));
                }
                if let Some(ref desc) = self.description {
                    lines.push(format!("│ Plan: {}", desc));
                }
                if let Some(path) = self.get_worktree_path() {
                    lines.push(format!("│ Worktree: {}", path));
                }
                // Add progress if available
                if let Some(progress) = self.extra.get("progressSummary").and_then(|v| v.as_str()) {
                    lines.push(format!("│ Progress: {}", progress));
                }
                if let Some(error) = self.extra.get("errorMessage").and_then(|v| v.as_str()) {
                    lines.push(format!("│ Error: {}", error));
                }
                lines.push(format!("│"));
                lines.push(format!("│ Press Enter to view output"));
                lines.push(format!("╰────────────────────────────────────────"));
            }
            ViewType::TaskOutput => {
                // Output view doesn't use this preview pane
                lines.push(format!("╭─ Task Output ──────────────────────────"));
                lines.push(format!("│ Streaming task output..."));
                lines.push(format!("╰────────────────────────────────────────"));
            }
        }

        lines.join("\n")
    }
}

/// TUI Application state
pub struct App {
    /// Current view type
    pub view_type: ViewType,
    /// Navigation stack for drill-down views
    pub view_stack: Vec<ViewState>,
    /// Current contract ID (when viewing tasks)
    pub contract_id: Option<Uuid>,
    /// Current contract name (for breadcrumb)
    pub contract_name: Option<String>,
    /// Current task ID (when viewing output)
    pub task_id: Option<Uuid>,
    /// Current task name (for breadcrumb)
    pub task_name: Option<String>,
    /// All items (unfiltered)
    pub items: Vec<ListItem>,
    /// Filtered items (based on search)
    pub filtered_items: Vec<ListItem>,
    /// Currently selected index in filtered list
    pub selected_index: usize,
    /// Current input mode
    pub input_mode: InputMode,
    /// Search query
    pub search_query: String,
    /// Fuzzy matcher
    matcher: SkimMatcherV2,
    /// Preview content (for selected item)
    pub preview_content: String,
    /// Whether preview is visible
    pub preview_visible: bool,
    /// Pending delete item (for confirmation)
    pub pending_delete: Option<Uuid>,
    /// Edit state for inline editing
    pub edit_state: EditState,
    /// Status message
    pub status_message: Option<String>,
    /// Whether the app should quit
    pub should_quit: bool,
    /// Action to return when exiting (for OutputPath, LaunchEditor)
    pub exit_action: Option<Action>,
    /// Output buffer for task output view
    pub output_buffer: OutputBuffer,
    /// WebSocket connection state
    pub ws_state: WsConnectionState,
}

impl App {
    pub fn new(view_type: ViewType) -> Self {
        Self {
            view_type,
            view_stack: Vec::new(),
            contract_id: None,
            contract_name: None,
            task_id: None,
            task_name: None,
            items: Vec::new(),
            filtered_items: Vec::new(),
            selected_index: 0,
            input_mode: InputMode::Normal,
            search_query: String::new(),
            matcher: SkimMatcherV2::default(),
            preview_content: String::new(),
            preview_visible: false,
            pending_delete: None,
            edit_state: EditState::default(),
            status_message: None,
            should_quit: false,
            exit_action: None,
            output_buffer: OutputBuffer::new(),
            ws_state: WsConnectionState::Disconnected,
        }
    }

    /// Push current state to navigation stack and prepare for new view
    pub fn push_view(&mut self, new_view: ViewType) {
        // Save current state
        let state = ViewState {
            view_type: self.view_type,
            contract_id: self.contract_id,
            contract_name: self.contract_name.clone(),
            task_id: self.task_id,
            task_name: self.task_name.clone(),
            selected_index: self.selected_index,
            scroll_offset: 0, // TODO: track scroll offset if needed
        };
        self.view_stack.push(state);

        // Switch to new view
        self.view_type = new_view;
        self.items.clear();
        self.filtered_items.clear();
        self.selected_index = 0;
        self.search_query.clear();
        self.preview_content.clear();
        self.preview_visible = false;
    }

    /// Pop from navigation stack and restore previous view state
    pub fn pop_view(&mut self) -> bool {
        if let Some(state) = self.view_stack.pop() {
            self.view_type = state.view_type;
            self.contract_id = state.contract_id;
            self.contract_name = state.contract_name;
            self.task_id = state.task_id;
            self.task_name = state.task_name;
            self.selected_index = state.selected_index;
            self.items.clear();
            self.filtered_items.clear();
            self.search_query.clear();
            self.preview_content.clear();
            self.preview_visible = false;
            true
        } else {
            false
        }
    }

    /// Check if we can go back
    pub fn can_go_back(&self) -> bool {
        !self.view_stack.is_empty()
    }

    /// Get breadcrumb path for current view
    pub fn get_breadcrumb(&self) -> String {
        let mut parts = vec!["Contracts".to_string()];

        if self.view_type == ViewType::Tasks || self.view_type == ViewType::TaskOutput {
            if let Some(ref name) = self.contract_name {
                parts.push(name.clone());
            }
            parts.push("Tasks".to_string());
        }

        if self.view_type == ViewType::TaskOutput {
            if let Some(ref name) = self.task_name {
                parts.push(name.clone());
            }
            parts.push("Output".to_string());
        }

        parts.join(" > ")
    }

    /// Set items and update filtered list
    pub fn set_items(&mut self, items: Vec<ListItem>) {
        self.items = items;
        self.update_filtered_items();
    }

    /// Update filtered items based on search query
    pub fn update_filtered_items(&mut self) {
        if self.search_query.is_empty() {
            self.filtered_items = self.items.clone();
        } else {
            let mut scored: Vec<_> = self.items
                .iter()
                .filter_map(|item| {
                    let score = self.matcher.fuzzy_match(&item.name, &self.search_query)?;
                    Some((score, item.clone()))
                })
                .collect();

            // Sort by score (highest first)
            scored.sort_by(|a, b| b.0.cmp(&a.0));
            self.filtered_items = scored.into_iter().map(|(_, item)| item).collect();
        }

        // Reset selection if out of bounds
        if self.selected_index >= self.filtered_items.len() {
            self.selected_index = self.filtered_items.len().saturating_sub(1);
        }
    }

    /// Get currently selected item
    pub fn selected_item(&self) -> Option<&ListItem> {
        self.filtered_items.get(self.selected_index)
    }

    /// Handle an action and return the resulting action
    pub fn handle_action(&mut self, action: Action) -> Action {
        match action {
            Action::Up => {
                if self.selected_index > 0 {
                    self.selected_index -= 1;
                }
                Action::None
            }
            Action::Down => {
                if self.selected_index < self.filtered_items.len().saturating_sub(1) {
                    self.selected_index += 1;
                }
                Action::None
            }
            Action::Select => {
                // Build detailed view for selected item
                if let Some(item) = self.selected_item() {
                    self.preview_content = item.build_detail_view(self.view_type);
                    self.preview_visible = true;
                }
                Action::None
            }
            Action::DrillDown => {
                // Drill down into selected item
                match self.view_type {
                    ViewType::Contracts => {
                        // From contracts, drill into tasks
                        if let Some(item) = self.selected_item() {
                            let contract_id = item.id;
                            let contract_name = item.name.clone();
                            return Action::LoadTasks { contract_id, contract_name };
                        }
                    }
                    ViewType::Tasks => {
                        // From tasks, drill into task output
                        if let Some(item) = self.selected_item() {
                            let task_id = item.id;
                            let task_name = item.name.clone();
                            return Action::LoadTaskOutput { task_id, task_name };
                        }
                    }
                    ViewType::TaskOutput => {
                        // No further drill-down from output view
                    }
                }
                Action::None
            }
            Action::GoBack => {
                if self.can_go_back() {
                    self.pop_view();
                    // Signal to caller to refresh data for the restored view
                    Action::Refresh
                } else {
                    // At root level, quit
                    self.should_quit = true;
                    Action::Quit
                }
            }
            Action::Edit => {
                // Enter edit mode for selected item
                if let Some(item) = self.selected_item() {
                    let name = item.name.clone();
                    let description = item.description.clone().unwrap_or_default();
                    self.edit_state = EditState {
                        item_id: Some(item.id),
                        original_name: name.clone(),
                        original_description: description.clone(),
                        name,
                        description,
                        cursor: 0,
                    };
                    self.edit_state.cursor = self.edit_state.name.len();
                    self.input_mode = InputMode::EditName;
                }
                Action::None
            }
            Action::EditChar(c) => {
                match self.input_mode {
                    InputMode::EditName => {
                        self.edit_state.name.insert(self.edit_state.cursor, c);
                        self.edit_state.cursor += 1;
                    }
                    InputMode::EditDescription => {
                        self.edit_state.description.insert(self.edit_state.cursor, c);
                        self.edit_state.cursor += 1;
                    }
                    _ => {}
                }
                Action::None
            }
            Action::EditBackspace => {
                match self.input_mode {
                    InputMode::EditName => {
                        if self.edit_state.cursor > 0 {
                            self.edit_state.cursor -= 1;
                            self.edit_state.name.remove(self.edit_state.cursor);
                        }
                    }
                    InputMode::EditDescription => {
                        if self.edit_state.cursor > 0 {
                            self.edit_state.cursor -= 1;
                            self.edit_state.description.remove(self.edit_state.cursor);
                        }
                    }
                    _ => {}
                }
                Action::None
            }
            Action::EditNextField => {
                match self.input_mode {
                    InputMode::EditName => {
                        self.input_mode = InputMode::EditDescription;
                        self.edit_state.cursor = self.edit_state.description.len();
                    }
                    InputMode::EditDescription => {
                        self.input_mode = InputMode::EditName;
                        self.edit_state.cursor = self.edit_state.name.len();
                    }
                    _ => {}
                }
                Action::None
            }
            Action::EditSave => {
                if let Some(id) = self.edit_state.item_id {
                    let name = self.edit_state.name.clone();
                    let description = self.edit_state.description.clone();
                    self.input_mode = InputMode::Normal;
                    // Return action to perform the update
                    return Action::PerformUpdate {
                        id,
                        item_type: self.view_type,
                        name,
                        description,
                    };
                }
                self.input_mode = InputMode::Normal;
                Action::None
            }
            Action::EditCancel => {
                self.edit_state = EditState::default();
                self.input_mode = InputMode::Normal;
                self.status_message = Some("Edit cancelled".to_string());
                Action::None
            }
            Action::Delete => {
                // First press: enter confirm mode
                if let Some(item) = self.selected_item() {
                    let id = item.id;
                    let name = item.name.clone();
                    self.pending_delete = Some(id);
                    self.input_mode = InputMode::Confirm;
                    self.status_message = Some(format!("Delete '{}'? (y/n)", name));
                }
                Action::None
            }
            Action::Navigate => {
                // Get worktree path and output it
                if let Some(item) = self.selected_item() {
                    if let Some(path) = item.get_worktree_path() {
                        self.should_quit = true;
                        self.exit_action = Some(Action::OutputPath(path.clone()));
                        return Action::OutputPath(path);
                    } else {
                        self.status_message = Some("No worktree path for this item".to_string());
                    }
                }
                Action::None
            }
            Action::ConfirmYes => {
                if self.input_mode == InputMode::Confirm {
                    if let Some(delete_id) = self.pending_delete.take() {
                        self.input_mode = InputMode::Normal;
                        // Return action to perform the delete
                        return Action::PerformDelete {
                            id: delete_id,
                            item_type: self.view_type,
                        };
                    }
                    self.input_mode = InputMode::Normal;
                }
                Action::None
            }
            Action::ConfirmNo => {
                if self.input_mode == InputMode::Confirm {
                    self.pending_delete = None;
                    self.input_mode = InputMode::Normal;
                    self.status_message = Some("Delete cancelled".to_string());
                }
                Action::None
            }
            Action::EnterSearch => {
                self.input_mode = InputMode::Search;
                Action::None
            }
            Action::ExitSearch => {
                self.input_mode = InputMode::Normal;
                Action::None
            }
            Action::SearchChar(c) => {
                self.search_query.push(c);
                self.update_filtered_items();
                Action::None
            }
            Action::SearchBackspace => {
                self.search_query.pop();
                self.update_filtered_items();
                Action::None
            }
            Action::ClearSearch => {
                self.search_query.clear();
                self.update_filtered_items();
                Action::None
            }
            Action::Quit => {
                self.should_quit = true;
                Action::Quit
            }
            Action::Refresh => {
                // Signal to caller to refresh data
                Action::Refresh
            }
            Action::OutputPath(path) => {
                self.should_quit = true;
                self.exit_action = Some(Action::OutputPath(path.clone()));
                Action::OutputPath(path)
            }
            Action::LaunchEditor(path) => {
                self.should_quit = true;
                self.exit_action = Some(Action::LaunchEditor(path.clone()));
                Action::LaunchEditor(path)
            }
            Action::LoadTasks { contract_id, contract_name } => {
                // Prepare for tasks view
                self.push_view(ViewType::Tasks);
                self.contract_id = Some(contract_id);
                self.contract_name = Some(contract_name.clone());
                Action::LoadTasks { contract_id, contract_name }
            }
            Action::LoadTaskOutput { task_id, task_name } => {
                // Prepare for output view
                self.push_view(ViewType::TaskOutput);
                self.task_id = Some(task_id);
                self.task_name = Some(task_name.clone());
                Action::LoadTaskOutput { task_id, task_name }
            }
            Action::PerformDelete { id, item_type } => {
                // Pass through to caller for API call
                Action::PerformDelete { id, item_type }
            }
            Action::PerformUpdate { id, item_type, name, description } => {
                // Pass through to caller for API call
                Action::PerformUpdate { id, item_type, name, description }
            }
            Action::ScrollUp => {
                if self.view_type == ViewType::TaskOutput {
                    self.output_buffer.scroll_up(5);
                }
                Action::None
            }
            Action::ScrollDown => {
                if self.view_type == ViewType::TaskOutput {
                    self.output_buffer.scroll_down(5);
                }
                Action::None
            }
            Action::ScrollToBottom => {
                if self.view_type == ViewType::TaskOutput {
                    self.output_buffer.scroll_to_bottom();
                }
                Action::None
            }
            Action::None => Action::None,
        }
    }

    /// Get the name of the item being deleted (for confirmation dialog)
    pub fn get_pending_delete_name(&self) -> Option<String> {
        self.pending_delete.and_then(|id| {
            self.filtered_items.iter()
                .find(|item| item.id == id)
                .map(|item| item.name.clone())
        })
    }
}