use serde_json::Value;
use super::super::helpers::optional_string;
pub(super) fn timeline_entry_title(entry_type: &str, item: &Value) -> Option<String> {
match entry_type {
"userMessage" => Some("你".to_string()),
"agentMessage" => {
let phase = optional_string(item, "phase");
if matches!(phase.as_deref(), Some("final_answer")) {
Some("最终回复".to_string())
} else if matches!(phase.as_deref(), Some("commentary")) {
Some("中间回复".to_string())
} else {
Some("Codex".to_string())
}
}
"hookPrompt" => Some("系统提示".to_string()),
"plan" => Some("执行计划".to_string()),
"reasoning" | "reasoning_text" | "summary_text" => Some("思考过程".to_string()),
"commandExecution" => {
optional_string(item, "command").or_else(|| Some("命令输出".to_string()))
}
"fileChange" => Some("文件改动".to_string()),
"mcpToolCall" => {
let server = optional_string(item, "server");
let tool = optional_string(item, "tool");
match (server, tool) {
(Some(server), Some(tool)) => Some(format!("{server} / {tool}")),
(_, Some(tool)) => Some(tool),
_ => Some("MCP 工具".to_string()),
}
}
"dynamicToolCall" => optional_string(item, "tool").or_else(|| Some("动态工具".to_string())),
"collabToolCall" => optional_string(item, "tool").or_else(|| Some("协作代理".to_string())),
"webSearch" => optional_string(item, "query").or_else(|| Some("网络搜索".to_string())),
"imageView" => Some("查看图片".to_string()),
"imageGeneration" => Some("图像生成".to_string()),
"function_call" => optional_string(item, "name").or_else(|| Some("函数调用".to_string())),
"function_call_output" => {
optional_string(item, "name").or_else(|| Some("函数结果".to_string()))
}
"custom_tool_call" => {
optional_string(item, "name").or_else(|| Some("自定义工具".to_string()))
}
"custom_tool_call_output" => {
optional_string(item, "name").or_else(|| Some("自定义工具结果".to_string()))
}
"file_search_call" => Some("文件搜索".to_string()),
"web_search_call" => Some("网络搜索".to_string()),
"code_interpreter_call" => Some("代码解释器".to_string()),
"shell_call" | "local_shell_call" => Some("Shell 调用".to_string()),
"shell_call_output" | "local_shell_call_output" => Some("Shell 输出".to_string()),
"apply_patch_call" => Some("补丁调用".to_string()),
"apply_patch_call_output" => Some("补丁结果".to_string()),
"image_generation_call" => Some("图像生成".to_string()),
"mcp_call" => {
let server = optional_string(item, "server_label");
let tool = optional_string(item, "name");
match (server, tool) {
(Some(server), Some(tool)) => Some(format!("{server} / {tool}")),
(_, Some(tool)) => Some(tool),
_ => Some("MCP 调用".to_string()),
}
}
"mcp_approval_request" => Some("MCP 审批请求".to_string()),
"mcp_approval_response" => Some("MCP 审批结果".to_string()),
"mcp_list_tools" => Some("MCP 工具列表".to_string()),
"enteredReviewMode" => Some("进入 Review 模式".to_string()),
"exitedReviewMode" => Some("退出 Review 模式".to_string()),
"contextCompaction" | "compaction" => Some("上下文压缩".to_string()),
other => Some(other.to_string()),
}
}
pub(super) fn timeline_entry_status(
entry_type: &str,
item: &Value,
is_streaming: bool,
) -> Option<String> {
let explicit_status = match entry_type {
"userMessage"
| "agentMessage"
| "hookPrompt"
| "reasoning"
| "plan"
| "reasoning_text"
| "summary_text"
| "commandExecution"
| "fileChange"
| "mcpToolCall"
| "dynamicToolCall"
| "collabToolCall"
| "imageGeneration"
| "function_call"
| "function_call_output"
| "custom_tool_call"
| "custom_tool_call_output"
| "file_search_call"
| "web_search_call"
| "computer_call"
| "computer_call_output"
| "code_interpreter_call"
| "shell_call"
| "shell_call_output"
| "local_shell_call"
| "local_shell_call_output"
| "apply_patch_call"
| "apply_patch_call_output"
| "image_generation_call"
| "mcp_call"
| "mcp_approval_request"
| "mcp_approval_response"
| "mcp_list_tools" => optional_string(item, "status"),
_ => None,
};
explicit_status.or_else(|| is_streaming.then(|| "inProgress".to_string()))
}
pub(super) fn timeline_text_from_thread_item(entry_type: &str, item: &Value) -> String {
match entry_type {
"userMessage" => item
.get("content")
.and_then(Value::as_array)
.map(|items| extract_content_items_text(items))
.filter(|text| !text.trim().is_empty())
.or_else(|| optional_string(item, "text"))
.unwrap_or_default(),
"hookPrompt" => pretty_json(item.get("fragments").unwrap_or(&Value::Null)),
"agentMessage" => optional_string(item, "text")
.or_else(|| {
item.get("content")
.and_then(Value::as_array)
.map(|items| extract_content_items_text(items))
})
.unwrap_or_default(),
"plan" => optional_string(item, "text").unwrap_or_default(),
"reasoning" | "reasoning_text" | "summary_text" => build_reasoning_text(
item.get("summary")
.and_then(Value::as_array)
.map(Vec::as_slice),
item.get("content")
.and_then(Value::as_array)
.map(Vec::as_slice),
),
"commandExecution" => optional_string(item, "aggregatedOutput").unwrap_or_default(),
"fileChange" => format_file_changes(item.get("changes").and_then(Value::as_array)),
"mcpToolCall" => format_tool_result(item, &["result", "error", "arguments"]),
"dynamicToolCall" => format_tool_result(item, &["contentItems", "arguments"]),
"collabToolCall" => format_tool_result(item, &["agentsStates", "prompt"]),
"webSearch" => build_web_search_text(item),
"imageView" => optional_string(item, "path").unwrap_or_default(),
"imageGeneration" => optional_string(item, "result").unwrap_or_default(),
"function_call" => format_tool_result(item, &["arguments"]),
"function_call_output" => format_tool_result(item, &["output"]),
"custom_tool_call" => format_tool_result(item, &["input", "arguments", "call_input"]),
"custom_tool_call_output" => format_tool_result(item, &["output"]),
"file_search_call" => format_tool_result(item, &["queries", "results"]),
"web_search_call" => format_tool_result(item, &["query", "action", "results"]),
"computer_call" => format_tool_result(item, &["action", "arguments"]),
"computer_call_output" => format_tool_result(item, &["output"]),
"code_interpreter_call" => format_tool_result(item, &["code", "outputs"]),
"shell_call" | "local_shell_call" => {
format_tool_result(item, &["action", "command", "commands"])
}
"shell_call_output" | "local_shell_call_output" => {
format_tool_result(item, &["output", "stdout", "stderr"])
}
"apply_patch_call" => format_tool_result(item, &["operation"]),
"apply_patch_call_output" => format_tool_result(item, &["output"]),
"image_generation_call" => optional_string(item, "result").unwrap_or_default(),
"mcp_call" => format_tool_result(item, &["arguments", "output", "error"]),
"mcp_approval_request" => format_tool_result(item, &["arguments", "reason"]),
"mcp_approval_response" => format_tool_result(item, &["reason"]),
"mcp_list_tools" => format_tool_result(item, &["tools", "error"]),
"enteredReviewMode" => "已进入 Review 模式。".to_string(),
"exitedReviewMode" => "已退出 Review 模式。".to_string(),
"contextCompaction" | "compaction" => "上下文已压缩。".to_string(),
_ => pretty_json(item),
}
}
pub(super) fn build_reasoning_text(summary: Option<&[Value]>, content: Option<&[Value]>) -> String {
let summary_text = summary
.map(extract_content_items_text)
.unwrap_or_default()
.trim()
.to_string();
let content_text = content
.map(extract_content_items_text)
.unwrap_or_default()
.trim()
.to_string();
match (summary_text.is_empty(), content_text.is_empty()) {
(false, false) => format!("思考摘要\n{summary_text}\n\n思考内容\n{content_text}"),
(false, true) => summary_text,
(true, false) => content_text,
(true, true) => String::new(),
}
}
pub(super) fn format_plan_payload(explanation: Option<&str>, plan: Option<&Vec<Value>>) -> String {
let mut lines = Vec::new();
if let Some(explanation) = explanation.filter(|value| !value.trim().is_empty()) {
lines.push(explanation.trim().to_string());
}
if let Some(plan) = plan {
for step in plan {
let step_text = optional_string(step, "step").unwrap_or_else(|| pretty_json(step));
let status = optional_string(step, "status")
.map(|value| format_plan_status(&value))
.unwrap_or("待处理");
lines.push(format!("[{status}] {step_text}"));
}
}
lines.join("\n")
}
fn extract_content_items_text(items: &[Value]) -> String {
items
.iter()
.filter_map(extract_content_item_text)
.collect::<Vec<_>>()
.join("\n")
}
fn extract_content_item_text(item: &Value) -> Option<String> {
let item_type = optional_string(item, "type");
let text = optional_string(item, "text");
match item_type.as_deref() {
Some("input_text")
| Some("output_text")
| Some("text")
| Some("reasoning_text")
| Some("summary_text") => text,
Some("input_image") => {
optional_string(item, "image_url").map(|url| format!("[图片] {url}"))
}
Some("input_file") => optional_string(item, "filename")
.or_else(|| optional_string(item, "file_id"))
.map(|name| format!("[文件] {name}")),
Some("refusal") => optional_string(item, "refusal"),
_ => text.or_else(|| {
(!item.is_null() && !item.is_object())
.then(|| item.to_string())
.or_else(|| {
let pretty = pretty_json(item);
(!pretty.is_empty()).then_some(pretty)
})
}),
}
}
fn build_web_search_text(item: &Value) -> String {
let query = optional_string(item, "query").unwrap_or_default();
let action = item.get("action").cloned().unwrap_or(Value::Null);
if action.is_null() {
return query;
}
if query.trim().is_empty() {
return pretty_json(&action);
}
format!("{query}\n\n{}", pretty_json(&action))
}
fn format_tool_result(item: &Value, keys: &[&str]) -> String {
let sections = keys
.iter()
.filter_map(|key| {
item.get(*key)
.filter(|value| !value.is_null())
.map(|value| {
let label = match *key {
"arguments" => "参数",
"result" | "contentItems" => "结果",
"error" => "错误",
"agentsStates" => "代理状态",
"prompt" => "提示词",
_ => *key,
};
format!("{label}\n{}", pretty_json(value))
})
})
.collect::<Vec<_>>();
sections.join("\n\n")
}
fn format_file_changes(changes: Option<&Vec<Value>>) -> String {
let Some(changes) = changes else {
return String::new();
};
let summary = changes
.iter()
.map(|change| {
let kind = optional_string(change, "type")
.or_else(|| optional_string(change, "status"))
.unwrap_or_else(|| "change".to_string());
let path = optional_string(change, "path")
.or_else(|| optional_string(change, "filePath"))
.unwrap_or_else(|| pretty_json(change));
format!("{kind}: {path}")
})
.collect::<Vec<_>>()
.join("\n");
if summary.trim().is_empty() {
pretty_json(&Value::Array(changes.clone()))
} else {
summary
}
}
fn pretty_json(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::String(text) => text.clone(),
_ => serde_json::to_string_pretty(value).unwrap_or_default(),
}
}
fn format_plan_status(status: &str) -> &'static str {
match status {
"completed" => "已完成",
"inProgress" | "in_progress" => "进行中",
"pending" => "待处理",
_ => "待处理",
}
}