diff options
| author | soryu <soryu@soryu.co> | 2026-05-18 01:21:30 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-18 01:21:30 +0100 |
| commit | f240675da99bc7705e473b8f70a2628812aa4c10 (patch) | |
| tree | 3ee2d24b431ccb8cd1a3013c86b34a5782a3e224 /makima/src/daemon/tui/app.rs | |
| parent | 0d996cf7590e3e52f424859c7d6f0e68640f119e (diff) | |
| download | soryu-master.tar.gz soryu-master.zip | |
The contracts table, supervisor task type, and all their backing
machinery have been inert for several PRs. The directives system reads
its own active contract body for spec text, and PR #135 removed the
last LLM surface that spawned supervisors.
This PR wipes the dead surface in one shot — the user authorised a DB
wipe, so the migration drops every legacy table with CASCADE rather
than carrying forward stub rows. Net change: −12k LOC across handlers,
repository, state, models, the TUI, and the listen module.
What's gone:
- contracts, contract_chat_*, contract_events, contract_repositories,
contract_type_templates tables.
- supervisor_states, supervisor_heartbeats tables.
- mesh_chat_conversations, mesh_chat_messages tables.
- tasks.contract_id/is_supervisor/supervisor_task_id/supervisor_worktree_task_id columns.
- directive_steps.contract_id/contract_type columns.
- files.contract_id/contract_phase columns.
- history_events.contract_id/phase columns.
- The Contract/Supervisor/MeshChat handler + model + repository
surface, plus the daemon TUI views that read them.
- The standalone listen.rs websocket handler (orphaned with the LLM).
What stays:
- mesh_supervisor handler: trimmed to just the questions + orders
backchannel used by `makima directive ask` / `create-order` (kept
the URL prefix for CLI client compat).
- directive_documents (the user-facing "contracts" surface).
- pending_questions in-memory state for the directive Ask flow.
cargo check, cargo test --lib (68 passed), tsc, and vite build all
clean.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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()) - }) - } -} |
