Skip to main content

perspt_tui/
agent_app.rs

1//! Agent App - Main TUI Application
2//!
3//! Coordinates all TUI components for the Agent mode with full keyboard navigation.
4//! Now with async event-driven architecture support.
5
6use crate::app_event::{AgentStateUpdate, AppEvent};
7use crate::dashboard::Dashboard;
8use crate::diff_viewer::DiffViewer;
9use crate::review_modal::{ReviewDecision, ReviewModal};
10use crate::task_tree::{TaskStatus, TaskTree};
11use crossterm::event::{KeyCode, KeyEventKind};
12use perspt_core::AgentEvent;
13use ratatui::{
14    crossterm::event::{self, Event},
15    layout::{Constraint, Direction, Layout},
16    style::{Color, Modifier, Style},
17    widgets::{Block, Borders, Tabs},
18    DefaultTerminal, Frame,
19};
20use std::io;
21
22/// Active tab
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum ActiveTab {
25    Dashboard,
26    Tasks,
27    Diff,
28}
29
30impl ActiveTab {
31    fn index(&self) -> usize {
32        match self {
33            ActiveTab::Dashboard => 0,
34            ActiveTab::Tasks => 1,
35            ActiveTab::Diff => 2,
36        }
37    }
38
39    #[allow(dead_code)]
40    fn from_index(i: usize) -> Self {
41        match i {
42            0 => ActiveTab::Dashboard,
43            1 => ActiveTab::Tasks,
44            _ => ActiveTab::Diff,
45        }
46    }
47}
48
49/// PSP-5 Phase 7: Aggregated review state for the active approval boundary.
50///
51/// Populated incrementally as VerificationComplete, BundleApplied, and
52/// ApprovalRequest events arrive. Consumed by the review modal and diff viewer.
53#[derive(Debug, Clone, Default)]
54pub struct NodeReviewState {
55    /// Node currently under review
56    pub node_id: Option<String>,
57    /// Node class (Interface, Implementation, Integration)
58    pub node_class: Option<String>,
59    /// Files created by the bundle
60    pub files_created: Vec<String>,
61    /// Files modified by the bundle
62    pub files_modified: Vec<String>,
63    /// Write operation count
64    pub writes_count: usize,
65    /// Diff operation count
66    pub diffs_count: usize,
67    /// Latest verification result fields
68    pub syntax_ok: Option<bool>,
69    pub build_ok: Option<bool>,
70    pub tests_ok: Option<bool>,
71    pub lint_ok: Option<bool>,
72    pub diagnostics_count: Option<usize>,
73    pub tests_passed: Option<usize>,
74    pub tests_failed: Option<usize>,
75    pub energy: Option<f32>,
76    /// Full energy component breakdown
77    pub energy_components: Option<perspt_core::EnergyComponents>,
78    /// Per-stage outcomes with sensor status
79    pub stage_outcomes: Vec<perspt_core::StageOutcome>,
80    /// Whether verification ran degraded
81    pub degraded: bool,
82    pub degraded_reasons: Vec<String>,
83    /// Verification summary line
84    pub summary: Option<String>,
85    /// Diff text for the viewer
86    pub diff: Option<String>,
87    /// Approval request description
88    pub description: Option<String>,
89}
90
91/// Agent app state
92pub struct AgentApp {
93    /// Dashboard component
94    pub dashboard: Dashboard,
95    /// Task tree component
96    pub task_tree: TaskTree,
97    /// Diff viewer component
98    pub diff_viewer: DiffViewer,
99    /// Review modal component
100    pub review_modal: ReviewModal,
101    /// Sender for action feedback to orchestrator
102    pub action_sender: Option<perspt_core::events::channel::ActionSender>,
103    /// Active tab
104    pub active_tab: ActiveTab,
105    /// Pending approval request ID
106    pub pending_request_id: Option<String>,
107    /// PSP-5 Phase 7: Aggregated review state for the active approval
108    pub review_state: NodeReviewState,
109    /// Should quit
110    pub should_quit: bool,
111    /// Is paused
112    pub paused: bool,
113}
114
115impl Default for AgentApp {
116    fn default() -> Self {
117        Self {
118            active_tab: ActiveTab::Dashboard,
119            dashboard: Dashboard::new(),
120            task_tree: TaskTree::new(),
121            diff_viewer: DiffViewer::new(),
122            review_modal: ReviewModal::new(),
123            action_sender: None,
124            pending_request_id: None,
125            review_state: NodeReviewState::default(),
126            should_quit: false,
127            paused: false,
128        }
129    }
130}
131
132impl AgentApp {
133    /// Create a new agent app
134    pub fn new() -> Self {
135        Self::default()
136    }
137
138    /// Set action sender
139    pub fn set_action_sender(&mut self, sender: perspt_core::events::channel::ActionSender) {
140        self.action_sender = Some(sender);
141    }
142
143    /// PSP-5 Phase 8: Prepopulate task tree from persisted node states.
144    ///
145    /// Called before resuming so the TUI shows completed nodes immediately
146    /// instead of waiting for orchestrator events (which skip terminal nodes).
147    pub fn prepopulate_from_store(&mut self, session_id: &str) {
148        let Ok(store) = perspt_store::SessionStore::new() else {
149            return;
150        };
151
152        let nodes = store.get_latest_node_states(session_id).unwrap_or_default();
153
154        for ns in &nodes {
155            let status = match ns.state.as_str() {
156                "Completed" | "COMPLETED" | "STABLE" => TaskStatus::Completed,
157                "Failed" | "FAILED" => TaskStatus::Failed,
158                "Escalated" | "ESCALATED" => TaskStatus::Escalated,
159                "Coding" => TaskStatus::Coding,
160                "Verifying" => TaskStatus::Verifying,
161                "Committing" => TaskStatus::Committing,
162                _ => TaskStatus::Pending,
163            };
164
165            // Add the node to the task tree if not already present
166            let goal = ns.goal.clone().unwrap_or_else(|| ns.node_id.clone());
167            self.task_tree
168                .add_or_update_node(&ns.node_id, &goal, status);
169        }
170
171        self.dashboard.log(format!(
172            "📦 Restored {} nodes from session {}",
173            nodes.len(),
174            &session_id[..8.min(session_id.len())]
175        ));
176    }
177
178    /// Run the app main loop
179    pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
180        while !self.should_quit {
181            terminal.draw(|frame| self.render(frame))?;
182            self.handle_events()?;
183        }
184        Ok(())
185    }
186
187    /// Handle input events
188    fn handle_events(&mut self) -> io::Result<()> {
189        if event::poll(std::time::Duration::from_millis(100))? {
190            if let Event::Key(key) = event::read()? {
191                if key.kind != KeyEventKind::Press {
192                    return Ok(());
193                }
194
195                // Handle modal first if visible
196                if self.review_modal.visible {
197                    match key.code {
198                        KeyCode::Left => self.review_modal.select_left(),
199                        KeyCode::Right => self.review_modal.select_right(),
200                        KeyCode::Char(c) => {
201                            if let Some(decision) = self.review_modal.handle_key(c) {
202                                self.handle_review_decision(decision);
203                                self.review_modal.hide();
204                            }
205                        }
206                        KeyCode::Enter => {
207                            let decision = self.review_modal.get_decision();
208                            self.handle_review_decision(decision);
209                            self.review_modal.hide();
210                        }
211                        KeyCode::Esc => self.review_modal.hide(),
212                        _ => {}
213                    }
214                    return Ok(());
215                }
216
217                match key.code {
218                    // Quit
219                    KeyCode::Char('q') => self.should_quit = true,
220                    // Pause/Resume
221                    KeyCode::Char('p') => self.paused = !self.paused,
222                    // Tab navigation
223                    KeyCode::Tab => self.next_tab(),
224                    KeyCode::BackTab => self.prev_tab(),
225                    KeyCode::Char('1') => self.active_tab = ActiveTab::Dashboard,
226                    KeyCode::Char('2') => self.active_tab = ActiveTab::Tasks,
227                    KeyCode::Char('3') => self.active_tab = ActiveTab::Diff,
228                    // Vertical navigation (vim-style)
229                    KeyCode::Up | KeyCode::Char('k') => self.handle_up(),
230                    KeyCode::Down | KeyCode::Char('j') => self.handle_down(),
231                    // Page navigation
232                    KeyCode::PageUp => self.handle_page_up(),
233                    KeyCode::PageDown => self.handle_page_down(),
234                    // Task tree specific
235                    KeyCode::Char(' ') | KeyCode::Enter => self.handle_select(),
236                    // Approve current
237                    KeyCode::Char('a') => self.show_approval_modal(),
238                    _ => {}
239                }
240            }
241        }
242        Ok(())
243    }
244
245    /// Handle logical app events
246    pub fn handle_app_event(&mut self, event: AppEvent) {
247        match event {
248            AppEvent::CoreEvent(core_event) => self.handle_core_event(core_event),
249            AppEvent::AgentUpdate(update) => self.handle_agent_update(update),
250            _ => {}
251        }
252    }
253
254    /// Handle events from the SRBN Orchestrator
255    fn handle_core_event(&mut self, event: AgentEvent) {
256        match event {
257            AgentEvent::PlanGenerated(plan) => {
258                self.dashboard
259                    .log(format!("Plan generated with {} tasks", plan.tasks.len()));
260                self.task_tree.populate_from_plan(plan.clone());
261            }
262            AgentEvent::TaskStatusChanged { node_id, status } => {
263                self.task_tree.update_status(&node_id, status.into());
264                // Update verifier stage indicator
265                let status_label: crate::task_tree::TaskStatus = status.into();
266                if matches!(
267                    status_label,
268                    crate::task_tree::TaskStatus::Verifying
269                        | crate::task_tree::TaskStatus::SheafCheck
270                        | crate::task_tree::TaskStatus::Coding
271                        | crate::task_tree::TaskStatus::Committing
272                ) {
273                    self.dashboard.verifier_stage = Some(format!("{:?}", status_label));
274                }
275                self.dashboard
276                    .log(format!("🔄 Task {} -> {:?}", node_id, status));
277            }
278            AgentEvent::Log(message) => {
279                self.dashboard.log(message);
280            }
281            AgentEvent::NodeCompleted { node_id, goal } => {
282                self.task_tree
283                    .update_status(&node_id, TaskStatus::Completed);
284                self.dashboard.log(format!("✓ {} - {}", node_id, goal));
285            }
286            AgentEvent::ApprovalRequest {
287                request_id,
288                node_id,
289                action_type,
290                description,
291                diff,
292            } => {
293                self.pending_request_id = Some(request_id);
294                // Populate review state node context
295                self.review_state.description = Some(description.clone());
296                self.review_state.diff = diff.clone();
297                if self.review_state.node_id.is_none() {
298                    self.review_state.node_id = Some(node_id.clone());
299                }
300                // Collect affected files from action type
301                let files = match &action_type {
302                    perspt_core::ActionType::FileWrite { path } => vec![path.clone()],
303                    perspt_core::ActionType::BundleWrite { files, .. } => files.clone(),
304                    _ => self
305                        .review_state
306                        .files_created
307                        .iter()
308                        .chain(self.review_state.files_modified.iter())
309                        .cloned()
310                        .collect(),
311                };
312
313                // PSP-5 Phase 7: Populate diff viewer bundle summary
314                self.diff_viewer.bundle_summary = Some(crate::diff_viewer::BundleSummary {
315                    node_id: node_id.clone(),
316                    node_class: self.review_state.node_class.clone().unwrap_or_default(),
317                    files_created: self.review_state.files_created.len(),
318                    files_modified: self.review_state.files_modified.len(),
319                    writes_count: self.review_state.writes_count,
320                    diffs_count: self.review_state.diffs_count,
321                });
322                if let Some(ref diff_text) = diff {
323                    self.diff_viewer.parse_diff(diff_text);
324                    // Tag hunks with operation labels from review state
325                    for hunk in &mut self.diff_viewer.hunks {
326                        if self.review_state.files_created.contains(&hunk.file_path) {
327                            hunk.operation = Some("created".to_string());
328                        } else if self.review_state.files_modified.contains(&hunk.file_path) {
329                            hunk.operation = Some("modified".to_string());
330                        }
331                    }
332                }
333
334                // PSP-5 Phase 7: Build stability metrics with verification context
335                use crate::review_modal::StabilityMetrics;
336                let stability = if self.review_state.energy.is_some()
337                    || self.review_state.syntax_ok.is_some()
338                {
339                    let energy = self.review_state.energy.unwrap_or(0.0);
340                    Some(StabilityMetrics {
341                        energy: crate::telemetry::EnergyComponents {
342                            v_syn: self
343                                .review_state
344                                .energy_components
345                                .as_ref()
346                                .map(|e| e.v_syn)
347                                .unwrap_or(0.0),
348                            v_str: self
349                                .review_state
350                                .energy_components
351                                .as_ref()
352                                .map(|e| e.v_str)
353                                .unwrap_or(0.0),
354                            v_log: self
355                                .review_state
356                                .energy_components
357                                .as_ref()
358                                .map(|e| e.v_log)
359                                .unwrap_or(0.0),
360                            v_boot: self
361                                .review_state
362                                .energy_components
363                                .as_ref()
364                                .map(|e| e.v_boot)
365                                .unwrap_or(0.0),
366                            v_sheaf: self
367                                .review_state
368                                .energy_components
369                                .as_ref()
370                                .map(|e| e.v_sheaf)
371                                .unwrap_or(0.0),
372                            total: energy,
373                        },
374                        is_stable: energy < 0.1,
375                        threshold: 0.1,
376                        attempts: 0,
377                        max_attempts: 0,
378                        syntax_ok: self.review_state.syntax_ok,
379                        build_ok: self.review_state.build_ok,
380                        tests_ok: self.review_state.tests_ok,
381                        lint_ok: self.review_state.lint_ok,
382                        tests_passed: self.review_state.tests_passed,
383                        tests_failed: self.review_state.tests_failed,
384                        degraded: self.review_state.degraded,
385                        degraded_reasons: self.review_state.degraded_reasons.clone(),
386                        node_class: self.review_state.node_class.clone(),
387                    })
388                } else {
389                    None
390                };
391
392                if let Some(stability) = stability {
393                    self.review_modal.show_with_stability(
394                        format!("Approval: {}", node_id),
395                        description,
396                        files,
397                        stability,
398                    );
399                } else {
400                    self.review_modal
401                        .show(format!("Approval: {}", node_id), description, files);
402                }
403            }
404            AgentEvent::Complete { success, message } => {
405                let emoji = if success { "🎉" } else { "❌" };
406                self.dashboard
407                    .log(format!("{} Session Complete: {}", emoji, message));
408            }
409            AgentEvent::EscalationClassified {
410                node_id,
411                category,
412                action,
413            } => {
414                self.dashboard.escalation_count += 1;
415                self.dashboard.log(format!(
416                    "⚠️ Escalation: {} → {} (action: {})",
417                    node_id, category, action
418                ));
419            }
420            AgentEvent::SheafValidationComplete {
421                node_id,
422                validators_run,
423                failures,
424                v_sheaf,
425            } => {
426                if failures > 0 {
427                    self.dashboard.log(format!(
428                        "🔍 Sheaf: {} — {}/{} failed (V_sheaf={:.3})",
429                        node_id, failures, validators_run, v_sheaf
430                    ));
431                } else {
432                    self.dashboard.log(format!(
433                        "✓ Sheaf: {} — {}/{} passed",
434                        node_id, validators_run, validators_run
435                    ));
436                }
437            }
438            AgentEvent::GraphRewriteApplied {
439                trigger_node,
440                action,
441                nodes_affected,
442            } => {
443                self.dashboard.log(format!(
444                    "🔧 Rewrite: {} via {} ({} nodes)",
445                    trigger_node, action, nodes_affected
446                ));
447            }
448            // PSP-5 Phase 6: Provisional branch lifecycle events
449            AgentEvent::BranchCreated {
450                branch_id,
451                node_id,
452                parent_node_id,
453            } => {
454                self.dashboard.active_branches += 1;
455                self.dashboard.log(format!(
456                    "🌿 Branch: {} for {} (parent: {})",
457                    &branch_id[..branch_id.len().min(16)],
458                    node_id,
459                    parent_node_id
460                ));
461            }
462            AgentEvent::InterfaceSealed {
463                node_id,
464                sealed_paths,
465                seal_hash,
466            } => {
467                self.dashboard.log(format!(
468                    "🔒 Sealed: {} ({} artifact{}) [{}]",
469                    node_id,
470                    sealed_paths.len(),
471                    if sealed_paths.len() == 1 { "" } else { "s" },
472                    &seal_hash[..seal_hash.len().min(12)]
473                ));
474            }
475            AgentEvent::BranchFlushed {
476                parent_node_id,
477                flushed_branch_ids,
478                reason,
479            } => {
480                self.dashboard.active_branches = self
481                    .dashboard
482                    .active_branches
483                    .saturating_sub(flushed_branch_ids.len());
484                self.dashboard.log(format!(
485                    "🗑️  Flushed: {} branch(es) from {} — {}",
486                    flushed_branch_ids.len(),
487                    parent_node_id,
488                    reason
489                ));
490            }
491            AgentEvent::DependentUnblocked {
492                child_node_id,
493                parent_node_id,
494            } => {
495                self.dashboard.log(format!(
496                    "🔓 Unblocked: {} (parent {} sealed)",
497                    child_node_id, parent_node_id
498                ));
499            }
500            AgentEvent::BranchMerged { branch_id, node_id } => {
501                self.dashboard.active_branches = self.dashboard.active_branches.saturating_sub(1);
502                self.dashboard.log(format!(
503                    "✅ Merged: branch {} for {}",
504                    &branch_id[..branch_id.len().min(16)],
505                    node_id
506                ));
507            }
508            AgentEvent::ContextDegraded {
509                node_id,
510                budget_exceeded,
511                missing_owned_files,
512                included_file_count,
513                total_bytes: _,
514                reason,
515            } => {
516                let detail = if budget_exceeded {
517                    format!("{} files included (budget exceeded)", included_file_count)
518                } else {
519                    format!("{} owned file(s) missing", missing_owned_files.len())
520                };
521                self.dashboard.log(format!(
522                    "⚠️ Context degraded: {} — {} ({})",
523                    node_id, reason, detail
524                ));
525            }
526            AgentEvent::ProvenanceDrift {
527                node_id,
528                missing_files,
529                reason: _,
530            } => {
531                self.dashboard.log(format!(
532                    "⚠️ Provenance drift: {} — {} file(s) missing since last run",
533                    node_id,
534                    missing_files.len()
535                ));
536            }
537            AgentEvent::ToolReadiness {
538                plugins,
539                strictness,
540            } => {
541                self.dashboard
542                    .log(format!("🔧 Verifier strictness: {}", strictness));
543                for pr in &plugins {
544                    if pr.degraded_stages.is_empty() {
545                        self.dashboard
546                            .log(format!("🔌 {} — all stages available", pr.plugin_name));
547                    } else {
548                        self.dashboard.log(format!(
549                            "🔌 {} — degraded: {}",
550                            pr.plugin_name,
551                            pr.degraded_stages.join(", ")
552                        ));
553                    }
554                }
555            }
556            // PSP-5 Phase 7: Populate review state from verification and bundle events
557            AgentEvent::VerificationComplete {
558                node_id,
559                syntax_ok,
560                build_ok,
561                tests_ok,
562                lint_ok,
563                diagnostics_count,
564                tests_passed,
565                tests_failed,
566                energy,
567                energy_components,
568                stage_outcomes,
569                degraded,
570                degraded_reasons,
571                summary,
572                node_class,
573            } => {
574                self.review_state.node_id = Some(node_id.clone());
575                self.review_state.node_class = Some(node_class);
576                self.review_state.syntax_ok = Some(syntax_ok);
577                self.review_state.build_ok = Some(build_ok);
578                self.review_state.tests_ok = Some(tests_ok);
579                self.review_state.lint_ok = Some(lint_ok);
580                self.review_state.diagnostics_count = Some(diagnostics_count);
581                self.review_state.tests_passed = Some(tests_passed);
582                self.review_state.tests_failed = Some(tests_failed);
583                self.review_state.energy = Some(energy);
584                self.review_state.energy_components = Some(energy_components.clone());
585                self.review_state.stage_outcomes = stage_outcomes;
586                self.review_state.degraded = degraded;
587                self.review_state.degraded_reasons = degraded_reasons;
588                self.review_state.summary = Some(summary.clone());
589
590                self.dashboard.update_energy(energy);
591                self.dashboard.energy_components = Some(energy_components);
592                self.dashboard.verifier_stage = Some(if degraded {
593                    "Degraded".to_string()
594                } else {
595                    "Complete".to_string()
596                });
597                self.dashboard
598                    .log(format!("🔍 Verified: {} — {}", node_id, summary));
599            }
600            AgentEvent::BundleApplied {
601                node_id,
602                files_created,
603                files_modified,
604                writes_count,
605                diffs_count,
606                node_class,
607            } => {
608                self.review_state.node_id = Some(node_id.clone());
609                self.review_state.node_class = Some(node_class);
610                self.review_state.files_created = files_created.clone();
611                self.review_state.files_modified = files_modified.clone();
612                self.review_state.writes_count = writes_count;
613                self.review_state.diffs_count = diffs_count;
614
615                self.dashboard.log(format!(
616                    "📦 Bundle: {} ({} writes, {} diffs)",
617                    node_id, writes_count, diffs_count
618                ));
619            }
620            _ => {}
621        }
622    }
623
624    fn handle_review_decision(&mut self, decision: ReviewDecision) {
625        let request_id = self.pending_request_id.take();
626        // PSP-5 Phase 7: Reset review state after decision
627        self.review_state = NodeReviewState::default();
628
629        match decision {
630            ReviewDecision::Approve => {
631                self.dashboard.log("✓ Changes approved".to_string());
632                if let (Some(sender), Some(rid)) = (&self.action_sender, request_id) {
633                    let _ = sender.send(perspt_core::AgentAction::Approve { request_id: rid });
634                }
635            }
636            ReviewDecision::Reject => {
637                self.dashboard.log("✗ Changes rejected".to_string());
638                if let (Some(sender), Some(rid)) = (&self.action_sender, request_id) {
639                    let _ = sender.send(perspt_core::AgentAction::Reject {
640                        request_id: rid,
641                        reason: Some("User rejected in TUI".to_string()),
642                    });
643                }
644            }
645            ReviewDecision::Edit => {
646                self.dashboard.log("📝 Opening in editor...".to_string());
647            }
648            ReviewDecision::ViewDiff => {
649                self.active_tab = ActiveTab::Diff;
650            }
651            ReviewDecision::RequestCorrection => {
652                self.dashboard.log("🔄 Correction requested".to_string());
653                if let (Some(sender), Some(rid)) = (&self.action_sender, request_id) {
654                    let _ = sender.send(perspt_core::AgentAction::RequestCorrection {
655                        request_id: rid,
656                        feedback: "User requested correction via TUI review".to_string(),
657                    });
658                }
659            }
660            ReviewDecision::Skip => {
661                self.dashboard.log("⏭ Skipped review".to_string());
662            }
663        }
664    }
665
666    fn handle_agent_update(&mut self, update: AgentStateUpdate) {
667        match update {
668            AgentStateUpdate::Energy { node_id, energy } => {
669                self.dashboard.update_energy(energy);
670                self.dashboard.current_node = Some(node_id.clone());
671                self.task_tree.update_energy(&node_id, energy);
672            }
673            AgentStateUpdate::Status { node_id, status } => {
674                self.task_tree.update_status(&node_id, status);
675            }
676            AgentStateUpdate::Log(msg) => {
677                self.dashboard.log(msg);
678            }
679            AgentStateUpdate::NodeCompleted(node_id) => {
680                self.dashboard.log(format!("Node {} completed", node_id));
681            }
682            AgentStateUpdate::Complete => {
683                self.dashboard.log("Orchestration complete".to_string());
684                self.dashboard.status = "Complete".to_string();
685            }
686        }
687    }
688
689    fn next_tab(&mut self) {
690        self.active_tab = match self.active_tab {
691            ActiveTab::Dashboard => ActiveTab::Tasks,
692            ActiveTab::Tasks => ActiveTab::Diff,
693            ActiveTab::Diff => ActiveTab::Dashboard,
694        };
695    }
696
697    fn prev_tab(&mut self) {
698        self.active_tab = match self.active_tab {
699            ActiveTab::Dashboard => ActiveTab::Diff,
700            ActiveTab::Tasks => ActiveTab::Dashboard,
701            ActiveTab::Diff => ActiveTab::Tasks,
702        };
703    }
704
705    fn handle_up(&mut self) {
706        match self.active_tab {
707            ActiveTab::Tasks => self.task_tree.previous(),
708            ActiveTab::Diff => self.diff_viewer.scroll_up(),
709            _ => {}
710        }
711    }
712
713    fn handle_down(&mut self) {
714        match self.active_tab {
715            ActiveTab::Tasks => self.task_tree.next(),
716            ActiveTab::Diff => self.diff_viewer.scroll_down(),
717            _ => {}
718        }
719    }
720
721    fn handle_page_up(&mut self) {
722        if self.active_tab == ActiveTab::Diff {
723            self.diff_viewer.page_up(20);
724        }
725    }
726
727    fn handle_page_down(&mut self) {
728        if self.active_tab == ActiveTab::Diff {
729            self.diff_viewer.page_down(20);
730        }
731    }
732
733    fn handle_select(&mut self) {
734        if self.active_tab == ActiveTab::Tasks {
735            if let Some(node) = self.task_tree.selected_task() {
736                self.dashboard.log(format!("Selected: {}", node.id));
737            }
738        }
739    }
740
741    fn show_approval_modal(&mut self) {
742        // Placeholder for manual approval trigger if needed
743        self.dashboard
744            .log("Manual approval modal Not Implemented".to_string());
745    }
746
747    pub fn handle_terminal_event(&mut self, event: crossterm::event::Event) -> bool {
748        // Legacy bridge for run_agent_tui_with_orchestrator
749        if let crossterm::event::Event::Key(key) = event {
750            if key.code == KeyCode::Char('q') {
751                return false;
752            }
753        }
754        true
755    }
756
757    pub fn render(&mut self, frame: &mut Frame) {
758        let chunks = Layout::default()
759            .direction(Direction::Vertical)
760            .constraints([Constraint::Length(3), Constraint::Min(0)])
761            .split(frame.area());
762
763        // Header with Tabs
764        let titles = vec!["[1] Dashboard", "[2] Task Tree", "[3] Diff Viewer"];
765        let tabs = Tabs::new(titles)
766            .block(
767                Block::default()
768                    .borders(Borders::ALL)
769                    .title(" perspt Agent mode "),
770            )
771            .select(self.active_tab.index())
772            .style(Style::default().fg(Color::Cyan))
773            .highlight_style(
774                Style::default()
775                    .add_modifier(Modifier::BOLD)
776                    .bg(Color::Black)
777                    .fg(Color::Yellow),
778            );
779        frame.render_widget(tabs, chunks[0]);
780
781        // Main Content
782        match self.active_tab {
783            ActiveTab::Dashboard => self.dashboard.render(frame, chunks[1]),
784            ActiveTab::Tasks => self.task_tree.render(frame, chunks[1]),
785            ActiveTab::Diff => self.diff_viewer.render(frame, chunks[1]),
786        }
787
788        // Modals
789        if self.review_modal.visible {
790            self.review_modal.render(frame, frame.area());
791        }
792    }
793}
794
795/// Run the agent TUI with a real SRBNOrchestrator
796pub async fn run_agent_tui_with_orchestrator(
797    mut orchestrator: perspt_agent::SRBNOrchestrator,
798    task: String,
799) -> anyhow::Result<()> {
800    use crate::app_event::AppEvent;
801    use perspt_core::events::channel;
802
803    // Create channels for bidirectional communication
804    let (event_sender, mut event_receiver) = channel::event_channel();
805    let (action_sender, action_receiver) = channel::action_channel();
806
807    // Connect orchestrator to TUI
808    orchestrator.connect_tui(event_sender, action_receiver);
809
810    // Initializing terminal
811    let mut terminal = ratatui::init();
812    let mut app = AgentApp::new();
813    app.set_action_sender(action_sender);
814
815    // Spawn orchestrator in background task
816    let orchestrator_handle = tokio::spawn(async move { orchestrator.run(task).await });
817
818    // Main event loop
819    loop {
820        // Render
821        terminal.draw(|frame| app.render(frame))?;
822
823        // Handle events with timeout for responsiveness
824        tokio::select! {
825            // Terminal events
826            _ = tokio::time::sleep(std::time::Duration::from_millis(50)) => {
827                if crossterm::event::poll(std::time::Duration::from_millis(0))? {
828                    if let crossterm::event::Event::Key(key) = crossterm::event::read()? {
829                        if key.kind == crossterm::event::KeyEventKind::Press {
830                            // Map Key Events to app state
831                            if key.code == KeyCode::Char('q') {
832                                app.should_quit = true;
833                            }
834                            // Pass keys to modal if visible
835                            if app.review_modal.visible {
836                                match key.code {
837                                    KeyCode::Left => app.review_modal.select_left(),
838                                    KeyCode::Right => app.review_modal.select_right(),
839                                    KeyCode::Char(c) => {
840                                        if let Some(decision) = app.review_modal.handle_key(c) {
841                                            app.handle_review_decision(decision);
842                                            app.review_modal.hide();
843                                        }
844                                    }
845                                    KeyCode::Enter => {
846                                        let decision = app.review_modal.get_decision();
847                                        app.handle_review_decision(decision);
848                                        app.review_modal.hide();
849                                    }
850                                    KeyCode::Esc => app.review_modal.hide(),
851                                    _ => {}
852                                }
853                            } else {
854                                match key.code {
855                                    KeyCode::Tab => app.next_tab(),
856                                    KeyCode::Char('1') => app.active_tab = ActiveTab::Dashboard,
857                                    KeyCode::Char('2') => app.active_tab = ActiveTab::Tasks,
858                                    KeyCode::Char('3') => app.active_tab = ActiveTab::Diff,
859                                    KeyCode::Up | KeyCode::Char('k') => app.handle_up(),
860                                    KeyCode::Down | KeyCode::Char('j') => app.handle_down(),
861                                    _ => {}
862                                }
863                            }
864                        }
865                    }
866                }
867            }
868            // Orchestrator events
869            Some(event) = event_receiver.recv() => {
870                app.handle_app_event(AppEvent::CoreEvent(event));
871            }
872        }
873
874        if app.should_quit {
875            break;
876        }
877
878        // Check if orchestrator finished
879        if orchestrator_handle.is_finished() {
880            // app.dashboard.log("🏁 Orchestrator finished".to_string());
881        }
882    }
883
884    ratatui::restore();
885    orchestrator_handle.abort();
886    Ok(())
887}