summaryrefslogblamecommitdiff
path: root/makima/src/daemon/tui/app.rs
blob: cb0e8f324a6eb3be986b4abedbb98fb33e769043 (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,
    /// Create contract - editing name
    CreateName,
    /// Create contract - editing description
    CreateDescription,
}

/// Create contract form field
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CreateFormField {
    Name,
    Description,
    ContractType,
    Repository,
}

/// Repository suggestion from history
#[derive(Debug, Clone)]
pub struct RepositorySuggestion {
    pub name: String,
    pub repository_url: Option<String>,
    pub local_path: Option<String>,
    pub source_type: String,
    pub use_count: i32,
}

/// State for create contract form
#[derive(Debug, Clone, Default)]
pub struct CreateContractState {
    /// Contract name
    pub name: String,
    /// Contract description
    pub description: String,
    /// Contract type: "simple" or "specification"
    pub contract_type: String,
    /// Repository URL (optional)
    pub repository_url: String,
    /// Currently focused field
    pub focused_field: usize,
    /// Cursor position in current text field
    pub cursor: usize,
    /// Available repository suggestions
    pub repo_suggestions: Vec<RepositorySuggestion>,
    /// Selected suggestion index (for repository field)
    pub selected_suggestion: usize,
    /// Whether suggestions popup is visible
    pub show_suggestions: bool,
    /// Whether suggestions have been loaded
    pub suggestions_loaded: bool,
}

impl CreateContractState {
    pub fn new() -> Self {
        Self {
            name: String::new(),
            description: String::new(),
            contract_type: "simple".to_string(),
            repository_url: String::new(),
            focused_field: 0,
            cursor: 0,
            repo_suggestions: Vec::new(),
            selected_suggestion: 0,
            show_suggestions: false,
            suggestions_loaded: false,
        }
    }

    /// Set repository suggestions
    pub fn set_suggestions(&mut self, suggestions: Vec<RepositorySuggestion>) {
        self.repo_suggestions = suggestions;
        self.selected_suggestion = 0;
        self.show_suggestions = !self.repo_suggestions.is_empty();
        self.suggestions_loaded = true;
    }

    /// Apply the selected suggestion to the form
    pub fn apply_selected_suggestion(&mut self) {
        if let Some(suggestion) = self.repo_suggestions.get(self.selected_suggestion) {
            // Apply the suggestion
            if let Some(ref url) = suggestion.repository_url {
                self.repository_url = url.clone();
            } else if let Some(ref path) = suggestion.local_path {
                self.repository_url = path.clone();
            }
            self.cursor = self.repository_url.len();
            self.show_suggestions = false;
        }
    }

    /// Navigate to next suggestion
    pub fn next_suggestion(&mut self) {
        if !self.repo_suggestions.is_empty() {
            self.selected_suggestion = (self.selected_suggestion + 1) % self.repo_suggestions.len();
        }
    }

    /// Navigate to previous suggestion
    pub fn prev_suggestion(&mut self) {
        if !self.repo_suggestions.is_empty() {
            self.selected_suggestion = if self.selected_suggestion == 0 {
                self.repo_suggestions.len() - 1
            } else {
                self.selected_suggestion - 1
            };
        }
    }

    /// Get the field at the given index
    pub fn field_at(&self, index: usize) -> CreateFormField {
        match index {
            0 => CreateFormField::Name,
            1 => CreateFormField::Description,
            2 => CreateFormField::ContractType,
            3 => CreateFormField::Repository,
            _ => CreateFormField::Name,
        }
    }

    /// Get the current field
    pub fn current_field(&self) -> CreateFormField {
        self.field_at(self.focused_field)
    }

    /// Get mutable reference to the current text field value
    pub fn current_value_mut(&mut self) -> Option<&mut String> {
        match self.current_field() {
            CreateFormField::Name => Some(&mut self.name),
            CreateFormField::Description => Some(&mut self.description),
            CreateFormField::Repository => Some(&mut self.repository_url),
            CreateFormField::ContractType => None, // Not a text field
        }
    }

    /// Get the current text field value
    pub fn current_value(&self) -> Option<&str> {
        match self.current_field() {
            CreateFormField::Name => Some(&self.name),
            CreateFormField::Description => Some(&self.description),
            CreateFormField::Repository => Some(&self.repository_url),
            CreateFormField::ContractType => None,
        }
    }

    /// Move to next field
    pub fn next_field(&mut self) {
        self.focused_field = (self.focused_field + 1) % 4;
        self.cursor = self.current_value().map(|v| v.len()).unwrap_or(0);
        // Don't hide suggestions - they stay visible
    }

    /// Move to previous field
    pub fn prev_field(&mut self) {
        self.focused_field = if self.focused_field == 0 { 3 } else { self.focused_field - 1 };
        self.cursor = self.current_value().map(|v| v.len()).unwrap_or(0);
        // Don't hide suggestions - they stay visible
    }

    /// Toggle contract type
    pub fn toggle_contract_type(&mut self) {
        self.contract_type = if self.contract_type == "simple" {
            "specification".to_string()
        } else {
            "simple".to_string()
        };
    }

