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::spread_feed::{new_spread_snapshot, spawn_spread_mark_feed};
17use crate::ui::watch::{run_watch_tui, WatchAgentMode, WatchConfig};
18use crate::market_conditions::{spawn_market_conditions_feed, MarketConditionsSnapshot};
19
20pub async fn run_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
21 show_dashboard(runtime, file).await
22}
23
24pub async fn run_watch(
25 runtime: &RuntimeConfig,
26 file: Option<PathBuf>,
27 monitor_only: bool,
28) -> Result<()> {
29 if runtime.output == crate::output::OutputFormat::Json {
30 anyhow::bail!("watch is interactive only — omit --json");
31 }
32
33 let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
34 let daemon = daemon_status(&rules_path);
35
36 let agent_mode = if daemon.running {
37 WatchAgentMode::External
38 } else if monitor_only {
39 WatchAgentMode::MonitorOnly
40 } else {
41 WatchAgentMode::Embedded
42 };
43
44 let will_spawn_agent = matches!(agent_mode, WatchAgentMode::Embedded);
45
46 if will_spawn_agent && !runtime.dry_run && !runtime.simulate {
47 let agent_id = RulesConfig::load(&rules_path)
48 .map(|r| r.agent_id)
49 .unwrap_or_else(|_| "agent".into());
50 require_trading_approval(
51 runtime,
52 "watch",
53 &format!("Run and watch options agent `{agent_id}`"),
54 )?;
55 }
56
57 let agent_runtime = RuntimeConfig::for_agent_trading(
58 runtime.output,
59 runtime.yes,
60 runtime.dry_run,
61 runtime.simulate,
62 runtime.trust,
63 true,
64 runtime.safety.clone(),
65 runtime.sink.clone(),
66 );
67
68 let market_snapshot = Arc::new(Mutex::new(MarketSnapshot::default()));
69 market_status::refresh_market_snapshot(runtime, &market_snapshot).await;
70 let snapshot_for_refresh = market_snapshot.clone();
71 let refresh_runtime = runtime.clone();
72 let _market_refresh = tokio::spawn(async move {
73 let mut interval = tokio::time::interval(Duration::from_secs(120));
74 interval.tick().await;
75 loop {
76 interval.tick().await;
77 market_status::refresh_market_snapshot(&refresh_runtime, &snapshot_for_refresh).await;
78 }
79 });
80
81 let market_conditions = Arc::new(Mutex::new(MarketConditionsSnapshot::default()));
82 if let Ok(market_api) = runtime.build_market_api() {
83 if let Ok(mut guard) = market_conditions.lock() {
84 crate::market_conditions::refresh_market_conditions(&market_api, &mut guard).await;
85 }
86 let _conditions_feed =
87 spawn_market_conditions_feed(market_api, market_conditions.clone());
88 }
89
90 let agent_health: Option<SharedAgentHealth> = if will_spawn_agent {
91 Some(new_shared_health())
92 } else {
93 None
94 };
95
96 let agent_handle = if will_spawn_agent {
97 let path = rules_path.clone();
98 let health = agent_health.clone().expect("health when spawning");
99 Some(tokio::spawn(async move {
100 let result = run_agent_loop(&agent_runtime, &path, false, Some(health.clone())).await;
101 if let Err(e) = result {
102 let msg = format!("agent exited: {e:#}");
103 let _ = crate::agent::paths::append_agent_log(&path, &msg);
104 if let Ok(mut g) = health.lock() {
105 g.loop_running = false;
106 g.record_error(&msg);
107 }
108 }
109 }))
110 } else {
111 None
112 };
113
114 if will_spawn_agent {
116 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
117 }
118
119 let spread_snapshot = new_spread_snapshot();
120 if let (Ok(market), Ok(trader)) = (
121 runtime.build_market_api(),
122 runtime.build_api(),
123 ) {
124 let _spread_feed = spawn_spread_mark_feed(
125 rules_path.clone(),
126 runtime.simulate,
127 spread_snapshot.clone(),
128 market,
129 trader,
130 );
131 }
132
133 let watch_config = WatchConfig {
134 rules_path: rules_path.clone(),
135 agent_mode,
136 market_snapshot,
137 market_conditions,
138 agent_health,
139 spread_snapshot,
140 };
141
142 let watch_result = tokio::task::spawn_blocking(move || run_watch_tui(&watch_config))
143 .await
144 .context("watch UI thread panicked")?;
145
146 if let Some(handle) = agent_handle {
147 handle.abort();
148 let _ = handle.await;
149 }
150
151 watch_result
152}
153
154pub async fn run_rules(runtime: &RuntimeConfig, command: RulesCommands) -> Result<()> {
155 match command {
156 RulesCommands::Show { file } => show_rules(runtime, file).await,
157 RulesCommands::List => {
158 if runtime.output == crate::output::OutputFormat::Json {
159 list_rules_files(runtime);
160 } else {
161 println!("\n{}", list_rules_files_display());
162 println!();
163 }
164 Ok(())
165 }
166 }
167}