1
1
// Copyright © 2025 Huly Labs. Use of this source code is governed by the MIT license.
2
2
use std:: collections:: { HashMap , HashSet } ;
3
+ use std:: io:: Write ;
3
4
use std:: path:: PathBuf ;
4
- use std:: vec;
5
+ use std:: { fs , vec} ;
5
6
6
7
use crate :: agent:: event:: { AgentCommandStatus , AgentState , ConfirmToolResponse } ;
7
8
use crate :: config:: Config ;
@@ -14,6 +15,7 @@ use crate::{
14
15
Theme ,
15
16
} ,
16
17
} ;
18
+ use anyhow:: Result ;
17
19
use crossterm:: event:: KeyEventKind ;
18
20
use ratatui:: layout:: Position ;
19
21
use ratatui:: prelude:: Rect ;
@@ -22,7 +24,7 @@ use ratatui::{
22
24
widgets:: ScrollbarState ,
23
25
DefaultTerminal ,
24
26
} ;
25
- use rig:: message:: { Message , UserContent } ;
27
+ use rig:: message:: { AssistantContent , Message , UserContent } ;
26
28
use rig:: tool:: Tool ;
27
29
use tokio:: sync:: mpsc;
28
30
use tui_textarea:: TextArea ;
@@ -96,6 +98,7 @@ pub struct UiState<'a> {
96
98
#[ derive( Debug ) ]
97
99
pub struct App < ' a > {
98
100
pub config : Config ,
101
+ pub data_dir : PathBuf ,
99
102
pub running : bool ,
100
103
pub events : UiEventMultiplexer ,
101
104
pub agent_sender : mpsc:: UnboundedSender < agent:: AgentControlEvent > ,
@@ -136,6 +139,7 @@ impl ModelState {
136
139
impl App < ' _ > {
137
140
pub fn new (
138
141
config : Config ,
142
+ data_dir : PathBuf ,
139
143
model_info : ModelInfo ,
140
144
sender : mpsc:: UnboundedSender < agent:: AgentControlEvent > ,
141
145
receiver : mpsc:: UnboundedReceiver < agent:: AgentOutputEvent > ,
@@ -144,6 +148,7 @@ impl App<'_> {
144
148
Self {
145
149
ui : UiState :: new ( config. workspace . clone ( ) ) ,
146
150
config,
151
+ data_dir,
147
152
running : true ,
148
153
events : UiEventMultiplexer :: new ( receiver) ,
149
154
agent_sender : sender,
@@ -152,6 +157,68 @@ impl App<'_> {
152
157
}
153
158
}
154
159
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
+
155
222
pub async fn run ( mut self , mut terminal : DefaultTerminal ) -> color_eyre:: Result < ( ) > {
156
223
if !self . model . messages . is_empty ( ) {
157
224
self . ui . history_follow_last = true ;
@@ -486,6 +553,9 @@ impl App<'_> {
486
553
. send ( AgentControlEvent :: CancelTask )
487
554
. unwrap ( )
488
555
}
556
+ KeyCode :: Char ( 'e' ) if key_event. modifiers == KeyModifiers :: CONTROL => {
557
+ self . export_history ( ) . unwrap ( ) ;
558
+ }
489
559
KeyCode :: BackTab => {
490
560
let mut focus = self . ui . focus . clone ( ) as u8 ;
491
561
if focus == 0 {
0 commit comments