1+ /*
2+ * Copyright © 2025 Mitja Leino
3+ *
4+ * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
5+ * documentation files (the “Software”), to deal in the Software without restriction, including without limitation
6+ * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software,
7+ * and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
8+ *
9+ * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
10+ *
11+ * THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
12+ * WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
13+ * OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
14+ * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+ */
16+ use crate :: api:: get_chat_client_implementation;
17+ use crate :: config:: AppConfig ;
18+ use crate :: models:: history_file:: HistoryFile ;
19+
20+ pub ( crate ) struct Session {
21+ pub ( crate ) config : AppConfig ,
22+ pub ( crate ) client : Box < dyn crate :: api:: ChatClient > ,
23+ pub ( crate ) history_file : HistoryFile ,
24+ }
25+
26+ impl Session {
27+ pub ( crate ) fn new ( config : AppConfig ) -> Self {
28+ let client = Self :: create_client ( & config) ;
29+ let history_file = HistoryFile :: new (
30+ config. cache_config . get_history_file_path ( ) ,
31+ config. data_dir . display ( ) . to_string ( ) ,
32+ ) . expect ( "Failed to determine history file" ) ;
33+
34+ Self {
35+ config,
36+ client,
37+ history_file,
38+ }
39+ }
40+
41+ fn create_client ( config : & AppConfig ) -> Box < dyn crate :: api:: ChatClient > {
42+ get_chat_client_implementation (
43+ & config. current_profile . provider ,
44+ & config. current_model . model ,
45+ config. user_config . system_prompt . clone ( ) ,
46+ config. user_config . max_tokens ,
47+ )
48+ }
49+
50+ pub ( crate ) fn rebuild_client ( & mut self ) {
51+ self . client = Self :: create_client ( & self . config ) ;
52+ }
53+
54+ pub fn update_config < F > ( & mut self , modifier : F )
55+ where
56+ F : FnOnce ( & mut AppConfig ) -> bool ,
57+ {
58+ let should_rebuild = modifier ( & mut self . config ) ;
59+ if should_rebuild {
60+ self . rebuild_client ( ) ;
61+ }
62+ }
63+ }
64+
65+ #[ cfg( test) ]
66+ mod tests {
67+ use super :: Session ;
68+ use crate :: config:: profiles_config:: { Model , ModelType , Profile } ;
69+ use crate :: config:: { AppConfig , CacheConfig , UserConfig } ;
70+ use tempfile:: TempDir ;
71+
72+ fn make_test_config ( temp : & TempDir , history_name : & str , system_prompt : & str ) -> AppConfig {
73+ let mut config = AppConfig :: default ( ) ;
74+
75+ // point data_dir to a temporary location to avoid touching user directories
76+ let chats_dir = temp. path ( ) . join ( "chats" ) ;
77+ std:: fs:: create_dir_all ( & chats_dir) . unwrap ( ) ;
78+ config. data_dir = chats_dir;
79+
80+ // minimal cache config so HistoryFile path is resolvable
81+ config. cache_config = CacheConfig {
82+ last_history_file : Some ( history_name. to_string ( ) ) ,
83+ last_profile_name : Some ( "test-profile" . to_string ( ) ) ,
84+ profile_models : None ,
85+ } ;
86+
87+ // minimal user config
88+ let mut user = UserConfig :: default ( ) ;
89+ user. system_prompt = system_prompt. to_string ( ) ;
90+ config. user_config = user. clone ( ) ;
91+
92+ // set a test profile that maps to our test stub ChatClient
93+ config. current_profile = Profile {
94+ name : "test-profile" . to_string ( ) ,
95+ provider : "test" . to_string ( ) ,
96+ models : vec ! [ Model {
97+ model: "test-model" . to_string( ) ,
98+ description: Some ( "test model" . to_string( ) ) ,
99+ model_type: ModelType :: Fast ,
100+ } ] ,
101+ } ;
102+
103+ config. current_model = Model {
104+ model : "test-model" . to_string ( ) ,
105+ description : Some ( "test model" . to_string ( ) ) ,
106+ model_type : ModelType :: Fast ,
107+ } ;
108+
109+ config
110+ }
111+
112+ #[ test]
113+ fn session_new_initializes_history_file ( ) {
114+ let temp = TempDir :: new ( ) . unwrap ( ) ;
115+ let config = make_test_config ( & temp, "sess.hist" , "sp-1" ) ;
116+
117+ let session = Session :: new ( config) ;
118+
119+ // History file filename should match
120+ assert_eq ! ( session. history_file. filename, "sess.hist" ) ;
121+ // File should exist on disk
122+ assert ! ( std:: path:: Path :: new( & session. history_file. path) . exists( ) ) ;
123+ }
124+
125+ #[ test]
126+ fn update_config_without_rebuild_keeps_client ( ) {
127+ let temp = TempDir :: new ( ) . unwrap ( ) ;
128+ let mut session = Session :: new ( make_test_config ( & temp, "sess2.hist" , "sp-initial" ) ) ;
129+
130+ // Changing the system prompt but returning false should not rebuild client
131+ session. update_config ( |c| {
132+ c. user_config . system_prompt = "sp-updated" . to_string ( ) ;
133+ false
134+ } ) ;
135+
136+ assert_eq ! ( session. client. system_prompt( ) , "sp-initial" . to_string( ) ) ;
137+ }
138+
139+ #[ test]
140+ fn update_config_with_rebuild_updates_client ( ) {
141+ let temp = TempDir :: new ( ) . unwrap ( ) ;
142+ let mut session = Session :: new ( make_test_config ( & temp, "sess3.hist" , "sp-a" ) ) ;
143+
144+ session. update_config ( |c| {
145+ c. user_config . system_prompt = "sp-b" . to_string ( ) ;
146+ true // request rebuild
147+ } ) ;
148+
149+ assert_eq ! ( session. client. system_prompt( ) , "sp-b" . to_string( ) ) ;
150+ }
151+
152+ #[ test]
153+ fn rebuild_client_applies_current_config ( ) {
154+ let temp = TempDir :: new ( ) . unwrap ( ) ;
155+ let mut session = Session :: new ( make_test_config ( & temp, "sess4.hist" , "sp-0" ) ) ;
156+
157+ session. config . user_config . system_prompt = "sp-1" . to_string ( ) ;
158+ session. rebuild_client ( ) ;
159+
160+ assert_eq ! ( session. client. system_prompt( ) , "sp-1" . to_string( ) ) ;
161+ }
162+ }
0 commit comments