Skip to main content

flow_tui/
app.rs

1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
2use flow_core::{Feature, Theme};
3use flow_db::Database;
4use ratatui::Frame;
5use std::path::PathBuf;
6use std::sync::Arc;
7use std::time::{Duration, Instant};
8
9use crate::theme::TuiTheme;
10use crate::views;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum View {
14    Kanban,
15    Agents,
16    Logs,
17    Graph,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum LayoutMode {
22    Full,
23    Compact,
24    Mobile,
25}
26
27pub struct App {
28    pub current_view: View,
29    pub layout_mode: LayoutMode,
30    pub theme: Theme,
31    pub tui_theme: TuiTheme,
32    pub features: Vec<Feature>,
33    pub selected_index: usize,
34    pub show_help: bool,
35    #[allow(dead_code)]
36    pub should_quit: bool,
37    pub terminal_width: u16,
38    pub terminal_height: u16,
39    #[allow(dead_code)]
40    pub scroll_offset: usize,
41    pub log_messages: Vec<String>,
42    pub db: Option<Arc<Database>>,
43    pub db_path: Option<PathBuf>,
44    pub db_error: Option<String>,
45    pub last_refresh: Instant,
46    pub refresh_interval: Duration,
47    pub status_message: Option<(String, Instant)>,
48    pub feature_stats: Option<flow_core::FeatureStats>,
49}
50
51impl Default for App {
52    fn default() -> Self {
53        Self::new()
54    }
55}
56
57impl App {
58    pub fn new() -> Self {
59        Self::with_db_path(None)
60    }
61
62    pub fn with_db_path(db_path: Option<PathBuf>) -> Self {
63        let theme = Theme::default();
64        let tui_theme = TuiTheme::from(&theme.colors());
65
66        let mut app = Self {
67            current_view: View::Kanban,
68            layout_mode: LayoutMode::Full,
69            theme,
70            tui_theme,
71            features: Vec::new(),
72            selected_index: 0,
73            show_help: false,
74            should_quit: false,
75            terminal_width: 120,
76            terminal_height: 30,
77            scroll_offset: 0,
78            log_messages: Vec::new(),
79            db: None,
80            db_path,
81            db_error: None,
82            last_refresh: Instant::now(),
83            refresh_interval: Duration::from_secs(2),
84            status_message: None,
85            feature_stats: None,
86        };
87
88        app.load_from_database();
89        app
90    }
91
92    pub fn render(&self, frame: &mut Frame) {
93        if self.show_help {
94            views::help::render(frame, self);
95        } else {
96            match self.current_view {
97                View::Kanban => views::kanban::render(frame, self),
98                View::Agents => views::agents::render(frame, self),
99                View::Logs => views::logs::render(frame, self),
100                View::Graph => views::graph::render(frame, self),
101            }
102        }
103    }
104
105    pub fn handle_key(&mut self, key: KeyEvent) -> bool {
106        // Quit commands
107        if key.code == KeyCode::Char('q')
108            || (key.code == KeyCode::Char('c') && key.modifiers.contains(KeyModifiers::CONTROL))
109        {
110            return true;
111        }
112
113        // Help toggle
114        if key.code == KeyCode::Char('?') {
115            self.show_help = !self.show_help;
116            return false;
117        }
118
119        // If help is showing, only toggle it off
120        if self.show_help {
121            self.show_help = false;
122            return false;
123        }
124
125        // View switching
126        match key.code {
127            KeyCode::Char('1') => self.current_view = View::Kanban,
128            KeyCode::Char('2') => self.current_view = View::Agents,
129            KeyCode::Char('3') => self.current_view = View::Logs,
130            KeyCode::Char('4') => self.current_view = View::Graph,
131            KeyCode::Tab => self.next_view(),
132            _ => {}
133        }
134
135        // Navigation
136        match key.code {
137            KeyCode::Char('j') | KeyCode::Down => self.next_item(),
138            KeyCode::Char('k') | KeyCode::Up => self.prev_item(),
139            _ => {}
140        }
141
142        // Theme cycling
143        if key.code == KeyCode::Char('t') {
144            self.cycle_theme();
145        }
146
147        // Database actions
148        match key.code {
149            KeyCode::Enter | KeyCode::Char(' ') => self.claim_selected_feature(),
150            KeyCode::Char('p') => self.mark_selected_passing(),
151            KeyCode::Char('f') => self.mark_selected_failing(),
152            KeyCode::Char('c') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
153                self.clear_selected_in_progress();
154            }
155            KeyCode::Char('r') => self.manual_refresh(),
156            _ => {}
157        }
158
159        false
160    }
161
162    pub fn resize(&mut self, width: u16, height: u16) {
163        self.terminal_width = width;
164        self.terminal_height = height;
165
166        // Update layout mode based on width
167        self.layout_mode = if width >= 120 {
168            LayoutMode::Full
169        } else if width >= 80 {
170            LayoutMode::Compact
171        } else {
172            LayoutMode::Mobile
173        };
174    }
175
176    fn next_view(&mut self) {
177        self.current_view = match self.current_view {
178            View::Kanban => View::Agents,
179            View::Agents => View::Logs,
180            View::Logs => View::Graph,
181            View::Graph => View::Kanban,
182        };
183    }
184
185    fn next_item(&mut self) {
186        if !self.features.is_empty() {
187            self.selected_index = (self.selected_index + 1) % self.features.len();
188        }
189    }
190
191    fn prev_item(&mut self) {
192        if !self.features.is_empty() {
193            self.selected_index = if self.selected_index == 0 {
194                self.features.len() - 1
195            } else {
196                self.selected_index - 1
197            };
198        }
199    }
200
201    fn cycle_theme(&mut self) {
202        self.theme = self.theme.next();
203        self.tui_theme = TuiTheme::from(&self.theme.colors());
204        self.add_log(format!("Theme changed to: {}", self.theme.name()));
205    }
206
207    pub fn add_log(&mut self, message: String) {
208        self.log_messages.push(message);
209        // Keep last 100 messages
210        if self.log_messages.len() > 100 {
211            self.log_messages.remove(0);
212        }
213    }
214
215    pub fn set_status(&mut self, message: String) {
216        self.status_message = Some((message, Instant::now()));
217    }
218
219    pub fn get_status_message(&self) -> Option<&str> {
220        if let Some((msg, timestamp)) = &self.status_message {
221            // Show status for 3 seconds
222            if timestamp.elapsed() < Duration::from_secs(3) {
223                return Some(msg.as_str());
224            }
225        }
226        None
227    }
228
229    pub fn should_refresh(&self) -> bool {
230        self.last_refresh.elapsed() >= self.refresh_interval
231    }
232
233    pub fn auto_refresh(&mut self) {
234        if self.should_refresh() {
235            self.load_from_database();
236        }
237    }
238
239    pub fn load_from_database(&mut self) {
240        self.last_refresh = Instant::now();
241
242        // Determine database path
243        let db_path = if let Some(ref path) = self.db_path {
244            path.clone()
245        } else {
246            // Try default path: ~/.agent-flow/flow.db
247            let home = match std::env::var("HOME") {
248                Ok(h) => PathBuf::from(h),
249                Err(_) => {
250                    self.db_error = Some("HOME environment variable not set".to_string());
251                    self.load_demo_data();
252                    return;
253                }
254            };
255            home.join(".agent-flow").join("flow.db")
256        };
257
258        // Try to open database
259        match Database::open(&db_path) {
260            Ok(db) => {
261                let db = Arc::new(db);
262                self.db = Some(Arc::clone(&db));
263                self.db_path = Some(db_path.clone());
264                self.db_error = None;
265
266                // Load features
267                if let Ok(conn) = db.writer().lock() {
268                    match flow_db::FeatureStore::get_all(&conn) {
269                        Ok(features) => {
270                            self.features = features;
271                            if self.selected_index >= self.features.len()
272                                && !self.features.is_empty()
273                            {
274                                self.selected_index = self.features.len() - 1;
275                            }
276                        }
277                        Err(e) => {
278                            self.db_error = Some(format!("Failed to load features: {e}"));
279                            self.add_log(format!("Error loading features: {e}"));
280                        }
281                    }
282
283                    // Load stats
284                    match flow_db::FeatureStore::get_stats(&conn) {
285                        Ok(stats) => {
286                            self.feature_stats = Some(stats);
287                        }
288                        Err(e) => {
289                            self.add_log(format!("Error loading stats: {e}"));
290                        }
291                    }
292                } else {
293                    self.db_error = Some("Failed to acquire database lock".to_string());
294                }
295
296                self.add_log(format!(
297                    "Loaded {} features from database",
298                    self.features.len()
299                ));
300            }
301            Err(e) => {
302                self.db_error = Some(format!("Failed to open database: {e}"));
303                self.add_log("No database found. Run 'flow init' to create one, or 'flow features add' to add features.".to_string());
304                self.load_demo_data();
305            }
306        }
307    }
308
309    fn manual_refresh(&mut self) {
310        self.load_from_database();
311        self.set_status("✓ Refreshed from database".to_string());
312    }
313
314    fn claim_selected_feature(&mut self) {
315        if self.features.is_empty() {
316            return;
317        }
318
319        let feature_id = self.features[self.selected_index].id;
320        let feature_name = self.features[self.selected_index].name.clone();
321
322        let db = match &self.db {
323            Some(db) => Arc::clone(db),
324            None => return,
325        };
326
327        let result = {
328            if let Ok(conn) = db.writer().lock() {
329                flow_db::FeatureStore::claim_and_get(&conn, feature_id)
330            } else {
331                return;
332            }
333        };
334
335        match result {
336            Ok(_) => {
337                self.set_status(format!("✓ Claimed \"{}\" (#{feature_id})", feature_name));
338                self.add_log(format!("Claimed feature #{feature_id}: {feature_name}"));
339                self.load_from_database();
340            }
341            Err(e) => {
342                self.set_status(format!("✗ Failed to claim: {e}"));
343                self.add_log(format!("Failed to claim feature #{feature_id}: {e}"));
344            }
345        }
346    }
347
348    fn mark_selected_passing(&mut self) {
349        if self.features.is_empty() {
350            return;
351        }
352
353        let feature_id = self.features[self.selected_index].id;
354        let feature_name = self.features[self.selected_index].name.clone();
355
356        let db = match &self.db {
357            Some(db) => Arc::clone(db),
358            None => return,
359        };
360
361        let result = {
362            if let Ok(conn) = db.writer().lock() {
363                flow_db::FeatureStore::mark_passing(&conn, feature_id)
364            } else {
365                return;
366            }
367        };
368
369        match result {
370            Ok(()) => {
371                self.set_status(format!("✓ Marked \"{}\" as passing", feature_name));
372                self.add_log(format!("Marked feature #{feature_id} as passing"));
373                self.load_from_database();
374            }
375            Err(e) => {
376                self.set_status(format!("✗ Failed to mark passing: {e}"));
377                self.add_log(format!(
378                    "Failed to mark feature #{feature_id} as passing: {e}"
379                ));
380            }
381        }
382    }
383
384    fn mark_selected_failing(&mut self) {
385        if self.features.is_empty() {
386            return;
387        }
388
389        let feature_id = self.features[self.selected_index].id;
390        let feature_name = self.features[self.selected_index].name.clone();
391
392        let db = match &self.db {
393            Some(db) => Arc::clone(db),
394            None => return,
395        };
396
397        let result = {
398            if let Ok(conn) = db.writer().lock() {
399                flow_db::FeatureStore::mark_failing(&conn, feature_id)
400            } else {
401                return;
402            }
403        };
404
405        match result {
406            Ok(()) => {
407                self.set_status(format!("✓ Marked \"{}\" as failing", feature_name));
408                self.add_log(format!("Marked feature #{feature_id} as failing"));
409                self.load_from_database();
410            }
411            Err(e) => {
412                self.set_status(format!("✗ Failed to mark failing: {e}"));
413                self.add_log(format!(
414                    "Failed to mark feature #{feature_id} as failing: {e}"
415                ));
416            }
417        }
418    }
419
420    fn clear_selected_in_progress(&mut self) {
421        if self.features.is_empty() {
422            return;
423        }
424
425        let feature_id = self.features[self.selected_index].id;
426        let feature_name = self.features[self.selected_index].name.clone();
427
428        let db = match &self.db {
429            Some(db) => Arc::clone(db),
430            None => return,
431        };
432
433        let result = {
434            if let Ok(conn) = db.writer().lock() {
435                flow_db::FeatureStore::clear_in_progress(&conn, feature_id)
436            } else {
437                return;
438            }
439        };
440
441        match result {
442            Ok(()) => {
443                self.set_status(format!("✓ Cleared in-progress for \"{}\"", feature_name));
444                self.add_log(format!("Cleared in-progress for feature #{feature_id}"));
445                self.load_from_database();
446            }
447            Err(e) => {
448                self.set_status(format!("✗ Failed to clear in-progress: {e}"));
449                self.add_log(format!(
450                    "Failed to clear in-progress for feature #{feature_id}: {e}"
451                ));
452            }
453        }
454    }
455
456    #[allow(clippy::too_many_lines)]
457    pub fn load_demo_data(&mut self) {
458        // Create demo features for testing
459        self.features = vec![
460            Feature {
461                id: 1,
462                priority: 100,
463                category: "Backend".to_string(),
464                name: "User Authentication".to_string(),
465                description: "Implement JWT-based authentication system".to_string(),
466                steps: vec![
467                    "Create user model".to_string(),
468                    "Implement JWT tokens".to_string(),
469                    "Add login/logout endpoints".to_string(),
470                ],
471                passes: false,
472                in_progress: false,
473                dependencies: vec![],
474                created_at: Some("2024-01-15T10:00:00Z".to_string()),
475                updated_at: Some("2024-01-15T10:00:00Z".to_string()),
476            },
477            Feature {
478                id: 2,
479                priority: 90,
480                category: "Frontend".to_string(),
481                name: "Login Page UI".to_string(),
482                description: "Create responsive login page".to_string(),
483                steps: vec![
484                    "Design mockup".to_string(),
485                    "Implement form".to_string(),
486                    "Add validation".to_string(),
487                ],
488                passes: false,
489                in_progress: false,
490                dependencies: vec![],
491                created_at: Some("2024-01-15T11:00:00Z".to_string()),
492                updated_at: Some("2024-01-15T11:00:00Z".to_string()),
493            },
494            Feature {
495                id: 3,
496                priority: 80,
497                category: "Backend".to_string(),
498                name: "API Rate Limiting".to_string(),
499                description: "Add rate limiting middleware".to_string(),
500                steps: vec![
501                    "Research solutions".to_string(),
502                    "Implement middleware".to_string(),
503                ],
504                passes: false,
505                in_progress: true,
506                dependencies: vec![1],
507                created_at: Some("2024-01-16T09:00:00Z".to_string()),
508                updated_at: Some("2024-01-16T14:30:00Z".to_string()),
509            },
510            Feature {
511                id: 4,
512                priority: 70,
513                category: "Testing".to_string(),
514                name: "Integration Tests".to_string(),
515                description: "Write comprehensive test suite".to_string(),
516                steps: vec![
517                    "Setup test framework".to_string(),
518                    "Write API tests".to_string(),
519                    "Add CI/CD integration".to_string(),
520                ],
521                passes: false,
522                in_progress: true,
523                dependencies: vec![1, 3],
524                created_at: Some("2024-01-16T10:00:00Z".to_string()),
525                updated_at: Some("2024-01-16T15:00:00Z".to_string()),
526            },
527            Feature {
528                id: 5,
529                priority: 60,
530                category: "Frontend".to_string(),
531                name: "Dashboard".to_string(),
532                description: "User dashboard with stats".to_string(),
533                steps: vec![
534                    "Create layout".to_string(),
535                    "Add charts".to_string(),
536                    "Implement data fetching".to_string(),
537                ],
538                passes: true,
539                in_progress: false,
540                dependencies: vec![1, 2],
541                created_at: Some("2024-01-14T08:00:00Z".to_string()),
542                updated_at: Some("2024-01-15T17:00:00Z".to_string()),
543            },
544            Feature {
545                id: 6,
546                priority: 50,
547                category: "Backend".to_string(),
548                name: "Email Notifications".to_string(),
549                description: "Send email notifications for events".to_string(),
550                steps: vec![
551                    "Setup email service".to_string(),
552                    "Create templates".to_string(),
553                ],
554                passes: true,
555                in_progress: false,
556                dependencies: vec![1],
557                created_at: Some("2024-01-13T14:00:00Z".to_string()),
558                updated_at: Some("2024-01-14T16:00:00Z".to_string()),
559            },
560            Feature {
561                id: 7,
562                priority: 40,
563                category: "DevOps".to_string(),
564                name: "Docker Setup".to_string(),
565                description: "Containerize application".to_string(),
566                steps: vec![
567                    "Create Dockerfile".to_string(),
568                    "Setup docker-compose".to_string(),
569                ],
570                passes: true,
571                in_progress: false,
572                dependencies: vec![],
573                created_at: Some("2024-01-12T09:00:00Z".to_string()),
574                updated_at: Some("2024-01-13T11:00:00Z".to_string()),
575            },
576            Feature {
577                id: 8,
578                priority: 30,
579                category: "Frontend".to_string(),
580                name: "Settings Page".to_string(),
581                description: "User settings and preferences".to_string(),
582                steps: vec![
583                    "Design UI".to_string(),
584                    "Implement form handling".to_string(),
585                ],
586                passes: true,
587                in_progress: false,
588                dependencies: vec![1, 2],
589                created_at: Some("2024-01-11T10:00:00Z".to_string()),
590                updated_at: Some("2024-01-12T15:00:00Z".to_string()),
591            },
592            Feature {
593                id: 9,
594                priority: 20,
595                category: "Documentation".to_string(),
596                name: "API Documentation".to_string(),
597                description: "Complete OpenAPI specs".to_string(),
598                steps: vec!["Write specs".to_string(), "Generate docs".to_string()],
599                passes: true,
600                in_progress: false,
601                dependencies: vec![1, 3],
602                created_at: Some("2024-01-10T13:00:00Z".to_string()),
603                updated_at: Some("2024-01-11T16:00:00Z".to_string()),
604            },
605            Feature {
606                id: 10,
607                priority: 10,
608                category: "Security".to_string(),
609                name: "Security Audit".to_string(),
610                description: "Comprehensive security review".to_string(),
611                steps: vec![
612                    "Run vulnerability scan".to_string(),
613                    "Review dependencies".to_string(),
614                    "Fix issues".to_string(),
615                ],
616                passes: true,
617                in_progress: false,
618                dependencies: vec![1, 3, 6],
619                created_at: Some("2024-01-09T08:00:00Z".to_string()),
620                updated_at: Some("2024-01-10T18:00:00Z".to_string()),
621            },
622        ];
623
624        self.add_log("Loaded 10 demo features".to_string());
625        self.add_log(format!("Current theme: {}", self.theme.name()));
626        self.add_log("Press ? for help".to_string());
627    }
628}