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/ui.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/ui.rs')
| -rw-r--r-- | makima/src/daemon/tui/ui.rs | 695 |
1 files changed, 0 insertions, 695 deletions
diff --git a/makima/src/daemon/tui/ui.rs b/makima/src/daemon/tui/ui.rs deleted file mode 100644 index 2a5a6ce..0000000 --- a/makima/src/daemon/tui/ui.rs +++ /dev/null @@ -1,695 +0,0 @@ -//! TUI rendering. - -use ratatui::{ - layout::{Alignment, Constraint, Direction, Layout, Rect}, - style::{Color, Modifier, Style}, - text::{Line, Span, Text}, - widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap}, - Frame, -}; - -use super::app::{App, CreateFormField, InputMode, ViewType, OutputMessageType, WsConnectionState}; -use super::event::{get_help_text, get_output_help_text}; - -/// Main render function -pub fn render(frame: &mut Frame, app: &App) { - // Create main layout - let chunks = Layout::default() - .direction(Direction::Vertical) - .constraints([ - Constraint::Length(3), // Header - Constraint::Min(10), // Main content - Constraint::Length(3), // Status/Help - ]) - .split(frame.area()); - - render_header(frame, app, chunks[0]); - render_main_content(frame, app, chunks[1]); - render_footer(frame, app, chunks[2]); - - // Render confirmation dialog if in confirm mode - if app.input_mode == InputMode::Confirm { - render_confirm_dialog(frame, app); - } - - // Render edit dialog if in edit mode - if matches!(app.input_mode, InputMode::EditName | InputMode::EditDescription) { - render_edit_dialog(frame, app); - } - - // Render create contract dialog if in create mode - if matches!(app.input_mode, InputMode::CreateName | InputMode::CreateDescription) { - render_create_dialog(frame, app); - } -} - -/// Render header with breadcrumb and search bar -fn render_header(frame: &mut Frame, app: &App, area: Rect) { - let breadcrumb = app.get_breadcrumb(); - - let header_text = if app.input_mode == InputMode::Search || !app.search_query.is_empty() { - format!("{} [Search: {}]", breadcrumb, app.search_query) - } else { - format!("{} ({} items)", breadcrumb, app.filtered_items.len()) - }; - - let header = Paragraph::new(header_text) - .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)) - .block(Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(if app.input_mode == InputMode::Search { - Color::Yellow - } else { - Color::White - }))); - - frame.render_widget(header, area); -} - -/// Render main content (list + optional preview) -fn render_main_content(frame: &mut Frame, app: &App, area: Rect) { - // TaskOutput view has its own rendering - if app.view_type == ViewType::TaskOutput { - render_output_view(frame, app, area); - return; - } - - if app.preview_visible && !app.preview_content.is_empty() { - // Split horizontally: list on left, preview on right - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Percentage(50), - Constraint::Percentage(50), - ]) - .split(area); - - render_list(frame, app, chunks[0]); - render_preview(frame, app, chunks[1]); - } else { - render_list(frame, app, area); - } -} - -/// Render the item list -fn render_list(frame: &mut Frame, app: &App, area: Rect) { - let items: Vec<ListItem> = app.filtered_items - .iter() - .enumerate() - .map(|(i, item)| { - let is_selected = i == app.selected_index; - - // Build the display line - let status_str = item.status - .as_ref() - .map(|s| format!(" [{}]", s)) - .unwrap_or_default(); - - let content = format!("{}{}", item.name, status_str); - - let style = if is_selected { - Style::default() - .fg(Color::Black) - .bg(Color::Cyan) - .add_modifier(Modifier::BOLD) - } else { - let status_color = item.status.as_ref().map(|s| { - match s.to_lowercase().as_str() { - "running" | "active" => Color::Green, - "pending" | "waiting" => Color::Yellow, - "completed" | "done" => Color::Blue, - "failed" | "error" => Color::Red, - _ => Color::White, - } - }).unwrap_or(Color::White); - - Style::default().fg(status_color) - }; - - ListItem::new(Line::from(vec![ - Span::styled(content, style), - ])) - }) - .collect(); - - let list = List::new(items) - .block(Block::default() - .borders(Borders::ALL) - .title(format!(" {} ", app.view_type.as_str()))); - - frame.render_widget(list, area); -} - -/// Render the preview panel -fn render_preview(frame: &mut Frame, app: &App, area: Rect) { - let preview = Paragraph::new(Text::raw(&app.preview_content)) - .wrap(Wrap { trim: false }) - .block(Block::default() - .borders(Borders::ALL) - .title(" Preview ")); - - frame.render_widget(preview, area); -} - -/// Render footer with help text and status -fn render_footer(frame: &mut Frame, app: &App, area: Rect) { - // Use output-specific help text when in output view - let help_text = if app.view_type == ViewType::TaskOutput { - get_output_help_text() - } else { - get_help_text(app.input_mode) - }; - - // Build status text with WS connection state for output view - let ws_status = if app.view_type == ViewType::TaskOutput { - match app.ws_state { - WsConnectionState::Connected => " [WS: Connected]", - WsConnectionState::Connecting => " [WS: Connecting...]", - WsConnectionState::Reconnecting => " [WS: Reconnecting...]", - WsConnectionState::Disconnected => " [WS: Disconnected]", - } - } else { - "" - }; - - let status_text = app.status_message - .as_ref() - .map(|s| format!(" | {}", s)) - .unwrap_or_default(); - - let footer_text = format!("{}{}{}", help_text, ws_status, status_text); - - let footer = Paragraph::new(footer_text) - .style(Style::default().fg(Color::DarkGray)) - .block(Block::default().borders(Borders::ALL)); - - frame.render_widget(footer, area); -} - -/// Render confirmation dialog as a centered popup -fn render_confirm_dialog(frame: &mut Frame, app: &App) { - let item_name = app.get_pending_delete_name() - .unwrap_or_else(|| "this item".to_string()); - - // Calculate popup size and position - let area = frame.area(); - let popup_width = 50.min(area.width.saturating_sub(4)); - let popup_height = 7; - - let popup_x = (area.width.saturating_sub(popup_width)) / 2; - let popup_y = (area.height.saturating_sub(popup_height)) / 2; - - let popup_area = Rect { - x: popup_x, - y: popup_y, - width: popup_width, - height: popup_height, - }; - - // Clear the area behind the popup - frame.render_widget(Clear, popup_area); - - // Build popup content - let text = vec![ - Line::from(""), - Line::from(Span::styled( - "Delete Confirmation", - Style::default().fg(Color::Red).add_modifier(Modifier::BOLD), - )), - Line::from(""), - Line::from(format!("Delete '{}'?", item_name)), - Line::from(""), - Line::from(vec![ - Span::styled("y", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": confirm "), - Span::styled("n", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::raw(": cancel"), - ]), - ]; - - let popup = Paragraph::new(text) - .alignment(Alignment::Center) - .block(Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Red)) - .title(" Confirm ")); - - frame.render_widget(popup, popup_area); -} - -/// Render edit dialog as a centered popup -fn render_edit_dialog(frame: &mut Frame, app: &App) { - // Calculate popup size and position - make it wider - let area = frame.area(); - let popup_width = 80.min(area.width.saturating_sub(4)); - let popup_height = 14; - - let popup_x = (area.width.saturating_sub(popup_width)) / 2; - let popup_y = (area.height.saturating_sub(popup_height)) / 2; - - let popup_area = Rect { - x: popup_x, - y: popup_y, - width: popup_width, - height: popup_height, - }; - - // Clear the area behind the popup - frame.render_widget(Clear, popup_area); - - // Determine which field is active - let editing_name = app.input_mode == InputMode::EditName; - - // Calculate max display width (popup width - borders - label) - let max_field_width = (popup_width as usize).saturating_sub(16); - - // Build the name field with cursor and truncation - let name_display = if editing_name { - let cursor_pos = app.edit_state.cursor.min(app.edit_state.name.len()); - let (before, after) = app.edit_state.name.split_at(cursor_pos); - let display = format!("{}|{}", before, after); - // Show end of string if cursor is past visible area - if display.len() > max_field_width { - let start = display.len().saturating_sub(max_field_width); - format!("...{}", &display[start..]) - } else { - display - } - } else { - if app.edit_state.name.len() > max_field_width { - format!("{}...", &app.edit_state.name[..max_field_width.saturating_sub(3)]) - } else { - app.edit_state.name.clone() - } - }; - - // Build the description field with cursor and truncation - let desc_display = if !editing_name { - let cursor_pos = app.edit_state.cursor.min(app.edit_state.description.len()); - let (before, after) = app.edit_state.description.split_at(cursor_pos); - let display = format!("{}|{}", before, after); - // Show end of string if cursor is past visible area - if display.len() > max_field_width { - let start = display.len().saturating_sub(max_field_width); - format!("...{}", &display[start..]) - } else { - display - } - } else { - if app.edit_state.description.len() > max_field_width { - format!("{}...", &app.edit_state.description[..max_field_width.saturating_sub(3)]) - } else { - app.edit_state.description.clone() - } - }; - - // Determine field label based on view type - let desc_label = match app.view_type { - ViewType::Tasks => "Plan", - _ => "Desc", - }; - - // Style for active vs inactive fields - let active_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD); - let inactive_style = Style::default().fg(Color::White); - let label_style = Style::default().fg(Color::DarkGray); - - // Build popup content - use left alignment for fields - let text = vec![ - Line::from(""), - Line::from(Span::styled( - " Edit Item", - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - )), - Line::from(""), - // Name field - Line::from(vec![ - Span::styled(" Name: ", label_style), - Span::styled( - name_display, - if editing_name { active_style } else { inactive_style }, - ), - ]), - Line::from(""), - // Description field - Line::from(vec![ - Span::styled(format!(" {}: ", desc_label), label_style), - Span::styled( - desc_display, - if !editing_name { active_style } else { inactive_style }, - ), - ]), - Line::from(""), - Line::from(""), - Line::from(vec![ - Span::styled(" Tab", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": switch "), - Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": save "), - Span::styled("Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::raw(": cancel"), - ]), - ]; - - let popup = Paragraph::new(text) - .block(Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) - .title(" Edit ")); - - frame.render_widget(popup, popup_area); -} - -/// Render the create contract dialog -fn render_create_dialog(frame: &mut Frame, app: &App) { - // Calculate popup size and position - make it taller if suggestions are shown - let area = frame.area(); - let state = &app.create_state; - let current_field = state.current_field(); - // Show suggestions whenever we have them (like the frontend does) - let show_suggestions = state.show_suggestions && !state.repo_suggestions.is_empty(); - - let popup_width = 70.min(area.width.saturating_sub(4)); - let base_height = 20; - let suggestion_height = if show_suggestions { - (state.repo_suggestions.len().min(5) + 2) as u16 - } else { - 0 - }; - let popup_height = base_height + suggestion_height; - - let popup_x = (area.width.saturating_sub(popup_width)) / 2; - let popup_y = (area.height.saturating_sub(popup_height)) / 2; - - let popup_area = Rect { - x: popup_x, - y: popup_y, - width: popup_width, - height: popup_height, - }; - - // Clear the area behind the popup - frame.render_widget(Clear, popup_area); - - let max_field_width = (popup_width as usize).saturating_sub(18); - - // Styles - let active_style = Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD); - let inactive_style = Style::default().fg(Color::White); - let label_style = Style::default().fg(Color::DarkGray); - let hint_style = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC); - let suggestion_style = Style::default().fg(Color::White); - let selected_suggestion_style = Style::default().fg(Color::Black).bg(Color::Cyan); - - // Helper to build text field with cursor - let build_field = |value: &str, cursor: usize, is_active: bool| -> String { - if is_active { - let cursor_pos = cursor.min(value.len()); - let (before, after) = value.split_at(cursor_pos); - let display = format!("{}|{}", before, after); - if display.len() > max_field_width { - let start = display.len().saturating_sub(max_field_width); - format!("...{}", &display[start..]) - } else { - display - } - } else { - if value.len() > max_field_width { - format!("{}...", &value[..max_field_width.saturating_sub(3)]) - } else if value.is_empty() { - "(empty)".to_string() - } else { - value.to_string() - } - } - }; - - // Build field displays - let name_display = build_field(&state.name, state.cursor, current_field == CreateFormField::Name); - let desc_display = build_field(&state.description, state.cursor, current_field == CreateFormField::Description); - let repo_display = build_field(&state.repository_url, state.cursor, current_field == CreateFormField::Repository); - - // Contract type selector - let type_display = if state.contract_type == "simple" { - vec![ - Span::styled("[●] ", Style::default().fg(Color::Green)), - Span::raw("Simple "), - Span::styled("[ ] ", Style::default().fg(Color::DarkGray)), - Span::styled("Specification", Style::default().fg(Color::DarkGray)), - ] - } else { - vec![ - Span::styled("[ ] ", Style::default().fg(Color::DarkGray)), - Span::styled("Simple ", Style::default().fg(Color::DarkGray)), - Span::styled("[●] ", Style::default().fg(Color::Green)), - Span::raw("Specification"), - ] - }; - - let mut text = vec![ - Line::from(""), - Line::from(Span::styled( - " New Contract", - Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD), - )), - Line::from(""), - // Name field (required) - Line::from(vec![ - Span::styled(" Name*: ", label_style), - Span::styled( - name_display, - if current_field == CreateFormField::Name { active_style } else { inactive_style }, - ), - ]), - Line::from(Span::styled(" Contract name (required)", hint_style)), - Line::from(""), - // Description field - Line::from(vec![ - Span::styled(" Description: ", label_style), - Span::styled( - desc_display, - if current_field == CreateFormField::Description { active_style } else { inactive_style }, - ), - ]), - Line::from(Span::styled(" Brief description of the work", hint_style)), - Line::from(""), - // Contract type selector - Line::from(vec![ - Span::styled(" Type: ", label_style), - ].into_iter().chain( - if current_field == CreateFormField::ContractType { - type_display.into_iter().map(|s| s).collect::<Vec<_>>() - } else { - type_display.into_iter().map(|mut s| { - s.style = s.style.fg(Color::DarkGray); - s - }).collect() - } - ).collect::<Vec<_>>()), - Line::from(Span::styled(" Simple: Plan→Execute | Spec: Research→Specify→Plan→Execute→Review", hint_style)), - Line::from(""), - // Repository URL field - Line::from(vec![ - Span::styled(" Repository: ", label_style), - Span::styled( - repo_display, - if current_field == CreateFormField::Repository { active_style } else { inactive_style }, - ), - ]), - Line::from(Span::styled(" Git repository URL (optional)", hint_style)), - ]; - - // Add suggestions section - if show_suggestions { - text.push(Line::from("")); - text.push(Line::from(Span::styled( - " Recent repositories (↑/↓ to select, Enter to apply):", - Style::default().fg(Color::Cyan), - ))); - - for (i, suggestion) in state.repo_suggestions.iter().take(5).enumerate() { - let is_selected = i == state.selected_suggestion; - let url_or_path = suggestion.repository_url.as_ref() - .or(suggestion.local_path.as_ref()) - .map(|s| s.as_str()) - .unwrap_or(""); - - // Truncate if too long - let display_url = if url_or_path.len() > max_field_width - 10 { - format!("...{}", &url_or_path[url_or_path.len().saturating_sub(max_field_width - 13)..]) - } else { - url_or_path.to_string() - }; - - let prefix = if is_selected { " → " } else { " " }; - let count_suffix = format!(" ({}×)", suggestion.use_count); - - text.push(Line::from(vec![ - Span::styled( - format!("{}{}{}", prefix, display_url, count_suffix), - if is_selected { selected_suggestion_style } else { suggestion_style }, - ), - ])); - } - } else if state.suggestions_loaded && state.repo_suggestions.is_empty() { - // Show message when suggestions loaded but empty - text.push(Line::from("")); - text.push(Line::from(Span::styled( - " (No recent repositories - add repos to contracts to see suggestions here)", - hint_style, - ))); - } - - text.push(Line::from("")); - - // Help line - show different help when suggestions are visible - if show_suggestions { - text.push(Line::from(vec![ - Span::styled(" ↑/↓", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": select "), - Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": apply "), - Span::styled("Tab", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": next field "), - Span::styled("Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::raw(": cancel"), - ])); - } else { - text.push(Line::from(vec![ - Span::styled(" Tab/↑↓", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": switch "), - Span::styled("Enter", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": create "), - Span::styled("Space", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::raw(": toggle type "), - Span::styled("Esc", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::raw(": cancel"), - ])); - } - - let popup = Paragraph::new(text) - .block(Block::default() - .borders(Borders::ALL) - .border_style(Style::default().fg(Color::Cyan)) - .title(" Create Contract ")); - - frame.render_widget(popup, popup_area); -} - -/// Render the task output view -fn render_output_view(frame: &mut Frame, app: &App, area: Rect) { - let buffer = &app.output_buffer; - - // Calculate visible area (subtract 2 for borders) - let visible_height = area.height.saturating_sub(2) as usize; - - // Build lines to display - let total_lines = buffer.lines.len(); - let start_idx = if total_lines > visible_height { - total_lines - .saturating_sub(visible_height) - .saturating_sub(buffer.scroll_offset) - } else { - 0 - }; - - let lines: Vec<Line> = buffer.lines - .iter() - .skip(start_idx) - .take(visible_height) - .map(|line| render_output_line(line)) - .collect(); - - // Build title with scroll indicator - let scroll_indicator = if buffer.auto_scroll { - "[auto-scroll]".to_string() - } else if buffer.scroll_offset > 0 { - format!("[+{}]", buffer.scroll_offset) - } else { - String::new() - }; - - let title = format!(" Task Output {} ", scroll_indicator); - - let paragraph = Paragraph::new(lines) - .block(Block::default() - .borders(Borders::ALL) - .title(title) - .border_style(Style::default().fg(match app.ws_state { - WsConnectionState::Connected => Color::Green, - WsConnectionState::Connecting | WsConnectionState::Reconnecting => Color::Yellow, - WsConnectionState::Disconnected => Color::Red, - }))); - - frame.render_widget(paragraph, area); -} - -/// Render a single output line with appropriate styling -fn render_output_line(line: &super::app::OutputLine) -> Line<'static> { - match line.message_type { - OutputMessageType::Assistant => { - // Blue left indicator for assistant messages - Line::from(vec![ - Span::styled("│ ", Style::default().fg(Color::Blue)), - Span::styled(line.content.clone(), Style::default().fg(Color::White)), - ]) - } - OutputMessageType::ToolUse => { - // Yellow asterisk for tool calls - let tool_name = line.tool_name.clone().unwrap_or_else(|| "tool".to_string()); - Line::from(vec![ - Span::styled("* ", Style::default().fg(Color::Yellow)), - Span::styled(format!("[{}] ", tool_name), Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)), - Span::styled(line.content.clone(), Style::default().fg(Color::DarkGray)), - ]) - } - OutputMessageType::ToolResult => { - // Green/red indicator for tool results - let indicator = if line.is_error { "✗ " } else { " + " }; - let color = if line.is_error { Color::Red } else { Color::Green }; - Line::from(vec![ - Span::styled(indicator, Style::default().fg(color)), - Span::styled(line.content.clone(), Style::default().fg(Color::Gray)), - ]) - } - OutputMessageType::Result => { - // Green checkmark for final results - let mut spans = vec![ - Span::styled("✓ ", Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)), - Span::styled(line.content.clone(), Style::default().fg(Color::Green)), - ]; - // Add cost/duration if available - if let Some(cost) = line.cost_usd { - spans.push(Span::styled( - format!(" [${:.4}]", cost), - Style::default().fg(Color::DarkGray), - )); - } - if let Some(ms) = line.duration_ms { - spans.push(Span::styled( - format!(" [{}ms]", ms), - Style::default().fg(Color::DarkGray), - )); - } - Line::from(spans) - } - OutputMessageType::System => { - // Dim gray for system messages - Line::from(vec![ - Span::styled(" ", Style::default()), - Span::styled(line.content.clone(), Style::default().fg(Color::DarkGray)), - ]) - } - OutputMessageType::Error => { - // Red for errors - Line::from(vec![ - Span::styled("! ", Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)), - Span::styled(line.content.clone(), Style::default().fg(Color::Red)), - ]) - } - OutputMessageType::Raw => { - // Plain text - Line::from(line.content.clone()) - } - } -} |
