Skip to main content

graphrag_cli/
app.rs

1//! Main application logic and event loop
2
3use crate::{
4    action::{Action, QueryExplainedPayload, QueryMode, SourceRef, StatusType},
5    commands::SlashCommand,
6    handlers::{FileOperations, GraphRAGHandler},
7    query_history::{QueryEntry, QueryHistory},
8    theme::Theme,
9    tui::{Event, Tui},
10    ui::{HelpOverlay, InfoPanel, QueryInput, RawResultsViewer, ResultsViewer, StatusBar},
11    workspace::{WorkspaceManager, WorkspaceMetadata},
12};
13use chrono::Utc;
14use color_eyre::eyre::Result;
15use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
16use ratatui::layout::{Constraint, Direction, Layout};
17use std::{path::PathBuf, time::Instant};
18use tokio::sync::mpsc::{self, UnboundedReceiver, UnboundedSender};
19
20/// Main application state
21pub struct App {
22    /// Should the app quit?
23    should_quit: bool,
24    /// TUI instance
25    tui: Tui,
26    /// GraphRAG handler
27    graphrag: GraphRAGHandler,
28    /// Action sender
29    action_tx: UnboundedSender<Action>,
30    /// Action receiver
31    action_rx: UnboundedReceiver<Action>,
32    /// Query input component
33    query_input: QueryInput,
34    /// Results viewer component (LLM-processed answer)
35    results_viewer: ResultsViewer,
36    /// Raw results viewer component (search results before LLM)
37    raw_results_viewer: RawResultsViewer,
38    /// Info panel component
39    info_panel: InfoPanel,
40    /// Status bar component
41    status_bar: StatusBar,
42    /// Help overlay component
43    help_overlay: HelpOverlay,
44    /// Query history
45    query_history: QueryHistory,
46    /// Workspace manager
47    #[allow(dead_code)]
48    workspace_manager: WorkspaceManager,
49    /// Current workspace metadata
50    #[allow(dead_code)]
51    workspace_metadata: Option<WorkspaceMetadata>,
52    /// Configuration file path
53    config_path: Option<PathBuf>,
54    /// Active query mode (Ask / Explain / Reason)
55    query_mode: QueryMode,
56    /// Currently focused pane: 0=input, 1=results, 2=raw, 3=info
57    focused_pane: u8,
58    /// Theme
59    #[allow(dead_code)]
60    theme: Theme,
61}
62
63impl App {
64    /// Create a new application
65    pub fn new(config_path: Option<PathBuf>, _workspace: Option<String>) -> Result<Self> {
66        let (action_tx, action_rx) = mpsc::unbounded_channel();
67
68        let workspace_manager = WorkspaceManager::new()?;
69
70        Ok(Self {
71            should_quit: false,
72            tui: Tui::new()?,
73            graphrag: GraphRAGHandler::new(),
74            action_tx,
75            action_rx,
76            query_input: QueryInput::new(),
77            results_viewer: ResultsViewer::new(),
78            raw_results_viewer: RawResultsViewer::new(),
79            info_panel: InfoPanel::new(),
80            status_bar: StatusBar::new(),
81            help_overlay: HelpOverlay::new(),
82            query_history: QueryHistory::new(),
83            workspace_manager,
84            workspace_metadata: None,
85            config_path,
86            query_mode: QueryMode::default(),
87            focused_pane: 0,
88            theme: Theme::default(),
89        })
90    }
91
92    /// Set focused pane and update all component focus states.
93    /// pane: 0=input, 1=results, 2=raw_results, 3=info_panel
94    fn set_focus(&mut self, pane: u8) {
95        self.focused_pane = pane;
96        self.query_input.set_focused(pane == 0);
97        self.results_viewer.set_focused(pane == 1);
98        self.raw_results_viewer.set_focused(pane == 2);
99        self.info_panel.set_focused(pane == 3);
100    }
101
102    /// Run the application
103    pub async fn run(&mut self) -> Result<()> {
104        // Enter TUI mode
105        self.tui.enter()?;
106
107        // Load config if provided
108        if let Some(ref config_path) = self.config_path.clone() {
109            self.action_tx
110                .send(Action::LoadConfig(config_path.clone()))?;
111        } else {
112            self.action_tx.send(Action::SetStatus(
113                StatusType::Warning,
114                "No config loaded. Use /config <file> to load configuration".to_string(),
115            ))?;
116        }
117
118        // Main event loop
119        while !self.should_quit {
120            // Get next event
121            if let Some(event) = self.tui.next().await {
122                self.handle_event(event).await?;
123            }
124
125            // Process all pending actions
126            while let Ok(action) = self.action_rx.try_recv() {
127                self.update(action).await?;
128
129                if self.should_quit {
130                    break;
131                }
132            }
133
134            // Render
135            let query_input = &mut self.query_input;
136            let results_viewer = &mut self.results_viewer;
137            let raw_results_viewer = &mut self.raw_results_viewer;
138            let info_panel = &mut self.info_panel;
139            let status_bar = &mut self.status_bar;
140            let help_overlay = &mut self.help_overlay;
141
142            self.tui.terminal.draw(|f| {
143                use crate::ui::components::Component;
144
145                // Main vertical layout: Input + Content + Status
146                let main_chunks = Layout::default()
147                    .direction(Direction::Vertical)
148                    .constraints([
149                        Constraint::Length(3), // Query input
150                        Constraint::Min(0),    // Content area (results + info)
151                        Constraint::Length(3), // Status bar
152                    ])
153                    .split(f.area());
154
155                // Horizontal split: Left side (70%) and Info Panel (30%)
156                let content_chunks = Layout::default()
157                    .direction(Direction::Horizontal)
158                    .constraints([
159                        Constraint::Percentage(70), // Left side (results + raw results)
160                        Constraint::Percentage(30), // Info panel
161                    ])
162                    .split(main_chunks[1]);
163
164                // Vertical split for left side: Results Viewer (top) and Raw Results (bottom)
165                let left_chunks = Layout::default()
166                    .direction(Direction::Vertical)
167                    .constraints([
168                        Constraint::Percentage(60), // Results viewer (LLM answer)
169                        Constraint::Percentage(40), // Raw results viewer (search results)
170                    ])
171                    .split(content_chunks[0]);
172
173                // Render components
174                query_input.render(f, main_chunks[0]);
175                results_viewer.render(f, left_chunks[0]);
176                raw_results_viewer.render(f, left_chunks[1]);
177                info_panel.render(f, content_chunks[1]);
178                status_bar.render(f, main_chunks[2]);
179
180                // Help overlay (on top if visible)
181                if help_overlay.is_visible() {
182                    help_overlay.render(f, f.area());
183                }
184            })?;
185        }
186
187        // Exit TUI mode
188        self.tui.exit()?;
189
190        Ok(())
191    }
192
193    /// Handle terminal events
194    async fn handle_event(&mut self, event: Event) -> Result<()> {
195        match event {
196            Event::Crossterm(crossterm_event) => {
197                if let crossterm::event::Event::Key(key) = crossterm_event {
198                    self.handle_key_event(key)?;
199                }
200            },
201            Event::Tick => {
202                // Periodic update
203            },
204            Event::Render => {
205                // Render is handled in main loop
206            },
207            Event::Resize(w, h) => {
208                self.action_tx.send(Action::Resize(w, h))?;
209            },
210        }
211
212        Ok(())
213    }
214
215    /// Handle keyboard input
216    fn handle_key_event(&mut self, key: KeyEvent) -> Result<()> {
217        // Help overlay has priority
218        if self.help_overlay.is_visible() {
219            if matches!(key.code, KeyCode::Char('?') | KeyCode::Esc) {
220                self.action_tx.send(Action::ToggleHelp)?;
221            }
222            return Ok(());
223        }
224
225        // Global shortcuts (check these first, before passing to input)
226        match (key.code, key.modifiers) {
227            // Quit
228            (KeyCode::Char('c'), KeyModifiers::CONTROL) => {
229                self.action_tx.send(Action::Quit)?;
230                return Ok(());
231            },
232            // Help — works regardless of SHIFT state (? is Shift+/ on US keyboards;
233            // some terminals send SHIFT, others send NONE). Also map Ctrl+H for safety.
234            (KeyCode::Char('?'), _) | (KeyCode::Char('h'), KeyModifiers::CONTROL) => {
235                self.action_tx.send(Action::ToggleHelp)?;
236                return Ok(());
237            },
238            // Ctrl+N: cycle focus forward through panels
239            // Input(0) → Results(1) → Raw(2) → Info(3) → Input(0)
240            // When InfoPanel is focused, Ctrl+N instead cycles its internal tabs
241            (KeyCode::Char('n'), KeyModifiers::CONTROL) => {
242                if self.focused_pane == 3 {
243                    // InfoPanel focused: cycle internal tabs
244                    self.action_tx.send(Action::NextTab)?;
245                } else {
246                    self.action_tx.send(Action::NextPane)?;
247                }
248                return Ok(());
249            },
250            // Ctrl+P: cycle focus backward
251            (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
252                self.action_tx.send(Action::PreviousPane)?;
253                return Ok(());
254            },
255            // Ctrl+1/2/3/4: direct focus jump
256            (KeyCode::Char('1'), KeyModifiers::CONTROL) => {
257                self.set_focus(0);
258                self.action_tx.send(Action::FocusQueryInput)?;
259                return Ok(());
260            },
261            (KeyCode::Char('2'), KeyModifiers::CONTROL) => {
262                self.set_focus(1);
263                self.action_tx.send(Action::FocusResultsViewer)?;
264                return Ok(());
265            },
266            (KeyCode::Char('3'), KeyModifiers::CONTROL) => {
267                self.set_focus(2);
268                self.action_tx.send(Action::FocusRawResultsViewer)?;
269                return Ok(());
270            },
271            (KeyCode::Char('4'), KeyModifiers::CONTROL) => {
272                self.set_focus(3);
273                self.action_tx.send(Action::FocusInfoPanel)?;
274                return Ok(());
275            },
276            // Esc: return focus to input
277            (KeyCode::Esc, KeyModifiers::NONE) => {
278                self.set_focus(0);
279                self.action_tx.send(Action::FocusQueryInput)?;
280                return Ok(());
281            },
282            // Arrow keys for scrolling when a viewer panel is focused (not input)
283            (KeyCode::Down, KeyModifiers::NONE) if self.focused_pane != 0 => {
284                self.action_tx.send(Action::ScrollDown)?;
285                return Ok(());
286            },
287            (KeyCode::Up, KeyModifiers::NONE) if self.focused_pane != 0 => {
288                self.action_tx.send(Action::ScrollUp)?;
289                return Ok(());
290            },
291            // Alt+Up/Down for scrolling anywhere (even in input)
292            (KeyCode::Down, KeyModifiers::ALT) => {
293                self.action_tx.send(Action::ScrollDown)?;
294                return Ok(());
295            },
296            (KeyCode::Up, KeyModifiers::ALT) => {
297                self.action_tx.send(Action::ScrollUp)?;
298                return Ok(());
299            },
300            _ => {},
301        }
302
303        // Pass input to query input component first (it has priority for typing)
304        if let Some(action) = self.query_input.handle_key(key) {
305            self.action_tx.send(action)?;
306            return Ok(());
307        }
308
309        // Scrolling keybindings (only activated if input didn't consume the key)
310        match (key.code, key.modifiers) {
311            // Scrolling with j/k (vim-style) - only when not typing in input
312            (KeyCode::Char('j'), KeyModifiers::NONE) => {
313                self.action_tx.send(Action::ScrollDown)?;
314            },
315            (KeyCode::Char('k'), KeyModifiers::NONE) => {
316                self.action_tx.send(Action::ScrollUp)?;
317            },
318            // Scrolling with PageDown / PageUp
319            (KeyCode::PageDown, KeyModifiers::NONE)
320            | (KeyCode::Char('d'), KeyModifiers::CONTROL) => {
321                self.action_tx.send(Action::ScrollPageDown)?;
322            },
323            (KeyCode::PageUp, KeyModifiers::NONE) | (KeyCode::Char('u'), KeyModifiers::CONTROL) => {
324                self.action_tx.send(Action::ScrollPageUp)?;
325            },
326            // Scrolling with Home/End
327            (KeyCode::Home, KeyModifiers::NONE) => {
328                self.action_tx.send(Action::ScrollToTop)?;
329            },
330            (KeyCode::End, KeyModifiers::NONE) => {
331                self.action_tx.send(Action::ScrollToBottom)?;
332            },
333            _ => {},
334        }
335
336        Ok(())
337    }
338
339    /// Update application state based on action
340    async fn update(&mut self, action: Action) -> Result<()> {
341        use crate::ui::components::Component;
342
343        // Update components
344        self.query_input.handle_action(&action);
345        self.results_viewer.handle_action(&action);
346        self.raw_results_viewer.handle_action(&action);
347        self.info_panel.handle_action(&action);
348        self.status_bar.handle_action(&action);
349        self.help_overlay.handle_action(&action);
350
351        // Handle actions
352        match action {
353            Action::Quit => {
354                self.should_quit = true;
355            },
356            Action::LoadConfig(path) => {
357                self.handle_load_config(path).await?;
358            },
359            Action::ExecuteQuery(query) => {
360                // Route by current mode
361                match self.query_mode {
362                    QueryMode::Ask => self.handle_execute_query(query).await?,
363                    QueryMode::Explain => self.handle_execute_explained_query(query).await?,
364                    QueryMode::Reason => self.handle_execute_reason_query(query).await?,
365                }
366            },
367            Action::ExecuteExplainedQuery(query) => {
368                self.handle_execute_explained_query(query).await?;
369            },
370            Action::ExecuteReasonQuery(query) => {
371                self.handle_execute_reason_query(query).await?;
372            },
373            Action::SetQueryMode(mode) => {
374                self.query_mode = mode;
375                // status_bar already updated via handle_action above
376            },
377            Action::NextPane => {
378                let next = (self.focused_pane + 1) % 4;
379                self.set_focus(next);
380            },
381            Action::PreviousPane => {
382                let prev = (self.focused_pane + 3) % 4;
383                self.set_focus(prev);
384            },
385            Action::ExecuteSlashCommand(cmd) => {
386                self.handle_slash_command(cmd).await?;
387            },
388            // QueryExplainedSuccess is handled by components (results_viewer, info_panel)
389            _ => {},
390        }
391
392        Ok(())
393    }
394
395    /// Handle loading configuration
396    async fn handle_load_config(&mut self, path: PathBuf) -> Result<()> {
397        self.action_tx.send(Action::StartProgress(
398            "Loading configuration...".to_string(),
399        ))?;
400
401        match crate::config::load_config(&path).await {
402            Ok(config) => {
403                self.graphrag.initialize(config).await?;
404                self.config_path = Some(path.clone());
405
406                self.action_tx.send(Action::StopProgress)?;
407                self.action_tx.send(Action::ConfigLoaded(format!(
408                    "Configuration loaded from {}",
409                    path.display()
410                )))?;
411                self.action_tx.send(Action::SetStatus(
412                    StatusType::Success,
413                    "Configuration loaded successfully".to_string(),
414                ))?;
415
416                // Update stats
417                self.update_stats().await;
418            },
419            Err(e) => {
420                self.action_tx.send(Action::StopProgress)?;
421                self.action_tx.send(Action::ConfigLoadError(format!(
422                    "Failed to load config: {}",
423                    e
424                )))?;
425                self.action_tx.send(Action::SetStatus(
426                    StatusType::Error,
427                    format!("Config load failed: {}", e),
428                ))?;
429            },
430        }
431
432        self.set_focus(0);
433        Ok(())
434    }
435
436    /// Handle executing a query
437    async fn handle_execute_query(&mut self, query: String) -> Result<()> {
438        if !self.graphrag.is_initialized().await {
439            self.action_tx.send(Action::SetStatus(
440                StatusType::Error,
441                "GraphRAG not initialized. Load a config first with /config".to_string(),
442            ))?;
443            return Ok(());
444        }
445
446        // Show "Generating answer..." message in Results Viewer
447        self.results_viewer.set_content(vec![
448            "🤖 Generating Answer...".to_string(),
449            "━".repeat(50),
450            String::new(),
451            format!("Query: {}", query),
452            String::new(),
453            "⟳ Searching knowledge graph...".to_string(),
454            "⟳ Processing results with LLM...".to_string(),
455            String::new(),
456            "Please wait...".to_string(),
457        ]);
458
459        self.action_tx
460            .send(Action::StartProgress(format!("Executing query: {}", query)))?;
461
462        let start = Instant::now();
463
464        match self.graphrag.query_with_raw(&query).await {
465            Ok((answer, raw_results)) => {
466                let duration = start.elapsed();
467
468                // Add to query history
469                let entry = QueryEntry {
470                    query: query.clone(),
471                    timestamp: Utc::now(),
472                    duration_ms: duration.as_millis(),
473                    results_count: raw_results.len(),
474                    results_preview: vec![answer[..answer.len().min(200)].to_string()],
475                };
476
477                self.query_history.add_entry(entry.clone());
478
479                // Update info panel with query history
480                self.info_panel
481                    .add_query(entry.query, entry.duration_ms, entry.results_count);
482
483                // Update raw results viewer with search results
484                let mut raw_display = vec![
485                    format!("🔍 Raw Search Results ({})", raw_results.len()),
486                    "━".repeat(50),
487                    String::new(),
488                ];
489                raw_display.extend(raw_results);
490                self.raw_results_viewer.set_content(raw_display);
491
492                self.action_tx.send(Action::StopProgress)?;
493                self.action_tx.send(Action::QuerySuccess(answer))?;
494                self.action_tx.send(Action::SetStatus(
495                    StatusType::Success,
496                    format!("Query completed in {}ms", duration.as_millis()),
497                ))?;
498            },
499            Err(e) => {
500                self.action_tx.send(Action::StopProgress)?;
501                self.action_tx
502                    .send(Action::QueryError(format!("Query failed: {}", e)))?;
503                self.action_tx.send(Action::SetStatus(
504                    StatusType::Error,
505                    format!("Query error: {}", e),
506                ))?;
507            },
508        }
509
510        Ok(())
511    }
512
513    /// Handle executing a query in Explain mode (returns confidence + sources)
514    async fn handle_execute_explained_query(&mut self, query: String) -> Result<()> {
515        if !self.graphrag.is_initialized().await {
516            self.action_tx.send(Action::SetStatus(
517                StatusType::Error,
518                "GraphRAG not initialized. Load a config first with /config".to_string(),
519            ))?;
520            return Ok(());
521        }
522
523        self.results_viewer.set_content(vec![
524            "## Generating Answer (EXPLAIN mode)…".to_string(),
525            String::new(),
526            format!("**Query:** {}", query),
527            String::new(),
528            "- Searching knowledge graph…".to_string(),
529            "- Computing confidence and source references…".to_string(),
530        ]);
531
532        self.action_tx
533            .send(Action::StartProgress(format!("EXPLAIN query: {}", query)))?;
534
535        let start = Instant::now();
536
537        match self.graphrag.query_explained(&query).await {
538            Ok(result) => {
539                let duration = start.elapsed();
540
541                let entry = QueryEntry {
542                    query: query.clone(),
543                    timestamp: Utc::now(),
544                    duration_ms: duration.as_millis(),
545                    results_count: result.sources.len(),
546                    results_preview: vec![result.answer[..result.answer.len().min(200)].to_string()],
547                };
548                self.query_history.add_entry(entry.clone());
549                self.info_panel
550                    .add_query(entry.query, entry.duration_ms, entry.results_count);
551
552                // Populate raw results with source list
553                let mut raw_display = vec![
554                    format!(
555                        "Sources ({}) | Confidence: {:.0}%",
556                        result.sources.len(),
557                        result.confidence * 100.0
558                    ),
559                    "━".repeat(50),
560                    String::new(),
561                ];
562                for (i, src) in result.sources.iter().enumerate() {
563                    raw_display.push(format!(
564                        "{}. [score: {:.2}] {}",
565                        i + 1,
566                        src.relevance_score,
567                        src.id
568                    ));
569                    let excerpt = &src.excerpt[..src.excerpt.len().min(120)];
570                    raw_display.push(format!("   {}", excerpt));
571                    raw_display.push(String::new());
572                }
573                self.raw_results_viewer.set_content(raw_display);
574
575                // Build payload for components
576                let payload = QueryExplainedPayload {
577                    answer: result.answer.clone(),
578                    confidence: result.confidence,
579                    sources: result
580                        .sources
581                        .iter()
582                        .map(|s| SourceRef {
583                            id: s.id.clone(),
584                            excerpt: s.excerpt.clone(),
585                            relevance_score: s.relevance_score,
586                        })
587                        .collect(),
588                };
589
590                self.action_tx.send(Action::StopProgress)?;
591                self.action_tx
592                    .send(Action::QueryExplainedSuccess(Box::new(payload)))?;
593                self.action_tx.send(Action::SetStatus(
594                    StatusType::Success,
595                    format!(
596                        "EXPLAIN query done in {}ms | confidence: {:.0}%",
597                        duration.as_millis(),
598                        result.confidence * 100.0
599                    ),
600                ))?;
601            },
602            Err(e) => {
603                self.action_tx.send(Action::StopProgress)?;
604                self.action_tx
605                    .send(Action::QueryError(format!("Query failed: {}", e)))?;
606                self.action_tx.send(Action::SetStatus(
607                    StatusType::Error,
608                    format!("Query error: {}", e),
609                ))?;
610            },
611        }
612
613        self.set_focus(0);
614        Ok(())
615    }
616
617    /// Handle executing a query in Reason mode (query decomposition)
618    async fn handle_execute_reason_query(&mut self, query: String) -> Result<()> {
619        if !self.graphrag.is_initialized().await {
620            self.action_tx.send(Action::SetStatus(
621                StatusType::Error,
622                "GraphRAG not initialized. Load a config first with /config".to_string(),
623            ))?;
624            return Ok(());
625        }
626
627        self.results_viewer.set_content(vec![
628            "## Generating Answer (REASON mode)…".to_string(),
629            String::new(),
630            format!("**Query:** {}", query),
631            String::new(),
632            "- Decomposing query into sub-questions…".to_string(),
633            "- Gathering context for each sub-question…".to_string(),
634            "- Synthesizing comprehensive answer…".to_string(),
635        ]);
636
637        self.action_tx
638            .send(Action::StartProgress(format!("REASON query: {}", query)))?;
639
640        let start = Instant::now();
641
642        match self.graphrag.query_with_reasoning(&query).await {
643            Ok(answer) => {
644                let duration = start.elapsed();
645
646                let entry = QueryEntry {
647                    query: query.clone(),
648                    timestamp: Utc::now(),
649                    duration_ms: duration.as_millis(),
650                    results_count: 0,
651                    results_preview: vec![answer[..answer.len().min(200)].to_string()],
652                };
653                self.query_history.add_entry(entry.clone());
654                self.info_panel
655                    .add_query(entry.query, entry.duration_ms, entry.results_count);
656
657                self.raw_results_viewer.set_content(vec![
658                    "REASON mode — query decomposition active".to_string(),
659                    "━".repeat(50),
660                    String::new(),
661                    "Sub-queries were generated and answered individually.".to_string(),
662                    "The result above synthesizes all sub-answers.".to_string(),
663                ]);
664
665                self.action_tx.send(Action::StopProgress)?;
666                self.action_tx.send(Action::QuerySuccess(answer))?;
667                self.action_tx.send(Action::SetStatus(
668                    StatusType::Success,
669                    format!("REASON query done in {}ms", duration.as_millis()),
670                ))?;
671            },
672            Err(e) => {
673                self.action_tx.send(Action::StopProgress)?;
674                self.action_tx
675                    .send(Action::QueryError(format!("Query failed: {}", e)))?;
676                self.action_tx.send(Action::SetStatus(
677                    StatusType::Error,
678                    format!("Query error: {}", e),
679                ))?;
680            },
681        }
682
683        self.set_focus(0);
684        Ok(())
685    }
686
687    /// Handle slash command
688    async fn handle_slash_command(&mut self, cmd_str: String) -> Result<()> {
689        match SlashCommand::parse(&cmd_str) {
690            Ok(cmd) => match cmd {
691                SlashCommand::Config(path) => {
692                    let expanded = FileOperations::expand_tilde(&path);
693                    self.action_tx.send(Action::LoadConfig(expanded))?;
694                },
695                SlashCommand::Load(path, rebuild) => {
696                    self.handle_load_document_with_rebuild(path, rebuild)
697                        .await?;
698                },
699                SlashCommand::Clear => {
700                    self.handle_clear_graph().await?;
701                },
702                SlashCommand::Rebuild => {
703                    self.handle_rebuild_graph().await?;
704                },
705                SlashCommand::Stats => {
706                    self.handle_show_stats().await?;
707                },
708                SlashCommand::Entities(filter) => {
709                    self.handle_list_entities(filter).await?;
710                },
711                SlashCommand::Workspace(name) => {
712                    self.handle_load_workspace(name).await?;
713                },
714                SlashCommand::WorkspaceList => {
715                    self.handle_list_workspaces().await?;
716                },
717                SlashCommand::WorkspaceSave(name) => {
718                    self.handle_save_workspace(name).await?;
719                },
720                SlashCommand::WorkspaceDelete(name) => {
721                    self.handle_delete_workspace(name).await?;
722                },
723                SlashCommand::Reason(query) => {
724                    self.handle_execute_reason_query(query).await?;
725                },
726                SlashCommand::Mode(mode_str) => {
727                    let mode = match mode_str.as_str() {
728                        "ask" => QueryMode::Ask,
729                        "explain" => QueryMode::Explain,
730                        "reason" => QueryMode::Reason,
731                        other => {
732                            self.action_tx.send(Action::SetStatus(
733                                StatusType::Error,
734                                format!("Unknown mode '{}'. Use: ask | explain | reason", other),
735                            ))?;
736                            return Ok(());
737                        },
738                    };
739                    self.query_mode = mode;
740                    self.action_tx.send(Action::SetQueryMode(mode))?;
741                    self.action_tx.send(Action::SetStatus(
742                        StatusType::Success,
743                        format!("Query mode set to: {}", mode.label()),
744                    ))?;
745                },
746                SlashCommand::ConfigShow => {
747                    self.handle_config_show().await?;
748                },
749                SlashCommand::Export(path) => {
750                    self.handle_export(path).await?;
751                },
752                SlashCommand::Help => {
753                    let help_text = SlashCommand::help_text();
754                    self.results_viewer
755                        .set_content(help_text.lines().map(|s| s.to_string()).collect());
756                },
757            },
758            Err(e) => {
759                self.action_tx.send(Action::SetStatus(
760                    StatusType::Error,
761                    format!("Command error: {}", e),
762                ))?;
763            },
764        }
765
766        // Always return focus to input after any slash command completes
767        self.set_focus(0);
768
769        Ok(())
770    }
771
772    /// Handle loading a document with rebuild option
773    async fn handle_load_document_with_rebuild(
774        &mut self,
775        path: PathBuf,
776        rebuild: bool,
777    ) -> Result<()> {
778        if !self.graphrag.is_initialized().await {
779            self.action_tx.send(Action::SetStatus(
780                StatusType::Error,
781                "GraphRAG not initialized. Load a config first".to_string(),
782            ))?;
783            return Ok(());
784        }
785
786        let expanded = FileOperations::expand_tilde(&path);
787
788        let progress_msg = if rebuild {
789            format!("Loading document (rebuild): {}", expanded.display())
790        } else {
791            format!("Loading document: {}", expanded.display())
792        };
793
794        self.action_tx.send(Action::StartProgress(progress_msg))?;
795
796        match self
797            .graphrag
798            .load_document_with_options(&expanded, rebuild)
799            .await
800        {
801            Ok(message) => {
802                self.action_tx.send(Action::StopProgress)?;
803                self.action_tx
804                    .send(Action::DocumentLoaded(message.clone()))?;
805                self.action_tx
806                    .send(Action::SetStatus(StatusType::Success, message))?;
807
808                self.update_stats().await;
809            },
810            Err(e) => {
811                self.action_tx.send(Action::StopProgress)?;
812                self.action_tx.send(Action::SetStatus(
813                    StatusType::Error,
814                    format!("Failed to load document: {}", e),
815                ))?;
816            },
817        }
818
819        Ok(())
820    }
821
822    /// Handle clearing the knowledge graph
823    async fn handle_clear_graph(&mut self) -> Result<()> {
824        if !self.graphrag.is_initialized().await {
825            self.action_tx.send(Action::SetStatus(
826                StatusType::Error,
827                "GraphRAG not initialized. Load a config first".to_string(),
828            ))?;
829            return Ok(());
830        }
831
832        self.action_tx.send(Action::StartProgress(
833            "Clearing knowledge graph...".to_string(),
834        ))?;
835
836        match self.graphrag.clear_graph().await {
837            Ok(message) => {
838                self.action_tx.send(Action::StopProgress)?;
839                self.action_tx
840                    .send(Action::SetStatus(StatusType::Success, message.clone()))?;
841
842                // Display confirmation in results viewer
843                self.results_viewer.set_content(vec![
844                    "✓ Knowledge Graph Cleared".to_string(),
845                    "━".repeat(50),
846                    String::new(),
847                    message,
848                    String::new(),
849                    "The knowledge graph has been cleared.".to_string(),
850                    "All entities and relationships have been removed.".to_string(),
851                    "Documents and chunks are preserved.".to_string(),
852                    String::new(),
853                    "Use /rebuild to rebuild from loaded documents.".to_string(),
854                    "Or use /load <file> to load a new document.".to_string(),
855                ]);
856
857                self.update_stats().await;
858            },
859            Err(e) => {
860                self.action_tx.send(Action::StopProgress)?;
861                self.action_tx.send(Action::SetStatus(
862                    StatusType::Error,
863                    format!("Failed to clear graph: {}", e),
864                ))?;
865            },
866        }
867
868        Ok(())
869    }
870
871    /// Handle rebuilding the knowledge graph
872    async fn handle_rebuild_graph(&mut self) -> Result<()> {
873        if !self.graphrag.is_initialized().await {
874            self.action_tx.send(Action::SetStatus(
875                StatusType::Error,
876                "GraphRAG not initialized. Load a config first".to_string(),
877            ))?;
878            return Ok(());
879        }
880
881        self.action_tx.send(Action::StartProgress(
882            "Rebuilding knowledge graph...".to_string(),
883        ))?;
884
885        match self.graphrag.rebuild_graph().await {
886            Ok(message) => {
887                self.action_tx.send(Action::StopProgress)?;
888                self.action_tx
889                    .send(Action::SetStatus(StatusType::Success, message.clone()))?;
890
891                // Display confirmation in results viewer
892                self.results_viewer.set_content(vec![
893                    "✓ Knowledge Graph Rebuilt".to_string(),
894                    "━".repeat(50),
895                    String::new(),
896                    message,
897                    String::new(),
898                    "The knowledge graph has been rebuilt from loaded documents.".to_string(),
899                    "All entities and relationships have been re-extracted.".to_string(),
900                    String::new(),
901                    "You can now query the updated graph.".to_string(),
902                ]);
903
904                self.update_stats().await;
905            },
906            Err(e) => {
907                self.action_tx.send(Action::StopProgress)?;
908                self.action_tx.send(Action::SetStatus(
909                    StatusType::Error,
910                    format!("Failed to rebuild graph: {}", e),
911                ))?;
912            },
913        }
914
915        Ok(())
916    }
917
918    /// Handle showing statistics
919    async fn handle_show_stats(&mut self) -> Result<()> {
920        if let Some(stats) = self.graphrag.get_stats().await {
921            let lines = vec![
922                "📊 Knowledge Graph Statistics".to_string(),
923                "━".repeat(50),
924                format!("Entities:      {}", stats.entities),
925                format!("Relationships: {}", stats.relationships),
926                format!("Documents:     {}", stats.documents),
927                format!("Chunks:        {}", stats.chunks),
928                String::new(),
929                format!("Total Queries: {}", self.query_history.total_queries()),
930            ];
931
932            self.results_viewer.set_content(lines);
933            self.action_tx.send(Action::SetStatus(
934                StatusType::Info,
935                "Stats displayed".to_string(),
936            ))?;
937        } else {
938            self.action_tx.send(Action::SetStatus(
939                StatusType::Warning,
940                "No graph loaded yet".to_string(),
941            ))?;
942        }
943
944        Ok(())
945    }
946
947    /// Handle listing entities
948    async fn handle_list_entities(&mut self, filter: Option<String>) -> Result<()> {
949        match self.graphrag.get_entities(filter.as_deref()).await {
950            Ok(entities) => {
951                let mut lines = vec![
952                    format!(
953                        "🔍 Entities{}",
954                        if filter.is_some() {
955                            format!(" (filtered by '{}')", filter.unwrap())
956                        } else {
957                            String::new()
958                        }
959                    ),
960                    "━".repeat(50),
961                ];
962
963                if entities.is_empty() {
964                    lines.push("No entities found.".to_string());
965                } else {
966                    for (i, entity) in entities.iter().take(50).enumerate() {
967                        lines.push(format!(
968                            "{}. {} ({})",
969                            i + 1,
970                            entity.name,
971                            entity.entity_type
972                        ));
973                    }
974
975                    if entities.len() > 50 {
976                        lines.push(String::new());
977                        lines.push(format!("... and {} more entities", entities.len() - 50));
978                    }
979                }
980
981                self.results_viewer.set_content(lines);
982                self.action_tx.send(Action::SetStatus(
983                    StatusType::Info,
984                    format!("Found {} entities", entities.len()),
985                ))?;
986            },
987            Err(e) => {
988                self.action_tx.send(Action::SetStatus(
989                    StatusType::Error,
990                    format!("Failed to list entities: {}", e),
991                ))?;
992            },
993        }
994
995        Ok(())
996    }
997
998    /// Handle loading workspace
999    async fn handle_load_workspace(&mut self, name: String) -> Result<()> {
1000        if !self.graphrag.is_initialized().await {
1001            self.action_tx.send(Action::SetStatus(
1002                StatusType::Error,
1003                "GraphRAG not initialized. Load a config first".to_string(),
1004            ))?;
1005            return Ok(());
1006        }
1007
1008        self.action_tx.send(Action::StartProgress(format!(
1009            "Loading workspace '{}'...",
1010            name
1011        )))?;
1012
1013        // Get workspace directory from workspace_manager (default: ~/.graphrag/workspaces)
1014        let workspace_dir = dirs::data_dir()
1015            .map(|p| p.join("graphrag").join("workspaces"))
1016            .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1017
1018        match self
1019            .graphrag
1020            .load_workspace(workspace_dir.to_str().unwrap(), &name)
1021            .await
1022        {
1023            Ok(message) => {
1024                self.action_tx.send(Action::StopProgress)?;
1025
1026                // Display success in results viewer
1027                self.results_viewer.set_content(vec![
1028                    "✓ Workspace Loaded".to_string(),
1029                    "━".repeat(50),
1030                    String::new(),
1031                    message,
1032                    String::new(),
1033                    "The workspace has been loaded successfully.".to_string(),
1034                    "You can now query the loaded graph.".to_string(),
1035                ]);
1036
1037                self.action_tx.send(Action::SetStatus(
1038                    StatusType::Success,
1039                    format!("Workspace '{}' loaded", name),
1040                ))?;
1041
1042                self.update_stats().await;
1043            },
1044            Err(e) => {
1045                self.action_tx.send(Action::StopProgress)?;
1046                self.action_tx.send(Action::SetStatus(
1047                    StatusType::Error,
1048                    format!("Failed to load workspace: {}", e),
1049                ))?;
1050            },
1051        }
1052
1053        Ok(())
1054    }
1055
1056    /// Handle listing workspaces
1057    async fn handle_list_workspaces(&mut self) -> Result<()> {
1058        let workspace_dir = dirs::data_dir()
1059            .map(|p| p.join("graphrag").join("workspaces"))
1060            .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1061
1062        match self
1063            .graphrag
1064            .list_workspaces(workspace_dir.to_str().unwrap())
1065            .await
1066        {
1067            Ok(list_output) => {
1068                self.results_viewer
1069                    .set_content(list_output.lines().map(|s| s.to_string()).collect());
1070                self.action_tx.send(Action::SetStatus(
1071                    StatusType::Info,
1072                    "Workspace list displayed".to_string(),
1073                ))?;
1074            },
1075            Err(e) => {
1076                self.action_tx.send(Action::SetStatus(
1077                    StatusType::Error,
1078                    format!("Failed to list workspaces: {}", e),
1079                ))?;
1080            },
1081        }
1082
1083        Ok(())
1084    }
1085
1086    /// Handle saving workspace
1087    async fn handle_save_workspace(&mut self, name: String) -> Result<()> {
1088        if !self.graphrag.is_initialized().await {
1089            self.action_tx.send(Action::SetStatus(
1090                StatusType::Error,
1091                "GraphRAG not initialized. Load a config first".to_string(),
1092            ))?;
1093            return Ok(());
1094        }
1095
1096        self.action_tx.send(Action::StartProgress(format!(
1097            "Saving workspace '{}'...",
1098            name
1099        )))?;
1100
1101        let workspace_dir = dirs::data_dir()
1102            .map(|p| p.join("graphrag").join("workspaces"))
1103            .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1104
1105        match self
1106            .graphrag
1107            .save_workspace(workspace_dir.to_str().unwrap(), &name)
1108            .await
1109        {
1110            Ok(message) => {
1111                self.action_tx.send(Action::StopProgress)?;
1112
1113                // Display success in results viewer
1114                self.results_viewer.set_content(vec![
1115                    "✓ Workspace Saved".to_string(),
1116                    "━".repeat(50),
1117                    String::new(),
1118                    message,
1119                    String::new(),
1120                    format!("Workspace location: {}", workspace_dir.display()),
1121                    String::new(),
1122                    "You can load this workspace later with:".to_string(),
1123                    format!("  /workspace {}", name),
1124                ]);
1125
1126                self.action_tx.send(Action::SetStatus(
1127                    StatusType::Success,
1128                    format!("Workspace '{}' saved", name),
1129                ))?;
1130            },
1131            Err(e) => {
1132                self.action_tx.send(Action::StopProgress)?;
1133                self.action_tx.send(Action::SetStatus(
1134                    StatusType::Error,
1135                    format!("Failed to save workspace: {}", e),
1136                ))?;
1137            },
1138        }
1139
1140        Ok(())
1141    }
1142
1143    /// Handle deleting workspace
1144    async fn handle_delete_workspace(&mut self, name: String) -> Result<()> {
1145        self.action_tx.send(Action::StartProgress(format!(
1146            "Deleting workspace '{}'...",
1147            name
1148        )))?;
1149
1150        let workspace_dir = dirs::data_dir()
1151            .map(|p| p.join("graphrag").join("workspaces"))
1152            .unwrap_or_else(|| std::path::PathBuf::from("./workspaces"));
1153
1154        match self
1155            .graphrag
1156            .delete_workspace(workspace_dir.to_str().unwrap(), &name)
1157            .await
1158        {
1159            Ok(message) => {
1160                self.action_tx.send(Action::StopProgress)?;
1161
1162                // Display success in results viewer
1163                self.results_viewer.set_content(vec![
1164                    "✓ Workspace Deleted".to_string(),
1165                    "━".repeat(50),
1166                    String::new(),
1167                    message,
1168                    String::new(),
1169                    "The workspace has been permanently deleted.".to_string(),
1170                ]);
1171
1172                self.action_tx.send(Action::SetStatus(
1173                    StatusType::Success,
1174                    format!("Workspace '{}' deleted", name),
1175                ))?;
1176            },
1177            Err(e) => {
1178                self.action_tx.send(Action::StopProgress)?;
1179                self.action_tx.send(Action::SetStatus(
1180                    StatusType::Error,
1181                    format!("Failed to delete workspace: {}", e),
1182                ))?;
1183            },
1184        }
1185
1186        Ok(())
1187    }
1188
1189    /// Handle /config show — display current config file in results viewer
1190    async fn handle_config_show(&mut self) -> Result<()> {
1191        if let Some(ref path) = self.config_path.clone() {
1192            match tokio::fs::read_to_string(path).await {
1193                Ok(content) => {
1194                    let mut lines = vec![
1195                        format!("# Config: {}", path.display()),
1196                        String::new(),
1197                        "```".to_string(),
1198                    ];
1199                    lines.extend(content.lines().map(|l| l.to_string()));
1200                    lines.push("```".to_string());
1201                    self.results_viewer.set_content(lines);
1202                    self.action_tx.send(Action::SetStatus(
1203                        StatusType::Info,
1204                        format!("Showing config: {}", path.display()),
1205                    ))?;
1206                },
1207                Err(e) => {
1208                    self.action_tx.send(Action::SetStatus(
1209                        StatusType::Error,
1210                        format!("Cannot read config file: {}", e),
1211                    ))?;
1212                },
1213            }
1214        } else {
1215            self.action_tx.send(Action::SetStatus(
1216                StatusType::Warning,
1217                "No config loaded. Use /config <file> first.".to_string(),
1218            ))?;
1219        }
1220        Ok(())
1221    }
1222
1223    /// Handle /export <file> — write query history to a Markdown file
1224    async fn handle_export(&mut self, path: PathBuf) -> Result<()> {
1225        let entries = self.query_history.last_n(1000);
1226        if entries.is_empty() {
1227            self.action_tx.send(Action::SetStatus(
1228                StatusType::Warning,
1229                "No query history to export.".to_string(),
1230            ))?;
1231            return Ok(());
1232        }
1233
1234        let mut md = String::from("# GraphRAG Query History\n\n");
1235        for (i, entry) in entries.iter().enumerate() {
1236            md.push_str(&format!("## Query {}\n\n", i + 1));
1237            md.push_str(&format!("**Q:** {}\n\n", entry.query));
1238            if !entry.results_preview.is_empty() {
1239                md.push_str(&format!("**A:** {}\n\n", entry.results_preview[0]));
1240            }
1241            md.push_str(&format!(
1242                "*{}ms · {} sources*\n\n---\n\n",
1243                entry.duration_ms, entry.results_count
1244            ));
1245        }
1246
1247        let expanded = FileOperations::expand_tilde(&path);
1248        match tokio::fs::write(&expanded, md.as_bytes()).await {
1249            Ok(()) => {
1250                let msg = format!(
1251                    "Exported {} queries to {}",
1252                    entries.len(),
1253                    expanded.display()
1254                );
1255                self.results_viewer.set_content(vec![
1256                    "## Export Complete".to_string(),
1257                    String::new(),
1258                    msg.clone(),
1259                ]);
1260                self.action_tx
1261                    .send(Action::SetStatus(StatusType::Success, msg))?;
1262            },
1263            Err(e) => {
1264                self.action_tx.send(Action::SetStatus(
1265                    StatusType::Error,
1266                    format!("Export failed: {}", e),
1267                ))?;
1268            },
1269        }
1270        Ok(())
1271    }
1272
1273    /// Update graph statistics in info panel
1274    async fn update_stats(&mut self) {
1275        if let Some(stats) = self.graphrag.get_stats().await {
1276            self.info_panel.set_stats(stats);
1277        }
1278    }
1279}