use std::sync::Arc;
use crate::tui_internal::app_server_session::AppServerSession;
use crate::tui_internal::git_action_directives::parse_assistant_markdown;
use crate::tui_internal::history_cell::AgentMarkdownCell;
use crate::tui_internal::history_cell::HistoryCell;
use crate::tui_internal::history_cell::PlainHistoryCell;
use crate::tui_internal::history_cell::ReasoningSummaryCell;
use crate::tui_internal::history_cell::UserHistoryCell;
use crate::tui_internal::history_cell::split_reasoning_summary_parts;
use crate::tui_internal::inline_visualization::InlineVisualizationContext;
use crate::tui_internal::multi_agents::sub_agent_activity_summary;
use lemurclaw_core::app_server_protocol::Thread;
use lemurclaw_core::app_server_protocol::ThreadItem;
use lemurclaw_core::app_server_protocol::UserInput;
use lemurclaw_core::protocol::ThreadId;
use lemurclaw_core::protocol::items::UserMessageItem;
use ratatui::style::Stylize as _;
use ratatui::text::Line;
pub(crate) type TranscriptCells = Vec<Arc<dyn HistoryCell>>;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum RawReasoningVisibility {
Hidden,
Visible,
}
pub(crate) async fn load_session_transcript(
app_server: &mut AppServerSession,
thread_id: ThreadId,
raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>,
) -> std::io::Result<TranscriptCells> {
let thread = app_server
.thread_read(thread_id, true)
.await
.map_err(std::io::Error::other)?;
Ok(thread_to_transcript_cells(
thread,
raw_reasoning_visibility,
codex_home,
))
}
pub(crate) fn thread_to_transcript_cells(
thread: Thread,
raw_reasoning_visibility: RawReasoningVisibility,
codex_home: Option<&std::path::Path>,
) -> TranscriptCells {
let cwd = thread.cwd;
let inline_visualization_context = codex_home.and_then(|codex_home| {
ThreadId::from_string(&thread.id)
.ok()
.and_then(|thread_id| InlineVisualizationContext::new(codex_home, thread_id))
});
let mut cells: TranscriptCells = Vec::new();
for item in thread.turns.into_iter().flat_map(|turn| turn.items) {
match item {
ThreadItem::UserMessage {
id,
client_id,
content,
} => {
if content.iter().any(|input| {
matches!(
input,
UserInput::Audio { .. } | UserInput::LocalAudio { .. }
)
}) {
tracing::warn!(
user_message_id = id,
"audio user inputs are not supported by the TUI and will be omitted"
);
}
let item = UserMessageItem {
id,
client_id,
content: content
.into_iter()
.map(lemurclaw_core::app_server_protocol::UserInput::into_core)
.collect(),
};
cells.push(Arc::new(UserHistoryCell {
message: item.message(),
text_elements: item.text_elements(),
local_image_paths: item.local_image_paths(),
remote_image_urls: item.image_urls(),
}));
}
ThreadItem::AgentMessage { text, .. } => {
let parsed = parse_assistant_markdown(&text, cwd.as_path());
if !parsed.visible_markdown.trim().is_empty() {
cells.push(Arc::new(AgentMarkdownCell::new_with_inline_visualizations(
parsed.visible_markdown,
cwd.as_path(),
inline_visualization_context.clone(),
)));
}
}
ThreadItem::Plan { text, .. } => {
if !text.trim().is_empty() {
cells.push(Arc::new(crate::tui_internal::history_cell::new_proposed_plan(
text,
cwd.as_path(),
)));
}
}
ThreadItem::Reasoning {
summary, content, ..
} => {
let (header, text) =
if matches!(raw_reasoning_visibility, RawReasoningVisibility::Visible)
&& !content.is_empty()
{
("Reasoning".to_string(), content.join("\n\n"))
} else {
split_reasoning_summary_parts(&summary)
};
if !text.trim().is_empty() {
cells.push(Arc::new(ReasoningSummaryCell::new(
header,
text,
cwd.as_path(),
false,
)));
}
}
other => {
if let Some(cell) = fallback_transcript_cell(&other) {
cells.push(Arc::new(cell));
}
}
}
}
if cells.is_empty() {
cells.push(Arc::new(PlainHistoryCell::new(vec![
"No transcript content available".italic().dim().into(),
])));
}
cells
}
fn fallback_transcript_cell(item: &ThreadItem) -> Option<PlainHistoryCell> {
let lines = match item {
ThreadItem::HookPrompt { fragments, .. } => fragments
.iter()
.map(|fragment| {
vec![
"hook prompt: ".dim(),
fragment.text.trim().to_string().into(),
]
.into()
})
.collect::<Vec<_>>(),
ThreadItem::CommandExecution {
command,
status,
aggregated_output,
exit_code,
..
} => {
let mut lines: Vec<Line<'static>> =
vec![vec!["$ ".dim(), command.clone().into()].into()];
lines.push(
format!(
"status: {status:?}{}",
exit_code
.map(|code| format!(" · exit {code}"))
.unwrap_or_default()
)
.dim()
.into(),
);
if let Some(output) = aggregated_output.as_deref()
&& !output.trim().is_empty()
{
lines.extend(
output
.lines()
.map(|line| vec![" ".dim(), line.trim_end().to_string().dim()].into()),
);
}
lines
}
ThreadItem::FileChange {
changes, status, ..
} => vec![
format!("file changes: {status:?} · {} changes", changes.len())
.dim()
.into(),
],
ThreadItem::McpToolCall {
server,
tool,
status,
..
} => vec![
format!("mcp tool: {server}/{tool} · {status:?}")
.dim()
.into(),
],
ThreadItem::DynamicToolCall {
namespace,
tool,
status,
..
} => {
let name = namespace
.as_ref()
.map(|namespace| format!("{namespace}/{tool}"))
.unwrap_or_else(|| tool.clone());
vec![format!("tool: {name} · {status:?}").dim().into()]
}
ThreadItem::CollabAgentToolCall { tool, status, .. } => {
vec![format!("agent tool: {tool:?} · {status:?}").dim().into()]
}
ThreadItem::SubAgentActivity {
kind, agent_path, ..
} => {
vec![sub_agent_activity_summary(*kind, agent_path).dim().into()]
}
ThreadItem::WebSearch(item) => {
vec![vec!["web search: ".dim(), item.query.clone().into()].into()]
}
ThreadItem::ImageView { path, .. } => {
let path = path.render_for_ui();
vec![format!("image: {path}").dim().into()]
}
ThreadItem::ImageGeneration(item) => {
let saved = item
.saved_path
.as_ref()
.map(|path| format!(" · {}", path.as_path().display()))
.unwrap_or_default();
vec![
format!("image generation: {}{saved}", item.status)
.dim()
.into(),
]
}
ThreadItem::EnteredReviewMode { review, .. } => {
vec![vec!["review started: ".dim(), review.clone().into()].into()]
}
ThreadItem::ExitedReviewMode { review, .. } => {
vec![vec!["review finished: ".dim(), review.clone().into()].into()]
}
ThreadItem::ContextCompaction { .. } => {
vec!["context compacted".dim().into()]
}
ThreadItem::UserMessage { .. }
| ThreadItem::AgentMessage { .. }
| ThreadItem::Plan { .. }
| ThreadItem::Reasoning { .. }
| ThreadItem::Sleep(_) => return None,
};
(!lines.is_empty()).then(|| PlainHistoryCell::new(lines))
}