1use crate::console::{ConsoleRenderer, ConsoleRendererConfig, SyncMemorySink};
6use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
7use monoloop_connector_claude::{run_claude_print, ClaudeAgentConfig};
8use monoloop_contracts::{
9 CanonicalUnit, DialectBinding, DialectDescriptor, ExternalSessionId, InterpretationId,
10 InterpretationLimits, InterpreterOutputEvent,
11};
12use monoloop_interpreter::{DefaultInterpreterFactory, InterpreterFactory, StartInterpretation};
13use std::path::PathBuf;
14use std::time::Duration;
15use tokio::sync::mpsc;
16
17#[derive(Clone, Debug)]
19pub struct LiveClaudeRunOptions {
20 pub prompt: String,
22 pub cwd: PathBuf,
24 pub agent: ClaudeAgentConfig,
26 pub title: String,
28 pub artifact_stem: PathBuf,
30 pub render_console: bool,
32}
33
34impl LiveClaudeRunOptions {
35 pub fn for_project(project: impl Into<PathBuf>, prompt: impl Into<String>) -> Self {
37 let project = project.into();
38 let stem = project.join("target/live_claude_run");
39 let mut agent = ClaudeAgentConfig::for_project(project.clone());
40 agent.raw_dump_path = Some(PathBuf::from(format!("{}.raw.txt", stem.display())));
41 agent.run_deadline = Duration::from_secs(15 * 60);
42 Self {
43 prompt: prompt.into(),
44 cwd: project,
45 agent,
46 title: "Live Claude Code — interpretation review".into(),
47 artifact_stem: stem,
48 render_console: true,
49 }
50 }
51}
52
53#[derive(Clone, Debug)]
55pub struct LiveClaudeArtifactPaths {
56 pub html: PathBuf,
58 pub raw: PathBuf,
60 pub sequence: PathBuf,
62 pub chat: PathBuf,
64}
65
66#[derive(Clone, Debug)]
68pub struct LiveClaudeRunReport {
69 pub session_id: String,
71 pub exit_code: Option<i32>,
73 pub events: Vec<InterpreterOutputEvent>,
75 pub html: HtmlReport,
77 pub console_text: String,
79 pub sequence_text: String,
81 pub paths: LiveClaudeArtifactPaths,
83}
84
85pub async fn run_live_claude_prompt(
87 opts: LiveClaudeRunOptions,
88) -> Result<LiveClaudeRunReport, String> {
89 if let Some(parent) = opts.artifact_stem.parent() {
90 std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
91 }
92
93 let mut agent = opts.agent.clone();
94 agent.cwd = opts.cwd.clone();
95 agent.raw_dump_path = Some(PathBuf::from(format!(
96 "{}.raw.txt",
97 opts.artifact_stem.display()
98 )));
99
100 let (tx, mut updates) = mpsc::channel(256);
101 let run = tokio::spawn({
102 let agent = agent.clone();
103 let prompt = opts.prompt.clone();
104 async move { run_claude_print(&agent, &prompt, tx).await }
105 });
106
107 let dialect = DialectBinding::negotiated(DialectDescriptor::claude_code("1"));
108 let factory = DefaultInterpreterFactory::new();
109 let interp = factory
110 .start(StartInterpretation {
111 interpretation_id: InterpretationId::generate(),
112 connection_id: monoloop_contracts::ConnectionId::new("claude-live"),
113 external_session_id: None,
114 dialect,
115 limits: InterpretationLimits::default(),
116 })
117 .map_err(|e| e.to_string())?;
118
119 let input = interp.input.clone();
120 let pump = tokio::spawn(async move {
121 while let Some(bytes) = updates.recv().await {
122 if input.push_bytes(bytes).await.is_err() {
123 break;
124 }
125 }
126 });
127
128 let outcome = run
129 .await
130 .map_err(|e| e.to_string())?
131 .map_err(|e| e.to_string())?;
132 let _ = pump.await;
133
134 let _ = ExternalSessionId::new(outcome.session_id.clone());
137
138 let _ = interp.input.finish_clean().await;
139
140 let mut events = Vec::new();
141 let sink = std::sync::Arc::new(SyncMemorySink::new());
142 let console = ConsoleRenderer::new(ConsoleRendererConfig::default(), sink.clone());
143 loop {
144 match interp.events.recv().await {
145 Some(ev) => {
146 if opts.render_console {
147 console.render(&ev);
148 }
149 let done = matches!(ev, InterpreterOutputEvent::Ended(_));
150 events.push(ev);
151 if done {
152 break;
153 }
154 }
155 None => break,
156 }
157 }
158
159 let html = build_html_report(
160 &events,
161 &HtmlReportParams {
162 title: opts.title.clone(),
163 ..HtmlReportParams::default()
164 },
165 );
166 let html_path = PathBuf::from(format!("{}.html", opts.artifact_stem.display()));
167 write_html_report(&html_path, &html).map_err(|e| e.to_string())?;
168
169 let raw_path = PathBuf::from(format!("{}.raw.txt", opts.artifact_stem.display()));
170 if !outcome.raw_dump_text.is_empty() {
171 let _ = std::fs::write(&raw_path, &outcome.raw_dump_text);
172 }
173
174 let mut sequence_text = String::from("=== LIVE CLAUDE — CANONICAL TEXT ===\n");
175 for (i, ev) in events.iter().enumerate() {
176 if let InterpreterOutputEvent::Unit(u) = ev {
177 if let CanonicalUnit::Text(t) = &u.snapshot().unit {
178 sequence_text.push_str(&format!("{i:04} | {}\n", t.content));
179 }
180 }
181 }
182 sequence_text.push_str(&format!(
183 "\nsession={} exit={:?} sentences={} strategy={:?} confidence={:?}\n",
184 outcome.session_id,
185 outcome.exit_code,
186 html.sentence_count,
187 html.chat_projection.strategy,
188 html.chat_projection.confidence
189 ));
190 let seq_path = PathBuf::from(format!("{}.sequence.txt", opts.artifact_stem.display()));
191 let _ = std::fs::write(&seq_path, &sequence_text);
192
193 let chat_path = PathBuf::from(format!("{}.chat.txt", opts.artifact_stem.display()));
194 let _ = std::fs::write(&chat_path, &html.chat_projection.plain_text);
195
196 Ok(LiveClaudeRunReport {
197 session_id: outcome.session_id,
198 exit_code: outcome.exit_code,
199 events,
200 html,
201 console_text: sink.join(),
202 sequence_text,
203 paths: LiveClaudeArtifactPaths {
204 html: html_path,
205 raw: raw_path,
206 sequence: seq_path,
207 chat: chat_path,
208 },
209 })
210}