Skip to content

Commit e159e1d

Browse files
authored
Refactor history file handling and cleanup main (#39)
* Refactor history file handling and cleanup main * Improve documentation * Add todo
1 parent fc36df9 commit e159e1d

7 files changed

Lines changed: 115 additions & 77 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/todo.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@
66
- [x] Use ollama API instead of run commands
77
- [x] Parse the chat history to a correctly formatted JSON
88
- [x] Implement simple completions with rustyline
9-
- [x] Commands
10-
- [x] Files
9+
- [x] Commands
10+
- [x] Files
1111
- [x] Support multiline input with alt + enter (using rustyline)
1212
- [x] Update the default sysprompt
1313
- [x] Keep track of the model's context window and file size
1414
- [x] Add support for knowledge directory
1515
- [x] The model might not realize that it has the context file available
16+
- [ ] Support streaming with an interrupt thread
17+
- [ ] Add an option to hide thinking
1618
- [ ] Custom completions for `model` and `profile` commands
1719
- [ ] Keybinds for commands?
1820
- [ ] Support memories, which are included in the prompt by default (session/global) (could be implemented as a tool)
@@ -32,16 +34,17 @@
3234
- [ ] Create an extension system for adding tools
3335
- [ ] Add tool validation for user-defined tools (e.g., check unique names)
3436
- [ ] Should tool results be saved (to history or maybe an alternative file) or (even) printed?
35-
- `append_tool_input` in git history
37+
- `append_tool_input` in git history
3638

3739
## Commands
3840

3941
- [x] Allow changing the context file during a chat
40-
- [x] `config.create_editor` - Handle command/file command logic using the registry
42+
- [x] `config.create_editor` - Handle command/file command logic using the registry
4143
- [x] `prompt`- Enable creating, editing and using prompt files
42-
- The user should be able to define where their actual prompt is injected
44+
- The user should be able to define where their actual prompt is injected
4345
- [x] `clear` - Clear the current history file
44-
- [ ] `copy` - Copy the history file to another location. Edit the copy of the file?
46+
- [ ] `fork` - Copy the history file to another location. Edit the copy of the file
47+
- [ ] `rename` - Rename the current history file
4548
- [ ] Truncate chat (line count, estimated tokens, or LLM assisted)
4649

4750
## Completion overhaul

src/config/args.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
17+
use clap::Parser;
18+
use std::path::PathBuf;
19+
20+
#[derive(Parser)]
21+
#[command(author, version, about, long_about = None)]
22+
pub(crate) struct Args {
23+
/// Path to file containing chat history. Can be either relative (to `cforge_dir`) or absolute.
24+
/// If not provided, the last history file will be used, which is saved in the XDG cache file.
25+
pub(crate) history_file: Option<String>,
26+
27+
/// Optional file path, which's content is used as additional input for each chat message.
28+
#[arg(short = 'f', long = "file")]
29+
pub(crate) context_file: Option<PathBuf>,
30+
}

src/config/cache_config.rs

Lines changed: 37 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ impl CacheConfig {
5555
Self::new(None, None, None)
5656
}
5757

58-
pub(crate) fn load(cache_path: Option<PathBuf>) -> Self {
58+
pub(crate) fn load(cache_path: Option<PathBuf>, arg_history_file: &Option<String>) -> Self {
5959
let mut cache = Self::empty();
6060

6161
if let Some(cache_path) = cache_path {
@@ -68,6 +68,16 @@ impl CacheConfig {
6868
}
6969
};
7070

71+
if let Some(hf) = arg_history_file {
72+
cache.last_history_file = Some(hf.to_string());
73+
}
74+
75+
if cache.last_history_file.is_none() {
76+
println!("You must specify a history file `cforge <history_file>` for the first time.");
77+
println!("See `cforge --help` for more information.");
78+
panic!("No history file specified and no previous history file found from cache.");
79+
}
80+
7181
cache
7282
}
7383

@@ -83,6 +93,15 @@ impl CacheConfig {
8393
}
8494
}
8595
}
96+
97+
pub fn get_history_file_path(&self) -> String {
98+
match &self.last_history_file {
99+
Some(hf) => hf.to_string(),
100+
None => panic!(
101+
"No history file given as an argument or found from cache. This shouldn't happen"
102+
),
103+
}
104+
}
86105
}
87106

