schwab_cli/commands/
ui_cmd.rs1use std::path::PathBuf;
2use std::sync::{Arc, Mutex};
3use std::time::Duration;
4
5use anyhow::{Context, Result};
6
7use crate::agent::{daemon_status, run_agent_loop};
8use crate::cli::RulesCommands;
9use crate::config::RuntimeConfig;
10use crate::rules::RulesConfig;
11use crate::safety::require_trading_approval;
12use crate::ui::agent_health::{new_shared_health, SharedAgentHealth};
13use crate::ui::discover::{list_rules_files_display, resolve_rules_file};
14use crate::ui::market_status::{self, MarketSnapshot};
15use crate::ui::menu::{list_rules_files, show_dashboard, show_rules};
16use crate::ui::watch::{run_watch_tui, WatchAgentMode, WatchConfig};
17
18pub async fn run_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
19 show_dashboard(runtime, file).await
20}
21
22pub async fn run_watch(
23 runtime: &RuntimeConfig,
24 file: Option<PathBuf>,
25 monitor_only: bool,
26) -> Result<()> {
27 if runtime.output == crate::output::OutputFormat::Json {
28 anyhow::bail!("watch is interactive only — omit --json");
29 }
30
31 let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
32 let daemon = daemon_status(&rules_path);
33
34 let agent_mode = if daemon.running {
35 WatchAgentMode::External
36 } else if monitor_only {
37 WatchAgentMode::MonitorOnly
38 } else {
39 WatchAgentMode::Embedded
40 };
41
42 let will_spawn_agent = matches!(agent_mode, WatchAgentMode::Embedded);
43
44 if will_spawn_agent && !runtime.dry_run {
45 let agent_id = RulesConfig::load(&rules_path)
46 .map(|r| r.agent_id)
47 .unwrap_or_else(|_| "agent".into());
48 require_trading_approval(
49 runtime,
50 "watch",
51 &format!("Run and watch options agent `{agent_id}`"),
52 )?;
53 }
54
55 let mut agent_runtime = runtime.clone();
56 agent_runtime.suppress_tick_output = true;
57
58 let market_snapshot = Arc::new(Mutex::new(MarketSnapshot::default()));
59 market_status::refresh_market_snapshot(runtime, &market_snapshot).await;
60 let snapshot_for_refresh = market_snapshot.clone();
61 let refresh_runtime = runtime.clone();
62 let _market_refresh = tokio::spawn(async move {
63 let mut interval = tokio::time::interval(Duration::from_secs(120));
64 interval.tick().await;
65 loop {
66 interval.tick().await;
67 market_status::refresh_market_snapshot(&refresh_runtime, &snapshot_for_refresh).await;
68 }
69 });
70
71 let agent_health: Option<SharedAgentHealth> = if will_spawn_agent {
72 Some(new_shared_health())
73 } else {
74 None
75 };
76
77 let agent_handle = if will_spawn_agent {
78 let path = rules_path.clone();
79 let health = agent_health.clone().expect("health when spawning");
80 Some(tokio::spawn(async move {
81 let result = run_agent_loop(&agent_runtime, &path, false, Some(health.clone())).await;
82 if let Err(e) = result {
83 let msg = format!("agent exited: {e:#}");
84 let _ = crate::agent::paths::append_agent_log(&path, &msg);
85 if let Ok(mut g) = health.lock() {
86 g.loop_running = false;
87 g.record_error(&msg);
88 }
89 }
90 }))
91 } else {
92 None
93 };
94
95 if will_spawn_agent {
97 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
98 }
99
100 let watch_config = WatchConfig {
101 rules_path: rules_path.clone(),
102 agent_mode,
103 market_snapshot,
104 agent_health,
105 };
106
107 let watch_result = tokio::task::spawn_blocking(move || run_watch_tui(&watch_config))
108 .await
109 .context("watch UI thread panicked")?;
110
111 if let Some(handle) = agent_handle {
112 handle.abort();
113 let _ = handle.await;
114 }
115
116 watch_result
117}
118
119pub async fn run_rules(runtime: &RuntimeConfig, command: RulesCommands) -> Result<()> {
120 match command {
121 RulesCommands::Show { file } => show_rules(runtime, file).await,
122 RulesCommands::List => {
123 if runtime.output == crate::output::OutputFormat::Json {
124 list_rules_files(runtime);
125 } else {
126 println!("\n{}", list_rules_files_display());
127 println!();
128 }
129 Ok(())
130 }
131 }
132}