1use std::io::{self, Write};
2use std::path::PathBuf;
3
4use anyhow::Result;
5use inquire::Select;
6
7use crate::agent::{log_path, spawn_background, stop_daemon};
8use crate::config::RuntimeConfig;
9use crate::output::ResponseEnvelope;
10use crate::rules::RulesConfig;
11use crate::ui::context::{tail_lines, DashboardContext};
12use crate::ui::dashboard::render_dashboard;
13use crate::ui::discover::{list_rules_files_display, resolve_rules_file};
14use crate::ui::rules_view::render_rules_detail;
15
16pub async fn run_interactive_menu(runtime: &RuntimeConfig) -> Result<()> {
17 loop {
18 let choice = Select::new("schwab — what next?", menu_choices())
19 .with_help_message("Human mode interactive menu")
20 .prompt();
21
22 let choice = match choice {
23 Ok(c) => c,
24 Err(_) => break,
25 };
26
27 match choice.as_str() {
28 "Dashboard" => show_dashboard(runtime, None).await?,
29 "Watch agent (live)" => {
30 crate::commands::ui_cmd::run_watch(runtime, None, false).await?;
31 }
32 "Show rules" => show_rules(runtime, None).await?,
33 "Tail agent log" => tail_agent_log(None)?,
34 "Validate rules" => validate_rules(runtime, None).await?,
35 "Run agent (background)" => run_agent_background(runtime, None).await?,
36 "Stop agent" => stop_agent(runtime, None).await?,
37 "List rules files" => {
38 println!("\n{}", list_rules_files_display());
39 println!();
40 }
41 "Help" => print_menu_help(),
42 "Quit" => break,
43 _ => {}
44 }
45 }
46 Ok(())
47}
48
49fn menu_choices() -> Vec<String> {
50 vec![
51 "Dashboard".into(),
52 "Watch agent (live)".into(),
53 "Show rules".into(),
54 "Tail agent log".into(),
55 "Validate rules".into(),
56 "Run agent (background)".into(),
57 "Stop agent".into(),
58 "List rules files".into(),
59 "Help".into(),
60 "Quit".into(),
61 ]
62}
63
64pub async fn show_dashboard(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
65 let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
66 let ctx = DashboardContext::load(&rules_path)?;
67
68 if runtime.output == crate::output::OutputFormat::Json {
69 runtime.emit(ResponseEnvelope::ok("dashboard", ctx.to_json()));
70 return Ok(());
71 }
72
73 print!("{}", render_dashboard(&ctx));
74 io::stdout().flush().ok();
75 Ok(())
76}
77
78pub async fn show_rules(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
79 let rules_path = resolve_rules_file(file, runtime.is_interactive())?;
80 let ctx = DashboardContext::load(&rules_path)?;
81
82 if runtime.output == crate::output::OutputFormat::Json {
83 runtime.emit(ResponseEnvelope::ok("rules show", ctx.to_json()));
84 return Ok(());
85 }
86
87 print!("{}", render_rules_detail(&ctx));
88 io::stdout().flush().ok();
89 Ok(())
90}
91
92pub fn tail_agent_log(file: Option<PathBuf>) -> Result<()> {
93 let rules_path = resolve_rules_file(file, true)?;
94 let log = log_path(&rules_path);
95 let lines = tail_lines(&log, 40);
96 if lines.is_empty() {
97 println!("\n (no log at {})\n", log.display());
98 return Ok(());
99 }
100 println!("\n Agent log — {}\n", log.display());
101 for line in lines {
102 println!(" {line}");
103 }
104 println!();
105 Ok(())
106}
107
108async fn validate_rules(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
109 let rules_path = resolve_rules_file(file, true)?;
110 let rules = RulesConfig::load(&rules_path)?;
111 runtime.emit(ResponseEnvelope::ok(
112 "agent validate",
113 serde_json::json!({
114 "valid": true,
115 "agent_id": rules.agent_id,
116 "rules_path": rules_path,
117 "accounts": rules.accounts.len(),
118 "watchlist": rules.watchlist_items(),
119 "llm_enabled": rules.llm.enabled,
120 "telegram_enabled": rules.notify.telegram.enabled,
121 }),
122 ));
123 Ok(())
124}
125
126async fn run_agent_background(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
127 let rules_path = resolve_rules_file(file, true)?;
128 if !runtime.trust || !runtime.yes {
129 anyhow::bail!(
130 "background agent requires --trust --yes (live trading guardrails still apply)"
131 );
132 }
133 let mut extra = Vec::new();
134 if runtime.dry_run {
135 extra.push("--dry-run".into());
136 }
137 if runtime.simulate {
138 extra.push("--simulate".into());
139 }
140 extra.push("--trust".into());
141 extra.push("--yes".into());
142 let pid = spawn_background(&rules_path, &extra)?;
143 runtime.emit(ResponseEnvelope::ok(
144 "agent background",
145 serde_json::json!({
146 "pid": pid,
147 "rules": rules_path,
148 "log_file": log_path(&rules_path),
149 }),
150 ));
151 Ok(())
152}
153
154async fn stop_agent(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
155 let rules_path = resolve_rules_file(file, true)?;
156 stop_daemon(&rules_path)?;
157 runtime.emit(ResponseEnvelope::ok(
158 "agent stop",
159 serde_json::json!({ "stopped": true, "rules": rules_path }),
160 ));
161 Ok(())
162}
163
164fn print_menu_help() {
165 println!(
166 r#"
167 schwab dashboard [rules.yaml] Rich status dashboard
168 schwab watch [rules.yaml] Live TUI + agent (q stops both)
169 schwab watch --monitor-only Attach to running daemon only
170 schwab rules show [rules.yaml] Full rules breakdown
171 schwab rules list Discover rules/*.yaml
172 schwab agent run --background Start daemon
173 schwab agent stop Stop daemon
174
175 Set SCHWAB_RULES=path/to/rules.yaml to skip file prompts.
176 Agent mode: add --json for machine-readable output.
177"#
178 );
179}
180
181pub fn list_rules_files(runtime: &RuntimeConfig) {
182 use crate::ui::discover::discover_rules_files;
183 let files = discover_rules_files();
184 runtime.emit(ResponseEnvelope::ok(
185 "rules list",
186 serde_json::json!({
187 "files": files,
188 "env": "SCHWAB_RULES",
189 }),
190 ));
191}