From 0833fb1f30c0c3b920157deb882e0e902c3af02a Mon Sep 17 00:00:00 2001 From: soryu Date: Mon, 19 Jan 2026 13:47:32 +0000 Subject: Add interactive TUI browser for tasks, contracts, and files (makima view) (#7) * feat(tui): Implement fuzzy search with real-time filtering and highlighting Adds comprehensive fuzzy search functionality to the TUI browser: ## Fuzzy Matching (fuzzy.rs) - FuzzyMatcher wrapper using SkimMatcherV2 from fuzzy-matcher crate - fuzzy_match() returns score and matched character indices - fuzzy_match_all() supports multi-term search (space-separated) - Recency-aware scoring to boost recent items in results - Unit tests for all matching scenarios ## App State (app.rs) - FilteredItem struct with index, score, and matched_indices - apply_filter() uses fuzzy matching with score-based sorting - match_count() and has_no_matches() helper methods - Results sorted by match score (highest first) ## List View (list_view.rs) - Highlighted matched characters in search results - Yellow bold styling for matched chars - Status icons with color coding ## Search Input (search_input.rs) - Real-time match count display (X/Y matches) - Visual feedback for no matches (red border) - Placeholder text when search is empty - Active search mode indication (yellow border) ## Event Handling (event.rs) - Arrow key navigation while in search mode - Ctrl+K/J for vim-style navigation during search - Delete key support alongside backspace - Ctrl+U to clear search query - Tab toggles preview while searching - Escape clears search and exits search mode Co-Authored-By: Claude Opus 4.5 * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:20:34 UTC * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:31:19 UTC * Task completion checkpoint * [WIP] Heartbeat checkpoint - 2026-01-19 11:39:07 UTC * fix(tui): Fix module exports and main binary integration - Update mod.rs to properly export app, event, fuzzy, and ui modules - Add run() function for TUI entry point - Fix run_view() to use ViewCommand enum instead of ViewArgs - Fix event handling to use poll_event and handle_key_event Co-Authored-By: Claude Opus 4.5 --------- Co-authored-by: Claude Opus 4.5 --- makima/src/daemon/tui/views/contracts.rs | 24 +++++++++ makima/src/daemon/tui/views/files.rs | 90 ++++++++++++++++++++++++++++++++ makima/src/daemon/tui/views/mod.rs | 3 ++ makima/src/daemon/tui/views/tasks.rs | 71 +++++++++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 makima/src/daemon/tui/views/contracts.rs create mode 100644 makima/src/daemon/tui/views/files.rs create mode 100644 makima/src/daemon/tui/views/mod.rs create mode 100644 makima/src/daemon/tui/views/tasks.rs (limited to 'makima/src/daemon/tui/views') diff --git a/makima/src/daemon/tui/views/contracts.rs b/makima/src/daemon/tui/views/contracts.rs new file mode 100644 index 0000000..e2219b7 --- /dev/null +++ b/makima/src/daemon/tui/views/contracts.rs @@ -0,0 +1,24 @@ +//! Contracts view implementation. + +use uuid::Uuid; + +use crate::daemon::api::ApiClient; +use crate::daemon::tui::app::ListItem; + +/// Load contracts from API +pub async fn load_contracts( + _client: &ApiClient, +) -> Result, Box> { + // TODO: Implement listing all contracts + // This would require a new API endpoint + Ok(Vec::new()) +} + +/// Get full contract details for preview +pub async fn get_contract_preview( + _client: &ApiClient, + _contract_id: Uuid, +) -> Result> { + // TODO: Implement contract preview + Ok("Contract preview not yet implemented".to_string()) +} diff --git a/makima/src/daemon/tui/views/files.rs b/makima/src/daemon/tui/views/files.rs new file mode 100644 index 0000000..e21a989 --- /dev/null +++ b/makima/src/daemon/tui/views/files.rs @@ -0,0 +1,90 @@ +//! Files view implementation. + +use uuid::Uuid; + +use crate::daemon::api::ApiClient; +use crate::daemon::tui::app::ListItem; + +/// Load files from API +pub async fn load_files( + client: &ApiClient, + contract_id: Uuid, +) -> Result, Box> { + let result = client.contract_files(contract_id).await?; + + // Parse JSON response into ListItem + let files: Vec = serde_json::from_value(result.0)?; + + let items = files + .into_iter() + .filter_map(|f| { + let id_str = f.get("id")?.as_str()?; + let id = Uuid::parse_str(id_str).ok()?; + + Some(ListItem { + id, + name: f + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("Unnamed") + .to_string(), + status: None, + description: f + .get("description") + .and_then(|v| v.as_str()) + .map(String::from), + updated_at: f + .get("updatedAt") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + extra: f, + }) + }) + .collect(); + + Ok(items) +} + +/// Get full file details for preview +pub async fn get_file_preview( + client: &ApiClient, + contract_id: Uuid, + file_id: Uuid, +) -> Result> { + let result = client.contract_file(contract_id, file_id).await?; + let file: serde_json::Value = result.0; + + let name = file + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("Unknown"); + let description = file + .get("description") + .and_then(|v| v.as_str()) + .unwrap_or("-"); + + // Try to get body content + let body_preview = if let Some(body) = file.get("body") { + if let Some(body_array) = body.as_array() { + body_array + .iter() + .filter_map(|item| { + let text = item.get("text").and_then(|v| v.as_str())?; + Some(text.to_string()) + }) + .take(5) + .collect::>() + .join("\n") + } else { + "-".to_string() + } + } else { + "-".to_string() + }; + + Ok(format!( + "Name: {}\nDescription: {}\n\nContent:\n{}", + name, description, body_preview + )) +} diff --git a/makima/src/daemon/tui/views/mod.rs b/makima/src/daemon/tui/views/mod.rs new file mode 100644 index 0000000..699b6df --- /dev/null +++ b/makima/src/daemon/tui/views/mod.rs @@ -0,0 +1,3 @@ +pub mod contracts; +pub mod files; +pub mod tasks; diff --git a/makima/src/daemon/tui/views/tasks.rs b/makima/src/daemon/tui/views/tasks.rs new file mode 100644 index 0000000..fd52b11 --- /dev/null +++ b/makima/src/daemon/tui/views/tasks.rs @@ -0,0 +1,71 @@ +//! Tasks view implementation. + +use uuid::Uuid; + +use crate::daemon::api::ApiClient; +use crate::daemon::tui::app::ListItem; + +/// Load tasks from API +pub async fn load_tasks( + client: &ApiClient, + contract_id: Option, +) -> Result, Box> { + let Some(contract_id) = contract_id else { + // TODO: Implement listing all tasks across contracts + return Ok(Vec::new()); + }; + + let result = client.supervisor_tasks(contract_id).await?; + + // Parse JSON response into ListItem + let tasks: Vec = serde_json::from_value(result.0)?; + + let items = tasks + .into_iter() + .filter_map(|t| { + let id_str = t.get("id")?.as_str()?; + let id = Uuid::parse_str(id_str).ok()?; + + Some(ListItem { + id, + name: t + .get("name") + .and_then(|v| v.as_str()) + .unwrap_or("Unnamed") + .to_string(), + status: t.get("status").and_then(|v| v.as_str()).map(String::from), + description: t + .get("progressSummary") + .and_then(|v| v.as_str()) + .map(String::from), + updated_at: t + .get("updatedAt") + .and_then(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + extra: t, + }) + }) + .collect(); + + Ok(items) +} + +/// Get full task details for preview +pub async fn get_task_preview( + client: &ApiClient, + task_id: Uuid, +) -> Result> { + let result = client.supervisor_get_task(task_id).await?; + let task: serde_json::Value = result.0; + + Ok(format!( + "Name: {}\nStatus: {}\nPlan: {}\n\nProgress:\n{}", + task.get("name").and_then(|v| v.as_str()).unwrap_or("-"), + task.get("status").and_then(|v| v.as_str()).unwrap_or("-"), + task.get("plan").and_then(|v| v.as_str()).unwrap_or("-"), + task.get("progressSummary") + .and_then(|v| v.as_str()) + .unwrap_or("-"), + )) +} -- cgit v1.2.3