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.no_audio,
65 runtime.safety.clone(),
66 runtime.sink.clone(),
67 );
68
69 let market_snapshot = Arc::new(Mutex::new(MarketSnapshot::default()));
70 market_status::refresh_market_snapshot(runtime, &market_snapshot).await;
71 let snapshot_for_refresh = market_snapshot.clone();
72 let refresh_runtime = runtime.clone();
73 let _market_refresh = tokio::spawn(async move {
74 let mut interval = tokio::time::interval(Duration::from_secs(120));
75 interval.tick().await;
76 loop {
77 interval.tick().await;
78 market_status::refresh_market_snapshot(&refresh_runtime, &snapshot_for_refresh).await;
79 }
80 });
81
82 let market_conditions = Arc::new(Mutex::new(MarketConditionsSnapshot::default()));
83 if let Ok(market_api) = runtime.build_market_api() {
84 if let Ok(mut guard) = market_conditions.lock() {
85 crate::market_conditions::refresh_market_conditions(&market_api, &mut guard).await;
86 }
87 let _conditions_feed =
88 spawn_market_conditions_feed(market_api, market_conditions.clone());
89 }
90
91 let agent_health: Option<SharedAgentHealth> = if will_spawn_agent {
92 Some(new_shared_health())
93 } else {
94 None
95 };
96
97 let agent_handle = if will_spawn_agent {
98 let path = rules_path.clone();
99 let health = agent_health.clone().expect("health when spawning");
100 Some(tokio::spawn(async move {
101 let result = run_agent_loop(&agent_runtime, &path, false, Some(health.clone())).await;
102 if let Err(e) = result {
103 let msg = format!("agent exited: {e:#}");
104 let _ = crate::agent::paths::append_agent_log(&path, &msg);
105 if let Ok(mut g) = health.lock() {
106 g.loop_running = false;
107 g.record_error(&msg);
108 }
109 }
110 }))
111 } else {
112 None
113 };
114
115 if will_spawn_agent {
117 tokio::time::sleep(std::time::Duration::from_millis(300)).await;
118 }
119
120 let spread_snapshot = new_spread_snapshot();
121 if let (Ok(market), Ok(trader)) = (
122 runtime.build_market_api(),
123 runtime.build_api(),
124 ) {
125 let _spread_feed = spawn_spread_mark_feed(
126 rules_path.clone(),
127 runtime.simulate,
128 spread_snapshot.clone(),
129 market,
130 trader,
131 );
132 }
133
134 let watch_config = WatchConfig {
135 rules_path: rules_path.clone(),
136 agent_mode,
137 market_snapshot,
138 market_conditions,
139 agent_health,
140 spread_snapshot,
141 };
142
143 let watch_result = tokio::task::spawn_blocking(move || run_watch_tui(&watch_config))
144 .await
145 .context("watch UI thread panicked")?;
146
147 if let Some(handle) = agent_handle {
148 handle.abort();
149 let _ = handle.await;
150 }
151
152 watch_result
153}
154
155pub async fn run_rules(runtime: &RuntimeConfig, command: RulesCommands) -> Result<()> {
156 match command {
157 RulesCommands::Show { file } => show_rules(runtime, file).await,
158 RulesCommands::List => {
159 if runtime.output == crate::output::OutputFormat::Json {
160 list_rules_files(runtime);
161 } else {
162 println!("\n{}", list_rules_files_display());
163 println!();
164 }
165 Ok(())
166 }
167 }
168}