    /// Insert character at cursor
    pub fn insert_char(&mut self, c: char) {
        let cursor = self.cursor;
        match self.current_field() {
            CreateFormField::Name => {
                self.name.insert(cursor, c);
                self.cursor += 1;
            }
            CreateFormField::Description => {
                self.description.insert(cursor, c);
                self.cursor += 1;
            }
            CreateFormField::Repository => {
                self.repository_url.insert(cursor, c);
                self.cursor += 1;
            }
            CreateFormField::ContractType => {}
        }
    }

    /// Delete character before cursor
    pub fn backspace(&mut self) {
        if self.cursor > 0 {
            let cursor = self.cursor - 1;
            match self.current_field() {
                CreateFormField::Name => {
                    self.name.remove(cursor);
                    self.cursor = cursor;
                }
                CreateFormField::Description => {
                    self.description.remove(cursor);
                    self.cursor = cursor;
                }
                CreateFormField::Repository => {
                    self.repository_url.remove(cursor);
                    self.cursor = cursor;
                }
                CreateFormField::ContractType => {}
            }
        }
    }

    /// Check if form is valid (name is required)
    pub fn is_valid(&self) -> bool {
        !self.name.trim().is_empty()
    }
}

/// 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,
    /// Open create contract form
    NewContract,
    /// Add character in create form
    CreateChar(char),
    /// Backspace in create form
    CreateBackspace,
    /// Move to next field in create form
    CreateNextField,
    /// Move to previous field in create form
    CreatePrevField,
    /// Toggle value (for contract type)
    CreateToggle,
    /// Submit create form
    CreateSubmit,
    /// Cancel create form
    CreateCancel,
    /// Request to create contract (internal)
    PerformCreateContract {
        name: String,
        description: String,
        contract_type: String,
        repository_url: Option<String>,
    },
    /// Request to load repository suggestions (internal)
    LoadRepoSuggestions,
    /// Navigate to next suggestion in create form
    CreateNextSuggestion,
    /// Navigate to previous suggestion in create form
    CreatePrevSuggestion,
    /// Apply selected suggestion in create form
    CreateApplySuggestion,
}

/// 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,
    /// Create contract form state
    pub create_state: CreateContractState,
    /// 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(),
            create_state: CreateContractState::new(),
            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();
                            // Push view and set state before returning
                            self.push_view(ViewType::Tasks);
                            self.contract_id = Some(contract_id);
                            self.contract_name = Some(contract_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();
                            // Push view and set state before returning
                            self.push_view(ViewType::TaskOutput);
                            self.task_id = Some(task_id);
                            self.task_name = Some(task_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 } => {
                // Pass through to caller for data loading (view already pushed by DrillDown)
                Action::LoadTasks { contract_id, contract_name }
            }
            Action::LoadTaskOutput { task_id, task_name } => {
                // Pass through to caller for data loading (view already pushed by DrillDown)
                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::NewContract => {
                // Only allow creating contracts from contracts view
                if self.view_type == ViewType::Contracts {
                    self.create_state = CreateContractState::new();
                    self.input_mode = InputMode::CreateName;
                    // Request to load repository suggestions
                    return Action::LoadRepoSuggestions;
                }
                Action::None
            }
            Action::CreateChar(c) => {
                self.create_state.insert_char(c);
                Action::None
            }
            Action::CreateBackspace => {
                self.create_state.backspace();
                Action::None
            }
            Action::CreateNextField => {
                self.create_state.next_field();
                Action::None
            }
            Action::CreatePrevField => {
                self.create_state.prev_field();
                Action::None
            }
            Action::CreateToggle => {
                if self.create_state.current_field() == CreateFormField::ContractType {
                    self.create_state.toggle_contract_type();
                }
                Action::None
            }
            Action::CreateSubmit => {
                if self.create_state.is_valid() {
                    let name = self.create_state.name.clone();
                    let description = self.create_state.description.clone();
                    let contract_type = self.create_state.contract_type.clone();
                    let repository_url = if self.create_state.repository_url.is_empty() {
                        None
                    } else {
                        Some(self.create_state.repository_url.clone())
                    };
                    self.input_mode = InputMode::Normal;
                    return Action::PerformCreateContract {
                        name,
                        description,
                        contract_type,
                        repository_url,
                    };
                } else {
                    self.status_message = Some("Name is required".to_string());
                }
                Action::None
            }
            Action::CreateCancel => {
                self.create_state = CreateContractState::new();
                self.input_mode = InputMode::Normal;
                self.status_message = Some("Create cancelled".to_string());
                Action::None
            }
            Action::PerformCreateContract { name, description, contract_type, repository_url } => {
                // Pass through to caller for API call
                Action::PerformCreateContract { name, description, contract_type, repository_url }
            }
            Action::LoadRepoSuggestions => {
                // Pass through to caller for API call
                Action::LoadRepoSuggestions
            }
            Action::CreateNextSuggestion => {
                self.create_state.next_suggestion();
                Action::None
            }
            Action::CreatePrevSuggestion => {
                self.create_state.prev_suggestion();
                Action::None
            }
            Action::CreateApplySuggestion => {
                self.create_state.apply_selected_suggestion();
                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())
        })
    }
}