Skip to content

Commit bcc1b90

Browse files
author
User
committed
feat: export history to markdown
1 parent cfcddd6 commit bcc1b90

File tree

3 files changed

+85
-5
lines changed

3 files changed

+85
-5
lines changed

src/main.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -146,16 +146,24 @@ async fn main() -> color_eyre::Result<()> {
146146
let memory_index = agent.init_memory_index().await;
147147

148148
let messages = history.clone();
149+
let data_dir = args.data.clone();
149150
let agent_handler = tokio::spawn(async move {
150151
agent
151152
.run(&args.data, control_receiver, messages, memory_index)
152153
.await;
153154
});
154155

155156
let terminal = init_tui().unwrap();
156-
let result = tui::App::new(config, model_info, control_sender, output_receiver, history)
157-
.run(terminal)
158-
.await;
157+
let result = tui::App::new(
158+
config,
159+
data_dir.into(),
160+
model_info,
161+
control_sender,
162+
output_receiver,
163+
history,
164+
)
165+
.run(terminal)
166+
.await;
159167
let _ = agent_handler.await;
160168
ratatui::restore();
161169
result

src/tui/app.rs

Lines changed: 72 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// Copyright © 2025 Huly Labs. Use of this source code is governed by the MIT license.
22
use std::collections::{HashMap, HashSet};
3+
use std::io::Write;
34
use std::path::PathBuf;
4-
use std::vec;
5+
use std::{fs, vec};
56

67
use crate::agent::event::{AgentCommandStatus, AgentState, ConfirmToolResponse};
78
use crate::config::Config;
@@ -14,6 +15,7 @@ use crate::{
1415
Theme,
1516
},
1617
};
18+
use anyhow::Result;
1719
use crossterm::event::KeyEventKind;
1820
use ratatui::layout::Position;
1921
use ratatui::prelude::Rect;
@@ -22,7 +24,7 @@ use ratatui::{
2224
widgets::ScrollbarState,
2325
DefaultTerminal,
2426
};
25-
use rig::message::{Message, UserContent};
27+
use rig::message::{AssistantContent, Message, UserContent};
2628
use rig::tool::Tool;
2729
use tokio::sync::mpsc;
2830
use tui_textarea::TextArea;
@@ -96,6 +98,7 @@ pub struct UiState<'a> {
9698
#[derive(Debug)]
9799
pub struct App<'a> {
98100
pub config: Config,
101+
pub data_dir: PathBuf,
99102
pub running: bool,
100103
pub events: UiEventMultiplexer,
101104
pub agent_sender: mpsc::UnboundedSender<agent::AgentControlEvent>,
@@ -136,6 +139,7 @@ impl ModelState {
136139
impl App<'_> {
137140
pub fn new(
138141
config: Config,
142+
data_dir: PathBuf,
139143
model_info: ModelInfo,
140144
sender: mpsc::UnboundedSender<agent::AgentControlEvent>,
141145
receiver: mpsc::UnboundedReceiver<agent::AgentOutputEvent>,
@@ -144,6 +148,7 @@ impl App<'_> {
144148
Self {
145149
ui: UiState::new(config.workspace.clone()),
146150
config,
151+
data_dir,
147152
running: true,
148153
events: UiEventMultiplexer::new(receiver),
149154
agent_sender: sender,
@@ -152,6 +157,68 @@ impl App<'_> {
152157
}
153158
}
154159

160+
fn export_history(&self) -> Result<()> {
161+
let file = fs::File::create(self.data_dir.join("history.md"))?;
162+
let mut writer = std::io::BufWriter::new(file);
163+
164+
for message in self.model.messages.iter() {
165+
match message {
166+
Message::User { content } => {
167+
for item in content.iter() {
168+
match item {
169+
UserContent::Text(text) => {
170+
if text.text.starts_with("<environment_details>") {
171+
continue;
172+
}
173+
write!(writer, "### User\n\n{}\n\n", text.text)?;
174+
}
175+
UserContent::Image(image) => {
176+
write!(writer, "### User\n\n![image](data:{})\n\n", image.data)?;
177+
}
178+
UserContent::ToolResult(tool_result) => {
179+
write!(
180+
writer,
181+
"### User Tool Result\n\n{}\n\n",
182+
match tool_result.content.first() {
183+
rig::message::ToolResultContent::Text(text) =>
184+
match serde_json::from_str::<serde_json::Value>(
185+
&text.text
186+
) {
187+
Ok(v) =>
188+
v.as_str().unwrap_or(&text.text).to_string(),
189+
Err(_) => text.text,
190+
},
191+
rig::message::ToolResultContent::Image(image) =>
192+
format!("![image](data:{})", image.data),
193+
}
194+
)?;
195+
}
196+
_ => {}
197+
}
198+
}
199+
}
200+
Message::Assistant { content } => {
201+
for item in content.iter() {
202+
match item {
203+
AssistantContent::Text(text) => {
204+
write!(writer, "### Assistant\n\n{}\n\n", text.text)?;
205+
}
206+
AssistantContent::ToolCall(tool_call) => {
207+
write!(
208+
writer,
209+
"### Assistant [{}]\n\n{}\n\n",
210+
tool_call.function.name,
211+
serde_yaml::to_string(&tool_call.function.arguments).unwrap()
212+
)?;
213+
}
214+
}
215+
}
216+
}
217+
}
218+
}
219+
Ok(())
220+
}
221+
155222
pub async fn run(mut self, mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
156223
if !self.model.messages.is_empty() {
157224
self.ui.history_follow_last = true;
@@ -486,6 +553,9 @@ impl App<'_> {
486553
.send(AgentControlEvent::CancelTask)
487554
.unwrap()
488555
}
556+
KeyCode::Char('e') if key_event.modifiers == KeyModifiers::CONTROL => {
557+
self.export_history().unwrap();
558+
}
489559
KeyCode::BackTab => {
490560
let mut focus = self.ui.focus.clone() as u8;
491561
if focus == 0 {

src/tui/widgets/shortcuts.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ impl ShortcutsWidget {
3333
Span::styled(": Navigate | ", theme.inactive_style()),
3434
Span::styled("Enter", theme.highlight_style()),
3535
Span::styled(": Select | ", theme.inactive_style()),
36+
Span::styled("^e", theme.highlight_style()),
37+
Span::styled(": Export History | ", theme.inactive_style()),
3638
Span::styled("^w", theme.highlight_style()),
3739
Span::styled(": Quit ", theme.inactive_style()),
3840
])]);

0 commit comments

Comments
 (0)