use crate::console::{ConsoleRenderer, ConsoleRendererConfig, SyncMemorySink};
use crate::html_report::{build_html_report, write_html_report, HtmlReport, HtmlReportParams};
use monoloop_connector_cursor::{CursorAgentConfig, CursorAgentHandle, CursorSessionConfig};
use monoloop_contracts::{
CanonicalUnit, DialectBinding, DialectDescriptor, InterpretationId, InterpretationLimits,
InterpreterOutputEvent,
};
use monoloop_interpreter::{DefaultInterpreterFactory, InterpreterFactory, StartInterpretation};
use std::path::PathBuf;
use std::time::Duration;
#[derive(Clone, Debug)]
pub struct LiveCursorRunOptions {
pub prompt: String,
pub cwd: PathBuf,
pub agent: CursorAgentConfig,
pub session: CursorSessionConfig,
pub title: String,
pub artifact_stem: PathBuf,
pub render_console: bool,
pub drain_after_prompt: Duration,
}
impl LiveCursorRunOptions {
pub fn for_project(project: impl Into<PathBuf>, prompt: impl Into<String>) -> Self {
let project = project.into();
let stem = project.join("target/live_cursor_run");
let mut agent = CursorAgentConfig::for_project(project.clone());
agent.raw_dump_path = Some(PathBuf::from(format!("{}.raw.txt", stem.display())));
agent.rpc_deadline = Duration::from_secs(10 * 60);
agent = agent.with_auto_allow_permissions();
Self {
prompt: prompt.into(),
cwd: project.clone(),
agent,
session: CursorSessionConfig::new(project).with_agent_mode(),
title: "Live Cursor ACP — interpretation review".into(),
artifact_stem: stem,
render_console: true,
drain_after_prompt: Duration::from_millis(200),
}
}
pub fn with_ask_mode(mut self) -> Self {
self.session = self.session.with_ask_mode();
self
}
pub fn with_agent_mode(mut self) -> Self {
self.session = self.session.with_agent_mode();
self
}
}
#[derive(Clone, Debug)]
pub struct LiveCursorArtifactPaths {
pub html: PathBuf,
pub raw: PathBuf,
pub sequence: PathBuf,
pub chat: PathBuf,
}
#[derive(Clone, Debug)]
pub struct LiveCursorRunReport {
pub session_id: String,
pub prompt_result: String,
pub events: Vec<InterpreterOutputEvent>,
pub html: HtmlReport,
pub console_text: String,
pub sequence_text: String,
pub paths: LiveCursorArtifactPaths,
}
pub async fn run_live_cursor_prompt(
opts: LiveCursorRunOptions,
) -> Result<LiveCursorRunReport, String> {
if let Some(parent) = opts.artifact_stem.parent() {
std::fs::create_dir_all(parent).map_err(|e| e.to_string())?;
}
let mut agent = CursorAgentHandle::connect(opts.agent.clone())
.await
.map_err(|e| e.to_string())?;
let mut updates = agent.take_updates();
let mut session_cfg = opts.session.clone();
session_cfg.cwd = opts.cwd.clone();
let session = agent
.session_new(session_cfg)
.await
.map_err(|e| e.to_string())?;
let session_id = session.session_id.clone();
let dialect = DialectBinding::negotiated(DialectDescriptor::cursor_acp("1"));
let factory = DefaultInterpreterFactory::new();
let interp = factory
.start(StartInterpretation {
interpretation_id: InterpretationId::generate(),
connection_id: monoloop_contracts::ConnectionId::new("cursor-live"),
external_session_id: Some(session.external_session_id()),
dialect,
limits: InterpretationLimits::default(),
})
.map_err(|e| e.to_string())?;
let input = interp.input.clone();
let pump = tokio::spawn(async move {
while let Some(bytes) = updates.recv().await {
if input.push_bytes(bytes).await.is_err() {
break;
}
}
});
let prompt_result = session
.prompt_text(&opts.prompt)
.await
.map_err(|e| e.to_string())?;
let prompt_result_s = prompt_result.to_string();
tokio::time::sleep(opts.drain_after_prompt).await;
let dump_text = agent.raw_dump_text();
let _ = interp.input.finish_clean().await;
agent.shutdown().await;
let _ = pump.await;
let mut events = Vec::new();
let sink = std::sync::Arc::new(SyncMemorySink::new());
let console = ConsoleRenderer::new(ConsoleRendererConfig::default(), sink.clone());
loop {
match interp.events.recv().await {
Some(ev) => {
if opts.render_console {
console.render(&ev);
}
let done = matches!(ev, InterpreterOutputEvent::Ended(_));
events.push(ev);
if done {
break;
}
}
None => break,
}
}
let html = build_html_report(
&events,
&HtmlReportParams {
title: opts.title.clone(),
..HtmlReportParams::default()
},
);
let html_path = PathBuf::from(format!("{}.html", opts.artifact_stem.display()));
write_html_report(&html_path, &html).map_err(|e| e.to_string())?;
let raw_path = PathBuf::from(format!("{}.raw.txt", opts.artifact_stem.display()));
if !dump_text.is_empty() {
let _ = std::fs::write(&raw_path, &dump_text);
} else if !raw_path.is_file() {
let _ = std::fs::write(&raw_path, "");
}
let mut sequence_text = String::from("=== LIVE CURSOR — CANONICAL TEXT ===\n");
for (i, e) in events.iter().enumerate() {
if let InterpreterOutputEvent::Unit(u) = e {
if let CanonicalUnit::Text(t) = &u.snapshot().unit {
sequence_text.push_str(&format!("{i:04} | {}\n", t.content));
}
}
}
sequence_text.push_str(&format!(
"\nsessionId={session_id}\nprompt_result={prompt_result_s}\n"
));
let seq_path = PathBuf::from(format!("{}.sequence.txt", opts.artifact_stem.display()));
std::fs::write(&seq_path, &sequence_text).map_err(|e| e.to_string())?;
let chat_path = PathBuf::from(format!("{}.chat.txt", opts.artifact_stem.display()));
std::fs::write(&chat_path, &html.chat_projection.plain_text).map_err(|e| e.to_string())?;
let console_text = sink.join();
Ok(LiveCursorRunReport {
session_id,
prompt_result: prompt_result_s,
events,
html,
console_text,
sequence_text,
paths: LiveCursorArtifactPaths {
html: html_path,
raw: raw_path,
sequence: seq_path,
chat: chat_path,
},
})
}