summaryrefslogtreecommitdiff
path: root/makima/src/daemon/tui/app.rs
diff options
context:
space:
mode:
Diffstat (limited to 'makima/src/daemon/tui/app.rs')
-rw-r--r--makima/src/daemon/tui/app.rs1219
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())
- })
- }
-}