use serde::Serialize;
use serde_json::Value;
use crate::index::IndexEntry;
use crate::message::{ConversationMessage, MessageView, ProviderId};
use crate::text;
use chrono::{DateTime, Utc};
#[derive(Serialize)]
struct SessionJson {
id: String,
parent: Option<String>,
provider: String,
cwd: String,
count: u32,
label: Option<String>,
from: Option<DateTime<Utc>>,
until: Option<DateTime<Utc>>,
}
impl From<&IndexEntry> for SessionJson {
fn from(entry: &IndexEntry) -> Self {
let ProviderId {
label,
cwd,
count,
from,
until,
} = &entry.provider_id;
Self {
id: entry.id.clone(),
parent: entry.parent_id.clone(),
provider: entry.provider.as_str().to_string(),
cwd: cwd.display().to_string(),
count: *count,
label: label.clone(),
from: *from,
until: *until,
}
}
}
pub fn print_sessions(
entries: &[&IndexEntry],
writer: &mut dyn std::io::Write,
) -> anyhow::Result<()> {
let rows: Vec<SessionJson> = entries.iter().map(|e| SessionJson::from(*e)).collect();
serde_json::to_writer(&mut *writer, &rows)?;
writeln!(writer)?;
Ok(())
}
pub fn searchable_text(msg: &ConversationMessage) -> String {
match msg.view() {
MessageView::Text { text, .. } => text::sanitize(&text),
MessageView::Assistant {
thinking,
text,
tool_calls,
} => {
let mut parts: Vec<String> = thinking;
if !text.is_empty() {
parts.push(text);
}
for tc in &tool_calls {
parts.push(format!(
"{} {}",
tc.name,
summarize_tool_args(&tc.arguments)
));
}
text::sanitize(&parts.join(" "))
}
MessageView::ToolResult(tr) => {
if tr.content.is_empty() {
text::sanitize(&tr.tool_name)
} else {
text::sanitize(&format!("{} {}", tr.tool_name, tr.content))
}
}
MessageView::Bash(bo) => {
if bo.command.is_empty() {
text::sanitize(&bo.output)
} else if bo.output.is_empty() {
text::sanitize(&bo.command)
} else {
text::sanitize(&format!("{} {}", bo.command, bo.output))
}
}
}
}
pub fn summarize_tool_args(arguments: &Value) -> String {
if let Some(obj) = arguments.as_object() {
if let Some(path) = path_argument(arguments) {
return format!("path={path}");
}
for key in &["command", "query", "pattern", "description"] {
if let Some(val) = obj.get(*key)
&& let Some(s) = val.as_str()
{
return format!("{}={}", key, text::clip(s, 160));
}
}
if obj.is_empty() {
return String::new();
}
let mut keys: Vec<&str> = obj.keys().map(std::string::String::as_str).collect();
keys.sort_unstable();
return keys.join(", ");
}
if let Some(s) = arguments.as_str() {
return text::clip(s, 160);
}
if arguments.is_null() {
return String::new();
}
text::clip(&arguments.to_string(), 160)
}
pub fn path_argument(arguments: &Value) -> Option<String> {
let obj = arguments.as_object()?;
for key in &["path", "file_path", "filePath", "file"] {
if let Some(val) = obj.get(*key)
&& let Some(s) = val.as_str()
{
return Some(s.to_string());
}
}
None
}