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,
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 extra.push("--trust".into());
138 extra.push("--yes".into());
139 let pid = spawn_background(&rules_path, &extra)?;
140 runtime.emit(ResponseEnvelope::ok(
141 "agent background",
142 serde_json::json!({
143 "pid": pid,
144 "rules": rules_path,
145 "log_file": log_path(&rules_path),
146 }),
147 ));
148 Ok(())
149}
150
151async fn stop_agent(runtime: &RuntimeConfig, file: Option<PathBuf>) -> Result<()> {
152 let rules_path = resolve_rules_file(file, true)?;
153 stop_daemon(&rules_path)?;
154 runtime.emit(ResponseEnvelope::ok(
155 "agent stop",
156 serde_json::json!({ "stopped": true, "rules": rules_path }),
157 ));
158 Ok(())
159}
160
161fn print_menu_help() {
162 println!(
163 r#"
164 schwab dashboard [rules.yaml] Rich status dashboard
165 schwab watch [rules.yaml] Live TUI + agent (q stops both)
166 schwab watch --monitor-only Attach to running daemon only
167 schwab rules show [rules.yaml] Full rules breakdown
168 schwab rules list Discover rules/*.yaml
169 schwab agent run --background Start daemon
170 schwab agent stop Stop daemon
171
172 Set SCHWAB_RULES=path/to/rules.yaml to skip file prompts.
173 Agent mode: add --json for machine-readable output.
174"#
175 );
176}
177
178pub fn list_rules_files(runtime: &RuntimeConfig) {
179 use crate::ui::discover::discover_rules_files;
180 let files = discover_rules_files();
181 runtime.emit(ResponseEnvelope::ok(
182 "rules list",
183 serde_json::json!({
184 "files": files,
185 "env": "SCHWAB_RULES",
186 }),
187 ));
188}