1use 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#[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#[derive(Debug, Clone, Default)]
54pub struct NodeReviewState {
55 pub node_id: Option<String>,
57 pub node_class: Option<String>,
59 pub files_created: Vec<String>,
61 pub files_modified: Vec<String>,
63 pub writes_count: usize,
65 pub diffs_count: usize,
67 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 pub energy_components: Option<perspt_core::EnergyComponents>,
78 pub stage_outcomes: Vec<perspt_core::StageOutcome>,
80 pub degraded: bool,
82 pub degraded_reasons: Vec<String>,
83 pub summary: Option<String>,
85 pub diff: Option<String>,
87 pub description: Option<String>,
89}
90
91pub struct AgentApp {
93 pub dashboard: Dashboard,
95 pub task_tree: TaskTree,
97 pub diff_viewer: DiffViewer,
99 pub review_modal: ReviewModal,
101 pub action_sender: Option<perspt_core::events::channel::ActionSender>,
103 pub active_tab: ActiveTab,
105 pub pending_request_id: Option<String>,
107 pub review_state: NodeReviewState,
109 pub should_quit: bool,
111 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 pub fn new() -> Self {
135 Self::default()
136 }
137
138 pub fn set_action_sender(&mut self, sender: perspt_core::events::channel::ActionSender) {
140 self.action_sender = Some(sender);
141 }
142
143 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 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 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 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 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 KeyCode::Char('q') => self.should_quit = true,
220 KeyCode::Char('p') => self.paused = !self.paused,
222 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 KeyCode::Up | KeyCode::Char('k') => self.handle_up(),
230 KeyCode::Down | KeyCode::Char('j') => self.handle_down(),
231 KeyCode::PageUp => self.handle_page_up(),
233 KeyCode::PageDown => self.handle_page_down(),
234 KeyCode::Char(' ') | KeyCode::Enter => self.handle_select(),
236 KeyCode::Char('a') => self.show_approval_modal(),
238 _ => {}
239 }
240 }
241 }
242 Ok(())
243 }
244
245 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 if self.review_modal.visible {
790 self.review_modal.render(frame, frame.area());
791 }
792 }
793}
794
795pub 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 let (event_sender, mut event_receiver) = channel::event_channel();
805 let (action_sender, action_receiver) = channel::action_channel();
806
807 orchestrator.connect_tui(event_sender, action_receiver);
809
810 let mut terminal = ratatui::init();
812 let mut app = AgentApp::new();
813 app.set_action_sender(action_sender);
814
815 let orchestrator_handle = tokio::spawn(async move { orchestrator.run(task).await });
817
818 loop {
820 terminal.draw(|frame| app.render(frame))?;
822
823 tokio::select! {
825 _ = 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 if key.code == KeyCode::Char('q') {
832 app.should_quit = true;
833 }
834 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 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 if orchestrator_handle.is_finished() {
880 }
882 }
883
884 ratatui::restore();
885 orchestrator_handle.abort();
886 Ok(())
887}