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/bin/makima.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/bin/makima.rs')
| -rw-r--r-- | makima/src/bin/makima.rs | 540 |
1 files changed, 1 insertions, 539 deletions
diff --git a/makima/src/bin/makima.rs b/makima/src/bin/makima.rs index 7b8cdb6..bf8c35f 100644 --- a/makima/src/bin/makima.rs +++ b/makima/src/bin/makima.rs @@ -6,9 +6,8 @@ use std::sync::Arc; use makima::daemon::api::ApiClient; use makima::daemon::cli::{ - Cli, CliConfig, Commands, ConfigCommand, DirectiveCommand, ViewArgs, + Cli, CliConfig, Commands, ConfigCommand, DirectiveCommand, }; -use makima::daemon::tui::{self, Action, App, ListItem, ViewType, TuiWsClient, WsEvent, OutputLine, OutputMessageType, WsConnectionState, RepositorySuggestion}; use makima::daemon::config::{DaemonConfig, RepoEntry}; use makima::daemon::db::LocalDb; use makima::daemon::error::DaemonError; @@ -27,7 +26,6 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> { Commands::Server(args) => run_server(args).await, Commands::Daemon(args) => run_daemon(args).await, Commands::Directive(cmd) => run_directive(cmd).await, - Commands::View(args) => run_view(args).await, Commands::Config(cmd) => run_config(cmd).await, } } @@ -625,51 +623,6 @@ async fn run_directive_verify( } /// Run the TUI view command. -async fn run_view(args: ViewArgs) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { - // Load CLI config for defaults - let config = CliConfig::load(); - - // Get API URL and key, preferring CLI args > env vars > config file - // Filter out empty strings - let api_url = args.api_url - .filter(|s| !s.is_empty()) - .unwrap_or_else(|| config.get_api_url()); - let api_key = match args.api_key.filter(|s| !s.is_empty()) { - Some(key) => key, - None => config.get_api_key().ok_or_else(|| { - eprintln!("Error: No API key provided."); - eprintln!(); - eprintln!("Set your API key using one of these methods:"); - eprintln!(" 1. Run: makima config set-key YOUR_API_KEY"); - eprintln!(" 2. Set environment variable: export MAKIMA_API_KEY=YOUR_API_KEY"); - eprintln!(" 3. Pass via CLI: makima view --api-key YOUR_API_KEY"); - "No API key configured" - })?, - }; - - // Create API client - let client = ApiClient::new(api_url.clone(), api_key.clone())?; - - // Start WebSocket client for task output streaming - let ws_client = TuiWsClient::start(api_url, api_key); - - // Start at contracts view - let mut app = App::new(ViewType::Contracts); - - // Set initial search query if provided - if let Some(ref query) = args.query { - app.search_query = query.clone(); - } - - // Load initial contracts - let items = load_contracts(&client).await?; - app.set_items(items); - - // Run TUI with navigation support - let result = run_tui_with_navigation(app, client, ws_client).await; - - result -} /// Run config commands. async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { @@ -715,497 +668,6 @@ async fn run_config(cmd: ConfigCommand) -> Result<(), Box<dyn std::error::Error } /// Load contracts from API -async fn load_contracts(client: &ApiClient) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { - let result = client.list_contracts().await?; - let items = result.0.get("contracts") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(ListItem::from_contract).collect()) - .unwrap_or_default(); - Ok(items) -} - -/// Load tasks for a contract from API -async fn load_tasks(client: &ApiClient, contract_id: uuid::Uuid) -> Result<Vec<ListItem>, Box<dyn std::error::Error + Send + Sync>> { - // Use get_contract which returns tasks as part of the response (works with regular API key auth) - let result = client.get_contract(contract_id).await?; - let mut items: Vec<ListItem> = result.0.get("tasks") - .and_then(|v| v.as_array()) - .map(|arr| arr.iter().filter_map(ListItem::from_task).collect()) - .unwrap_or_default(); - - // Sort tasks: supervisor first, then by status (running first), then by name - items.sort_by(|a, b| { - // Check if task is supervisor (role field in extra data) - let a_is_supervisor = a.extra.get("role") - .and_then(|v| v.as_str()) - .map(|s| s == "supervisor") - .unwrap_or(false); - let b_is_supervisor = b.extra.get("role") - .and_then(|v| v.as_str()) - .map(|s| s == "supervisor") - .unwrap_or(false); - - // Supervisor first - match (a_is_supervisor, b_is_supervisor) { - (true, false) => std::cmp::Ordering::Less, - (false, true) => std::cmp::Ordering::Greater, - _ => { - // Then by status: running/working tasks first - let status_order = |s: Option<&String>| -> i32 { - match s.map(|x| x.as_str()) { - Some("running") | Some("working") => 0, - Some("pending") | Some("queued") => 1, - Some("completed") | Some("done") => 2, - Some("failed") | Some("error") => 3, - _ => 4, - } - }; - let a_order = status_order(a.status.as_ref()); - let b_order = status_order(b.status.as_ref()); - - match a_order.cmp(&b_order) { - std::cmp::Ordering::Equal => a.name.cmp(&b.name), - other => other, - } - } - } - }); - - Ok(items) -} - -/// Run the TUI with navigation support for drill-down views -async fn run_tui_with_navigation( - mut app: App, - client: ApiClient, - ws_client: TuiWsClient, -) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { - use crossterm::{ - event::{DisableMouseCapture, EnableMouseCapture}, - execute, - terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, - }; - use ratatui::backend::CrosstermBackend; - use std::io; - - // Setup terminal - enable_raw_mode()?; - let mut stdout = io::stdout(); - execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?; - let backend = CrosstermBackend::new(stdout); - let mut terminal = ratatui::Terminal::new(backend)?; - - let result = run_tui_loop(&mut terminal, &mut app, &client, &ws_client).await; - - // Cleanup WebSocket - ws_client.shutdown(); - - // Cleanup terminal - disable_raw_mode()?; - execute!( - terminal.backend_mut(), - LeaveAlternateScreen, - DisableMouseCapture - )?; - terminal.show_cursor()?; - - match result { - Ok(Some(path)) => { - // Output the path for shell integration - tui::print_path(&path); - } - Ok(None) => { - // Normal exit - } - Err(e) => { - eprintln!("TUI error: {}", e); - std::process::exit(1); - } - } - - Ok(()) -} - -/// Main TUI event loop with async data loading -async fn run_tui_loop( - terminal: &mut ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stdout>>, - app: &mut App, - client: &ApiClient, - ws_client: &TuiWsClient, -) -> Result<Option<String>, Box<dyn std::error::Error + Send + Sync>> { - use crossterm::event::{self, Event}; - use std::time::Duration; - - // Track currently subscribed task for cleanup - let mut subscribed_task_id: Option<uuid::Uuid> = None; - - loop { - terminal.draw(|f| tui::ui::render(f, app))?; - - // Process WebSocket events (non-blocking) - while let Some(ws_event) = ws_client.try_recv() { - handle_ws_event(app, ws_event); - } - - // Poll for keyboard events with short timeout (50ms for responsive WS handling) - if event::poll(Duration::from_millis(50))? { - if let Event::Key(key) = event::read()? { - let action = tui::event::handle_key_event(app, key); - match action { - Action::Quit => break, - Action::OutputPath(path) => return Ok(Some(path)), - Action::None => {} - _ => { - let result = app.handle_action(action); - match result { - Action::OutputPath(path) => return Ok(Some(path)), - Action::LoadTasks { contract_id, contract_name: _ } => { - // Unsubscribe from any previous task - if let Some(task_id) = subscribed_task_id.take() { - ws_client.unsubscribe(task_id); - } - // Load tasks for the selected contract - match load_tasks(client, contract_id).await { - Ok(items) => { - app.set_items(items); - } - Err(e) => { - app.status_message = Some(format!("Failed to load tasks: {}", e)); - } - } - } - Action::LoadTaskOutput { task_id, task_name: _ } => { - // Clear previous output - app.output_buffer.clear(); - app.ws_state = WsConnectionState::Connecting; - - // Unsubscribe from previous task if any - if let Some(old_task_id) = subscribed_task_id.take() { - ws_client.unsubscribe(old_task_id); - } - - // Load task output history first - app.status_message = Some("Loading output history...".to_string()); - match client.get_task_output(task_id).await { - Ok(result) => { - // Parse the entries array from response - if let Some(entries) = result.0.get("entries").and_then(|v| v.as_array()) { - for entry in entries { - if let Some(line) = parse_output_entry(entry) { - app.output_buffer.add_line(line); - } - } - let count = entries.len(); - app.status_message = Some(format!("Loaded {} history entries, streaming live...", count)); - } - } - Err(e) => { - app.status_message = Some(format!("Failed to load history: {}", e)); - } - } - - // Subscribe to new task output for live updates - ws_client.subscribe(task_id); - subscribed_task_id = Some(task_id); - } - Action::PerformDelete { id, item_type } => { - // Perform the delete API call - let delete_result = match item_type { - ViewType::Contracts => { - client.delete_contract(id).await - } - ViewType::Tasks => { - client.delete_task(id).await - } - ViewType::TaskOutput => { - // Can't delete from output view - Ok(()) - } - }; - - match delete_result { - Ok(()) => { - app.status_message = Some("Deleted successfully".to_string()); - // Remove item from list - app.items.retain(|item| item.id != id); - app.update_filtered_items(); - } - Err(e) => { - app.status_message = Some(format!("Delete failed: {}", e)); - } - } - } - Action::PerformUpdate { id, item_type, name, description } => { - // Perform the update API call - let update_result = match item_type { - ViewType::Contracts => { - client.update_contract(id, Some(name.clone()), Some(description.clone())).await.map(|_| ()) - } - ViewType::Tasks => { - // For tasks, description is the plan - client.update_task(id, Some(name.clone()), Some(description.clone())).await.map(|_| ()) - } - ViewType::TaskOutput => { - // Can't edit from output view - Ok(()) - } - }; - - match update_result { - Ok(()) => { - app.status_message = Some("Updated successfully".to_string()); - // Update item in list - for item in &mut app.items { - if item.id == id { - item.name = name.clone(); - item.description = Some(description.clone()); - break; - } - } - app.update_filtered_items(); - } - Err(e) => { - app.status_message = Some(format!("Update failed: {}", e)); - } - } - } - Action::Refresh => { - // Reload data for current view - match app.view_type { - ViewType::Contracts => { - // Unsubscribe from task when going back to contracts - if let Some(task_id) = subscribed_task_id.take() { - ws_client.unsubscribe(task_id); - } - match load_contracts(client).await { - Ok(items) => app.set_items(items), - Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)), - } - } - ViewType::Tasks => { - // Unsubscribe from task when going back to tasks - if let Some(task_id) = subscribed_task_id.take() { - ws_client.unsubscribe(task_id); - } - if let Some(contract_id) = app.contract_id { - match load_tasks(client, contract_id).await { - Ok(items) => app.set_items(items), - Err(e) => app.status_message = Some(format!("Refresh failed: {}", e)), - } - } - } - ViewType::TaskOutput => { - // Re-subscribe to the task output - if let Some(task_id) = app.task_id { - app.output_buffer.clear(); - app.ws_state = WsConnectionState::Connecting; - ws_client.subscribe(task_id); - subscribed_task_id = Some(task_id); - app.status_message = Some("Reconnecting...".to_string()); - } - } - } - } - Action::GoBack => { - // Unsubscribe when going back from output view - if let Some(task_id) = subscribed_task_id.take() { - ws_client.unsubscribe(task_id); - app.ws_state = WsConnectionState::Disconnected; - } - } - Action::PerformCreateContract { name: _, description: _, contract_type: _, repository_url: _ } => { - // Contracts removed in Phase 5 — directives are - // the only way to organise multi-task work now. - // The TUI's contract create form is dead code - // pending a wider TUI refresh. - app.status_message = Some( - "Contracts have been removed. Use directives instead.".to_string() - ); - } - Action::LoadRepoSuggestions => { - // Load repository suggestions for the create form - app.status_message = Some("Loading recent repositories...".to_string()); - // Force a redraw to show the status - terminal.draw(|f| tui::ui::render(f, app)).ok(); - - // Fetch all repository types (remote and local) - match client.get_repository_suggestions(None, Some(10)).await { - Ok(result) => { - // Parse suggestions from API response - let suggestions: Vec<RepositorySuggestion> = result.0 - .get("entries") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter().filter_map(|entry| { - let name = entry.get("name") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - let repository_url = entry.get("repositoryUrl") - .or_else(|| entry.get("repository_url")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let local_path = entry.get("localPath") - .or_else(|| entry.get("local_path")) - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let source_type = entry.get("sourceType") - .or_else(|| entry.get("source_type")) - .and_then(|v| v.as_str()) - .unwrap_or("remote") - .to_string(); - let use_count = entry.get("useCount") - .or_else(|| entry.get("use_count")) - .and_then(|v| v.as_i64()) - .unwrap_or(0) as i32; - - // Only include if we have a URL or path - if repository_url.is_some() || local_path.is_some() { - Some(RepositorySuggestion { - name, - repository_url, - local_path, - source_type, - use_count, - }) - } else { - None - } - }).collect() - }) - .unwrap_or_default(); - - let count = suggestions.len(); - app.create_state.set_suggestions(suggestions); - if count > 0 { - app.status_message = Some(format!("Found {} recent repositories", count)); - } else { - app.status_message = Some("No recent repositories found".to_string()); - } - } - Err(e) => { - app.status_message = Some(format!("Could not load suggestions: {}", e)); - app.create_state.suggestions_loaded = true; - } - } - } - _ => {} - } - } - } - } - } - - if app.should_quit { - break; - } - } - - Ok(None) -} - -/// Extract a repository name from a URL. -/// E.g., "https://github.com/owner/repo.git" -> "owner/repo" -fn extract_repo_name(url: &str) -> String { - // Remove .git suffix if present - let url = url.trim_end_matches(".git"); - - // Try to extract owner/repo from common Git hosting URLs - if let Some(path) = url.strip_prefix("https://github.com/") - .or_else(|| url.strip_prefix("https://gitlab.com/")) - .or_else(|| url.strip_prefix("https://bitbucket.org/")) - .or_else(|| url.strip_prefix("git@github.com:")) - .or_else(|| url.strip_prefix("git@gitlab.com:")) - .or_else(|| url.strip_prefix("git@bitbucket.org:")) - { - // Return owner/repo - return path.to_string(); - } - - // Fallback: try to get the last path segment - if let Some(last_segment) = url.rsplit('/').next() { - if !last_segment.is_empty() { - return last_segment.to_string(); - } - } - - // Last resort: use the full URL as the name - url.to_string() -} - -/// Parse an output entry from the API response into an OutputLine -fn parse_output_entry(entry: &serde_json::Value) -> Option<OutputLine> { - let message_type = entry.get("messageType") - .and_then(|v| v.as_str()) - .unwrap_or("raw"); - let content = entry.get("content") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - let tool_name = entry.get("toolName") - .and_then(|v| v.as_str()) - .map(|s| s.to_string()); - let is_error = entry.get("isError") - .and_then(|v| v.as_bool()) - .unwrap_or(false); - let cost_usd = entry.get("costUsd") - .and_then(|v| v.as_f64()); - let duration_ms = entry.get("durationMs") - .and_then(|v| v.as_u64()); - - Some(OutputLine { - message_type: OutputMessageType::from_str(message_type), - content, - tool_name, - is_error, - cost_usd, - duration_ms, - }) -} - -/// Handle a WebSocket event and update app state -fn handle_ws_event(app: &mut App, event: WsEvent) { - match event { - WsEvent::Connected => { - app.ws_state = WsConnectionState::Connected; - app.status_message = Some("Connected".to_string()); - } - WsEvent::Disconnected => { - app.ws_state = WsConnectionState::Disconnected; - app.status_message = Some("Disconnected".to_string()); - } - WsEvent::Reconnecting { attempt } => { - app.ws_state = WsConnectionState::Reconnecting; - app.status_message = Some(format!("Reconnecting (attempt {})...", attempt)); - } - WsEvent::Subscribed { task_id: _ } => { - app.ws_state = WsConnectionState::Connected; - app.status_message = Some("Subscribed to task output".to_string()); - } - WsEvent::Unsubscribed { task_id: _ } => { - // No status update needed - } - WsEvent::TaskOutput(output) => { - // Convert WebSocket event to OutputLine - let line = OutputLine { - message_type: OutputMessageType::from_str(&output.message_type), - content: output.content, - tool_name: output.tool_name, - is_error: output.is_error.unwrap_or(false), - cost_usd: output.cost_usd, - duration_ms: output.duration_ms, - }; - app.output_buffer.add_line(line); - // Clear status message once we're receiving output - if app.status_message.as_ref().map(|s| s.contains("Subscribed")).unwrap_or(false) { - app.status_message = None; - } - } - WsEvent::Error { message } => { - app.status_message = Some(format!("WS Error: {}", message)); - } - } -} fn init_logging(level: &str, format: &str) { let filter = EnvFilter::try_from_default_env() |