88107
#[cfg(test)]
@@ -91,60 +110,57 @@ mod tests {
91110

92111
use tempfile::TempDir;
93112

94-
use crate::config::cache_config::{CacheConfig, CACHE_FILE};
113+
use crate::config::cache_config::{CACHE_FILE, CacheConfig};
95114

96115
#[test]
116+
#[should_panic]
97117
fn load_invalid_cache_config() {
98118
let temp_dir = create_cache_config(
99119
"
100120
thisisa malformed \" string !\"
101121
",
102122
);
103123
let path_opt = Some(temp_dir.path().to_path_buf());
104-
let config = CacheConfig::load(path_opt);
105-
106-
assert_eq!(
107-
config.last_history_file,
108-
CacheConfig::empty().last_history_file
109-
);
124+
let _ = CacheConfig::load(path_opt, &None);
110125
}
111126

112127
#[test]
128+
#[should_panic]
113129
fn load_non_existent_cache_config() {
114130
let temp_dir = create_cache_config("");
115131
let path_opt = Some(temp_dir.path().join("doesnt_exist.toml").to_path_buf());
116-
let config = CacheConfig::load(path_opt);
117-
118-
assert_eq!(
119-
config.last_history_file,
120-
CacheConfig::empty().last_history_file
121-
);
132+
let _ = CacheConfig::load(path_opt, &None);
122133
}
123134

124135
#[test]
136+
#[should_panic]
125137
fn load_empty_cache_config() {
126138
let temp_dir = create_cache_config("");
127139
let path_opt = Some(temp_dir.path().to_path_buf());
128-
let config = CacheConfig::load(path_opt);
129-
130-
assert_eq!(
131-
config.last_history_file,
132-
CacheConfig::empty().last_history_file
133-
);
140+
let _ = CacheConfig::load(path_opt, &None);
134141
}
135142

136143
#[test]
137144
fn load_valid_cache_config() {
138145
let temp_dir = create_cache_config("last_history_file = \"some_history_file\"");
139146
let path_opt = Some(temp_dir.path().to_path_buf());
140-
let config = CacheConfig::load(path_opt);
147+
let config = CacheConfig::load(path_opt, &None);
141148

142149
assert_eq!(
143150
config.last_history_file,
144151
Some("some_history_file".to_string())
145152
);
146153
}
147154

155+
#[test]
156+
fn override_using_arg_history() {
157+
let temp_dir = create_cache_config("last_history_file = \"some_history_file\"");
158+
let path_opt = Some(temp_dir.path().to_path_buf());
159+
let config = CacheConfig::load(path_opt, &Some("override_path".to_string()));
160+
161+
assert_eq!(config.last_history_file, Some("override_path".to_string()));
162+
}
163+
148164
fn create_cache_config(content: &str) -> TempDir {
149165
let temp_dir: TempDir = TempDir::new().unwrap();
150166
let config_path: PathBuf = temp_dir.path().join(CACHE_FILE);

src/config/mod.rs

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
*/
1616
use std::{collections::HashMap, fs::create_dir_all, path::PathBuf};
1717

18-
use rustyline::{history::DefaultHistory, Cmd, Config, Editor, EventHandler, KeyEvent, Modifiers};
18+
use rustyline::{Cmd, Config, Editor, EventHandler, KeyEvent, Modifiers, history::DefaultHistory};
1919

2020
use crate::command::command_complete::CommandHelper;
2121
use crate::command::commands::{CommandStruct, FileCommandDirectory};
2222
use crate::config::profiles_config::{Model, ModelType, Profile};
23-
pub(crate) use crate::config::{cache_config::CacheConfig, rustyline_config::build, user_config::UserConfig};
23+
pub(crate) use crate::config::{
24+
cache_config::CacheConfig, rustyline_config::build, user_config::UserConfig,
25+
};
2426

27+
pub mod args;
2528
pub mod cache_config;
2629
pub mod profiles_config;
2730
pub mod rustyline_config;
@@ -39,8 +42,8 @@ pub struct AppConfig {
3942
}
4043

4144
impl AppConfig {
42-
pub fn load_config() -> AppConfig {
43-
let mut cache_config: CacheConfig = CacheConfig::load(get_cache_path());
45+
pub fn load_config(arg_history_file: &Option<String>) -> AppConfig {
46+
let mut cache_config: CacheConfig = CacheConfig::load(get_cache_path(), &arg_history_file);
4447
let user_config: UserConfig = UserConfig::load(get_config_path());
4548
let rustyline_config = build(&user_config);
4649

@@ -176,6 +179,13 @@ impl AppConfig {
176179

177180
println!("Switched to model: {}", model.model);
178181
}
182+
183+
pub(crate) fn print_model_info(&self) {
184+
println!(
185+
"\n\nYou're conversing with model '{}' ({}) from profile '{}'",
186+
&self.current_model, &self.current_model.model_type, &self.current_profile.name
187+
);
188+
}
179189
}
180190

181191
impl Default for AppConfig {
@@ -219,7 +229,10 @@ fn get_commands(command_registry: &HashMap<String, CommandStruct>) -> CommandVec
219229
}
220230
}
221231

222-
CommandVecs { all_commands, file_commands }
232+
CommandVecs {
233+
all_commands,
234+
file_commands,
235+
}
223236
}
224237

225238
/// Return XDG compliant config path

src/main.rs

Lines changed: 16 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -27,60 +27,27 @@ mod test_support;
2727

2828
use crate::api::{ChatClient, get_chat_client_implementation};
2929
use crate::command::commands::{CommandResult, create_command_registry};
30-
use crate::config::AppConfig;
30+
use crate::config::{AppConfig, args::Args};
3131
use crate::models::context_file::ContextFile;
3232
use crate::models::history_file::HistoryFile;
3333
use crate::traits::estimate_context_size::ContextEstimation;
3434
use clap::Parser;
3535
use colored::Colorize;
3636
use command::processor::CommandProcessor;
3737
use std::io::{self};
38-
use std::path::PathBuf;
39-
40-
#[derive(Parser)]
41-
#[command(author, version, about, long_about = None)]
42-
struct Args {
43-
/// Path to file containing chat history. Can be either relative (to `cforge_dir`) or absolute.
44-
/// If not provided, the last history file will be used, which is saved in `~/.cforge.toml`.
45-
history_file: Option<String>,
46-
47-
/// Optional file with content to be used as input for each chat message
48-
#[arg(short = 'f', long = "file")]
49-
context_file: Option<PathBuf>,
50-
}
5138

5239
fn main() -> io::Result<()> {
53-
let mut app_config = AppConfig::load_config();
5440
let args = Args::parse();
41+
let mut app_config = AppConfig::load_config(&args.history_file);
5542
let command_registry = create_command_registry(app_config.user_config.command_prefixes.clone());
5643
let mut context_file_path = args.context_file.clone();
57-
58-
let history_path = args.history_file.unwrap_or_else(|| {
59-
match app_config.cache_config.last_history_file.clone() {
60-
Some(path) => path,
61-
None => {
62-
println!(
63-
"You must specify a history file `cforge <history_file>` for the first time."
64-
);
65-
println!("See `cforge --help` for more information.");
66-
panic!("No history file specified and no previous history file found.");
67-
}
68-
}
69-
});
70-
71-
app_config.update_last_history_file(history_path.clone());
72-
7344
let mut history = HistoryFile::new(
74-
history_path.clone(),
45+
app_config.cache_config.get_history_file_path(),
7546
app_config.data_dir.display().to_string(),
7647
)?;
77-
println!("{}", history.get_content());
78-
println!(
79-
"\n\nYou're conversing with model '{}' ({}) from profile '{}'",
80-
&app_config.current_model,
81-
&app_config.current_model.model_type,
82-
&app_config.current_profile.name
83-
);
48+
49+
history.print_content();
50+
app_config.print_model_info();
8451

8552
let mut chat_client: Box<dyn ChatClient> = get_chat_client_implementation(
8653
&app_config.current_profile.provider,
@@ -108,12 +75,10 @@ fn main() -> io::Result<()> {
10875

10976
let context_file = ContextFile::new(&context_file_path);
11077

111-
if let Some(model_context_size) = chat_client.model_context_size()
112-
&& app_config.user_config.token_estimation
113-
{
78+
if app_config.user_config.token_estimation {
11479
print_token_usage(
11580
history.estimate_context_size() + context_file.estimate_context_size(),
116-
model_context_size,
81+
&chat_client.model_context_size(),
11782
);
11883
}
11984

@@ -161,7 +126,14 @@ fn main() -> io::Result<()> {
161126
}
162127

163128
/// Calculate and visualize token usage compared to model context size
164-
fn print_token_usage(estimated_tokens: usize, context_size: usize) {
129+
fn print_token_usage(estimated_tokens: usize, maybe_context_size: &Option<usize>) {
130+
let context_size = match maybe_context_size {
131+
Some(c) => *c,
132+
None => {
133+
return;
134+
}
135+
};
136+
165137
let percentage = (estimated_tokens as f64 / context_size as f64 * 100.0).min(100.0);
166138

167139
let bar_width = 50;

src/models/history_file.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ impl HistoryFile {
112112
&self.content
113113
}
114114

115+
pub(crate) fn print_content(&self) {
116+
println!("{}", &self.content);
117+
}
118+
115119
/// Get the content of the history file formatted as a JSON array
116120
///
117121
/// Returns a JSON array of `"role": "", "content": ""` messages

0 commit comments

Comments
 (0)