feat: add opencode ai agent option
This commit is contained in:
171
src-tauri/src/opencode_events.rs
Normal file
171
src-tauri/src/opencode_events.rs
Normal file
@@ -0,0 +1,171 @@
|
||||
use crate::ai_agents::AiAgentStreamEvent;
|
||||
|
||||
pub(crate) fn parse_line<F>(
|
||||
line: Result<String, std::io::Error>,
|
||||
emit: &mut F,
|
||||
) -> Option<serde_json::Value>
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
let line = match line {
|
||||
Ok(line) => line,
|
||||
Err(error) => {
|
||||
emit(AiAgentStreamEvent::Error {
|
||||
message: format!("Read error: {error}"),
|
||||
});
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
serde_json::from_str::<serde_json::Value>(trimmed).ok()
|
||||
}
|
||||
|
||||
pub(crate) fn dispatch_event<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
if json["type"].as_str() == Some("session") {
|
||||
emit_session_event(json, emit);
|
||||
}
|
||||
|
||||
match json["type"].as_str().unwrap_or_default() {
|
||||
"message" | "text" => emit_text(json, emit),
|
||||
"reasoning" => emit_reasoning(json, emit),
|
||||
"tool_use" | "tool" => emit_tool_start(json, emit),
|
||||
"tool_result" | "tool_done" => emit_tool_done(json, emit),
|
||||
"error" => emit_error_event(json, emit),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn session_id(json: &serde_json::Value) -> Option<&str> {
|
||||
json["sessionID"]
|
||||
.as_str()
|
||||
.or_else(|| json["session_id"].as_str())
|
||||
.or_else(|| json["session"]["id"].as_str())
|
||||
}
|
||||
|
||||
pub(crate) fn format_error(stderr_output: String, status: String) -> String {
|
||||
let lower = stderr_output.to_ascii_lowercase();
|
||||
if is_auth_error(&lower) {
|
||||
return "OpenCode CLI is not authenticated or has no provider configured. Run `opencode auth login` or configure a provider in OpenCode, then retry.".into();
|
||||
}
|
||||
|
||||
if stderr_output.trim().is_empty() {
|
||||
format!("opencode exited with status {status}")
|
||||
} else {
|
||||
stderr_output.lines().take(3).collect::<Vec<_>>().join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_session_event<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
if let Some(session_id) = session_id(json) {
|
||||
emit(AiAgentStreamEvent::Init {
|
||||
session_id: session_id.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_text<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
if let Some(text) = text_value(json) {
|
||||
emit(AiAgentStreamEvent::TextDelta {
|
||||
text: text.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_reasoning<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
if let Some(text) = text_value(json) {
|
||||
emit(AiAgentStreamEvent::ThinkingDelta {
|
||||
text: text.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn emit_tool_start<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
let tool_id = tool_id(json).unwrap_or("tool").to_string();
|
||||
let tool_name = json["name"]
|
||||
.as_str()
|
||||
.or_else(|| json["tool"].as_str())
|
||||
.unwrap_or("tool")
|
||||
.to_string();
|
||||
let input = json.get("input").map(|input| input.to_string());
|
||||
|
||||
emit(AiAgentStreamEvent::ToolStart {
|
||||
tool_name,
|
||||
tool_id,
|
||||
input,
|
||||
});
|
||||
}
|
||||
|
||||
fn emit_tool_done<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
let tool_id = tool_id(json).unwrap_or("tool").to_string();
|
||||
let output = json["output"]
|
||||
.as_str()
|
||||
.map(|output| output.to_string())
|
||||
.or_else(|| json.get("result").map(|result| result.to_string()));
|
||||
|
||||
emit(AiAgentStreamEvent::ToolDone { tool_id, output });
|
||||
}
|
||||
|
||||
fn emit_error_event<F>(json: &serde_json::Value, emit: &mut F)
|
||||
where
|
||||
F: FnMut(AiAgentStreamEvent),
|
||||
{
|
||||
if let Some(message) = message_value(json) {
|
||||
emit(AiAgentStreamEvent::Error {
|
||||
message: message.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_id(json: &serde_json::Value) -> Option<&str> {
|
||||
json["id"]
|
||||
.as_str()
|
||||
.or_else(|| json["toolID"].as_str())
|
||||
.or_else(|| json["tool_id"].as_str())
|
||||
}
|
||||
|
||||
fn text_value(json: &serde_json::Value) -> Option<&str> {
|
||||
json["text"]
|
||||
.as_str()
|
||||
.or_else(|| json["content"].as_str())
|
||||
.or_else(|| json["message"].as_str())
|
||||
}
|
||||
|
||||
fn message_value(json: &serde_json::Value) -> Option<&str> {
|
||||
json["message"]
|
||||
.as_str()
|
||||
.or_else(|| json["error"].as_str())
|
||||
.or_else(|| json["text"].as_str())
|
||||
}
|
||||
|
||||
fn is_auth_error(lower: &str) -> bool {
|
||||
["auth", "login", "sign in", "api key", "provider"]
|
||||
.iter()
|
||||
.any(|pattern| lower.contains(pattern))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "opencode_events_tests.rs"]
|
||||
mod tests;
|
||||
Reference in New Issue
Block a user