diff options
Diffstat (limited to 'makima/src/daemon/tui/app.rs')
| -rw-r--r-- | makima/src/daemon/tui/app.rs | 1219 |
1 files changed, 0 insertions, 1219 deletions
diff --git a/makima/src/daemon/tui/app.rs b/makima/src/daemon/tui/app.rs deleted file mode 100644 index cb0e8f3..0000000 --- a/makima/src/daemon/tui/app.rs +++ /dev/null @@ -1,1219 +0,0 @@ -//! 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()) - }) - } -} |
