use crate::context::ContextReader;
use crate::message::{
AssistantResponse, Context, ContextListing, ConversationMessage, Entry, MessageKind,
TextContent, ToolCall,
};
use anyhow::Context as _;
use rusqlite::Connection;
use serde_json::Value;
use std::path::PathBuf;
pub struct OpenCodeReader {
db_path: PathBuf,
}
impl OpenCodeReader {
pub fn new(db_path: PathBuf) -> Self {
Self { db_path }
}
}
impl ContextReader for OpenCodeReader {
fn list_contexts(&self) -> anyhow::Result<Vec<ContextListing>> {
let conn = Connection::open(&self.db_path)
.with_context(|| format!("failed to open opencode db at {}", self.db_path.display()))?;
let sql = "SELECT session_id, COUNT(*), MIN(time_created), MAX(time_created) \
FROM message GROUP BY session_id ORDER BY MAX(time_created) DESC";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map([], |row| {
Ok((
row.get::<_, String>(0)?,
row.get::<_, i64>(1)?,
row.get::<_, i64>(2)?,
row.get::<_, i64>(3)?,
))
})?;
let mut contexts = Vec::new();
for row in rows {
let (session_id, count, min_time, max_time) = row?;
let detail = format!(
"{} messages, {} to {}",
count,
format_time(min_time),
format_time(max_time)
);
contexts.push(ContextListing {
id: session_id,
detail,
path: Some(self.db_path.clone()),
});
}
Ok(contexts)
}
fn read_context(&self, context_id: &str) -> anyhow::Result<Context> {
let conn = Connection::open(&self.db_path)
.with_context(|| format!("failed to open opencode db at {}", self.db_path.display()))?;
let sql = "SELECT id, data FROM message WHERE session_id = ?1 ORDER BY time_created";
let mut stmt = conn.prepare(sql)?;
let rows = stmt.query_map(rusqlite::params![context_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
let mut messages_data: Vec<(String, Value)> = Vec::new();
for row in rows {
let (id, data_str) = row?;
let data: Value = serde_json::from_str(&data_str)?;
messages_data.push((id, data));
}
let part_sql =
"SELECT message_id, data FROM part WHERE session_id = ?1 ORDER BY time_created";
let mut part_stmt = conn.prepare(part_sql)?;
let part_rows = part_stmt.query_map(rusqlite::params![context_id], |row| {
Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
})?;
let mut parts: Vec<(String, Value)> = Vec::new();
for row in part_rows {
let (msg_id, data_str) = row?;
let data: Value = serde_json::from_str(&data_str)?;
parts.push((msg_id, data));
}
let mut entries = Vec::new();
let mut messages = Vec::new();
for (idx, (msg_id, msg_data)) in messages_data.iter().enumerate() {
let role = msg_data["role"].as_str().unwrap_or("unknown").to_string();
let entry_id = msg_data["id"]
.as_str()
.map_or_else(|| msg_id.clone(), std::string::ToString::to_string);
let parent_id = msg_data["parentID"]
.as_str()
.map(std::string::ToString::to_string)
.unwrap_or_default();
let msg_parts: Vec<&Value> = parts
.iter()
.filter(|(mid, _)| mid == msg_id)
.map(|(_, d)| d)
.collect();
let kind = build_message_kind(role.as_str(), &msg_parts);
messages.push(ConversationMessage {
entry_id: entry_id.clone(),
kind,
});
entries.push(Entry {
line: idx,
r#type: "message".to_string(),
id: entry_id.clone(),
parent_id,
});
}
Ok(Context {
path: self.db_path.clone(),
entries,
messages,
})
}
}
fn build_message_kind(role: &str, parts: &[&Value]) -> MessageKind {
match role {
"user" | "system" => {
let text: String = parts
.iter()
.filter(|p| p["type"].as_str() == Some("text"))
.map(|p| p["text"].as_str().unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\n");
MessageKind::TextContent(TextContent {
role: role.to_string(),
text,
})
}
"assistant" => {
let mut thinking = Vec::new();
let mut tool_calls = Vec::new();
let mut text = String::new();
for part in parts {
match part["type"].as_str() {
Some("text") => {
text.push_str(part["text"].as_str().unwrap_or(""));
}
Some("reasoning") => {
if let Some(t) = part["text"].as_str() {
thinking.push(t.to_string());
}
}
Some("tool") => {
if let Some(tc) = parse_tool_call(part) {
tool_calls.push(tc);
}
}
_ => {}
}
}
MessageKind::AssistantResponse(AssistantResponse {
thinking,
tool_calls,
text,
})
}
_ => {
let text: String = parts
.iter()
.filter(|p| p["type"].as_str() == Some("text"))
.map(|p| p["text"].as_str().unwrap_or("").to_string())
.collect::<Vec<_>>()
.join("\n");
MessageKind::TextContent(TextContent {
role: role.to_string(),
text,
})
}
}
}
fn parse_tool_call(part: &Value) -> Option<ToolCall> {
let name = part["name"].as_str()?;
let arguments = part["arguments"].clone();
Some(ToolCall {
name: name.to_string(),
arguments,
})
}
fn format_time(ts: i64) -> String {
if ts <= 0 {
return "unknown".to_string();
}
ts.to_string()
}