11// Copyright © 2025 Huly Labs. Use of this source code is governed by the MIT license.
22use std:: collections:: { HashMap , HashSet } ;
3+ use std:: io:: Write ;
34use std:: path:: PathBuf ;
4- use std:: vec;
5+ use std:: { fs , vec} ;
56
67use crate :: agent:: event:: { AgentCommandStatus , AgentState , ConfirmToolResponse } ;
78use crate :: config:: Config ;
@@ -14,6 +15,7 @@ use crate::{
1415 Theme ,
1516 } ,
1617} ;
18+ use anyhow:: Result ;
1719use crossterm:: event:: KeyEventKind ;
1820use ratatui:: layout:: Position ;
1921use 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 } ;
2628use rig:: tool:: Tool ;
2729use tokio:: sync:: mpsc;
2830use tui_textarea:: TextArea ;
@@ -96,6 +98,7 @@ pub struct UiState<'a> {
9698#[ derive( Debug ) ]
9799pub 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 {
136139impl 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 \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) ,
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 {
0 commit comments