Skip to content

Commit 8c7e58a

Browse files
authored
feat: support autonaming session (#1001)
1 parent bb1c34d commit 8c7e58a

File tree

7 files changed

+251
-72
lines changed

7 files changed

+251
-72
lines changed

assets/roles/%create-title%.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Create a concise, 3-6 word title.
2+
3+
**Notes**:
4+
- Avoid quotation marks or emojis
5+
- RESPOND ONLY WITH TITLE SLUG TEXT
6+
7+
**Examples**:
8+
stock-market-trends
9+
perfect-chocolate-chip-recipe
10+
remote-work-productivity-tips
11+
video-game-development-insights

src/config/input.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use std::{collections::HashMap, fs::File, io::Read, path::Path};
1414
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
1515

1616
const IMAGE_EXTS: [&str; 5] = ["png", "jpeg", "jpg", "webp", "gif"];
17+
const SUMMARY_MAX_WIDTH: usize = 80;
1718

1819
lazy_static::lazy_static! {
1920
static ref URL_RE: Regex = Regex::new(r"^[A-Za-z0-9_-]{2,}:/").unwrap();
@@ -58,12 +59,11 @@ impl Input {
5859

5960
pub async fn from_files(
6061
config: &GlobalConfig,
61-
text: &str,
62+
raw_text: &str,
6263
paths: Vec<String>,
6364
role: Option<Role>,
6465
) -> Result<Self> {
6566
let spinner = create_spinner("Loading files").await;
66-
let raw_text = text.to_string();
6767
let mut raw_paths = vec![];
6868
let mut local_paths = vec![];
6969
let mut remote_urls = vec![];
@@ -85,8 +85,8 @@ impl Input {
8585
spinner.stop();
8686
let (files, medias, data_urls) = ret.context("Failed to load files")?;
8787
let mut texts = vec![];
88-
if !text.is_empty() {
89-
texts.push(text.to_string());
88+
if !raw_text.is_empty() {
89+
texts.push(raw_text.to_string());
9090
};
9191
if !files.is_empty() {
9292
texts.push(String::new());
@@ -98,7 +98,7 @@ impl Input {
9898
Ok(Self {
9999
config: config.clone(),
100100
text: texts.join("\n"),
101-
raw: (raw_text, raw_paths),
101+
raw: (raw_text.to_string(), raw_paths),
102102
patched_text: None,
103103
continue_output: None,
104104
regenerate: false,
@@ -280,12 +280,12 @@ impl Input {
280280
.chars()
281281
.map(|c| if c.is_control() { ' ' } else { c })
282282
.collect();
283-
if text.width_cjk() > 70 {
283+
if text.width_cjk() > SUMMARY_MAX_WIDTH {
284284
let mut sum_width = 0;
285285
let mut chars = vec![];
286286
for c in text.chars() {
287287
sum_width += c.width_cjk().unwrap_or(1);
288-
if sum_width > 67 {
288+
if sum_width > SUMMARY_MAX_WIDTH - 3 {
289289
chars.extend(['.', '.', '.']);
290290
break;
291291
}

src/config/mod.rs

Lines changed: 118 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ mod session;
55

66
pub use self::agent::{list_agents, Agent};
77
pub use self::input::Input;
8-
pub use self::role::{Role, RoleLike, CODE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE};
8+
pub use self::role::{
9+
Role, RoleLike, CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, SHELL_ROLE,
10+
};
911
use self::session::Session;
1012

1113
use crate::client::{
@@ -37,6 +39,10 @@ use std::{
3739
};
3840
use syntect::highlighting::ThemeSet;
3941

42+
pub const TEMP_ROLE_NAME: &str = "%%";
43+
pub const TEMP_RAG_NAME: &str = "temp";
44+
pub const TEMP_SESSION_NAME: &str = "temp";
45+
4046
/// Monokai Extended
4147
const DARK_THEME: &[u8] = include_bytes!("../../assets/monokai-extended.theme.bin");
4248
const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.theme.bin");
@@ -52,10 +58,6 @@ const FUNCTIONS_FILE_NAME: &str = "functions.json";
5258
const FUNCTIONS_BIN_DIR_NAME: &str = "bin";
5359
const AGENTS_DIR_NAME: &str = "agents";
5460

55-
pub const TEMP_ROLE_NAME: &str = "%%";
56-
pub const TEMP_RAG_NAME: &str = "temp";
57-
pub const TEMP_SESSION_NAME: &str = "temp";
58-
5961
const CLIENTS_FIELD: &str = "clients";
6062

6163
const SERVE_ADDR: &str = "127.0.0.1:8000";
@@ -339,7 +341,10 @@ impl Config {
339341
}
340342

341343
pub fn session_file(&self, name: &str) -> PathBuf {
342-
self.sessions_dir().join(format!("{name}.yaml"))
344+
match name.split_once("/") {
345+
Some((dir, name)) => self.sessions_dir().join(dir).join(format!("{name}.yaml")),
346+
None => self.sessions_dir().join(format!("{name}.yaml")),
347+
}
343348
}
344349

345350
pub fn rag_file(&self, name: &str) -> PathBuf {
@@ -1081,7 +1086,10 @@ impl Config {
10811086
let session_name = match &self.session {
10821087
Some(session) => match name {
10831088
Some(v) => v.to_string(),
1084-
None => session.name().to_string(),
1089+
None => session
1090+
.autoname()
1091+
.unwrap_or_else(|| session.name())
1092+
.to_string(),
10851093
},
10861094
None => bail!("No session"),
10871095
};
@@ -1124,9 +1132,9 @@ impl Config {
11241132
Ok(())
11251133
}
11261134

1127-
pub fn set_append_conversation(&mut self) -> Result<()> {
1135+
pub fn set_save_session_this_time(&mut self) -> Result<()> {
11281136
if let Some(session) = self.session.as_mut() {
1129-
session.set_append_conversation();
1137+
session.set_save_session_this_time();
11301138
} else {
11311139
bail!("No session")
11321140
}
@@ -1137,14 +1145,42 @@ impl Config {
11371145
list_file_names(self.sessions_dir(), ".yaml")
11381146
}
11391147

1140-
pub fn should_compress_session(&mut self) -> bool {
1141-
if let Some(session) = self.session.as_mut() {
1142-
if session.need_compress(self.compress_threshold) {
1143-
session.set_compressing(true);
1144-
return true;
1148+
pub fn list_autoname_sessions(&self) -> Vec<String> {
1149+
list_file_names(self.sessions_dir().join("_"), ".yaml")
1150+
}
1151+
1152+
pub fn maybe_compress_session(config: GlobalConfig) {
1153+
let mut need_compress = false;
1154+
{
1155+
let mut config = config.write();
1156+
let compress_threshold = config.compress_threshold;
1157+
if let Some(session) = config.session.as_mut() {
1158+
if session.need_compress(compress_threshold) {
1159+
session.set_compressing(true);
1160+
need_compress = true;
1161+
}
11451162
}
1163+
};
1164+
if !need_compress {
1165+
return;
11461166
}
1147-
false
1167+
let color = if config.read().light_theme {
1168+
nu_ansi_term::Color::LightGray
1169+
} else {
1170+
nu_ansi_term::Color::DarkGray
1171+
};
1172+
print!(
1173+
"\n📢 {}\n",
1174+
color.italic().paint("Compressing the session."),
1175+
);
1176+
tokio::spawn(async move {
1177+
if let Err(err) = Config::compress_session(&config).await {
1178+
warn!("Failed to compress the session: {err}");
1179+
}
1180+
if let Some(session) = config.write().session.as_mut() {
1181+
session.set_compressing(false);
1182+
}
1183+
});
11481184
}
11491185

11501186
pub async fn compress_session(config: &GlobalConfig) -> Result<()> {
@@ -1156,7 +1192,13 @@ impl Config {
11561192
}
11571193
None => bail!("No session"),
11581194
}
1159-
let input = Input::from_str(config, config.read().summarize_prompt(), None);
1195+
1196+
let prompt = config
1197+
.read()
1198+
.summarize_prompt
1199+
.clone()
1200+
.unwrap_or_else(|| SUMMARIZE_PROMPT.into());
1201+
let input = Input::from_str(config, &prompt, None);
11601202
let client = input.create_client()?;
11611203
let summary = client.chat_completions(input).await?.text;
11621204
let summary_prompt = config
@@ -1171,21 +1213,58 @@ impl Config {
11711213
Ok(())
11721214
}
11731215

1174-
pub fn summarize_prompt(&self) -> &str {
1175-
self.summarize_prompt.as_deref().unwrap_or(SUMMARIZE_PROMPT)
1176-
}
1177-
11781216
pub fn is_compressing_session(&self) -> bool {
11791217
self.session
11801218
.as_ref()
11811219
.map(|v| v.compressing())
11821220
.unwrap_or_default()
11831221
}
11841222

1185-
pub fn end_compressing_session(&mut self) {
1186-
if let Some(session) = self.session.as_mut() {
1187-
session.set_compressing(false);
1223+
pub fn maybe_autoname_session(config: GlobalConfig) {
1224+
let mut need_autoname = false;
1225+
if let Some(session) = config.write().session.as_mut() {
1226+
if session.need_autoname() {
1227+
session.set_autonaming(true);
1228+
need_autoname = true;
1229+
}
1230+
}
1231+
if !need_autoname {
1232+
return;
1233+
}
1234+
let color = if config.read().light_theme {
1235+
nu_ansi_term::Color::LightGray
1236+
} else {
1237+
nu_ansi_term::Color::DarkGray
1238+
};
1239+
print!("\n📢 {}\n", color.italic().paint("Autonaming the session."),);
1240+
tokio::spawn(async move {
1241+
if let Err(err) = Config::autoname_session(&config).await {
1242+
warn!("Failed to autonaming the session: {err}");
1243+
}
1244+
if let Some(session) = config.write().session.as_mut() {
1245+
session.set_autonaming(false);
1246+
}
1247+
});
1248+
}
1249+
1250+
pub async fn autoname_session(config: &GlobalConfig) -> Result<()> {
1251+
let text = match config
1252+
.read()
1253+
.session
1254+
.as_ref()
1255+
.and_then(|v| v.chat_history_for_autonaming())
1256+
{
1257+
Some(v) => v,
1258+
None => bail!("No chat history"),
1259+
};
1260+
let role = config.read().retrieve_role(CREATE_TITLE_ROLE)?;
1261+
let input = Input::from_str(config, &text, Some(role));
1262+
let client = input.create_client()?;
1263+
let text = client.chat_completions(input).await?.text;
1264+
if let Some(session) = config.write().session.as_mut() {
1265+
session.set_autoname(&text);
11881266
}
1267+
Ok(())
11891268
}
11901269

11911270
pub async fn use_rag(
@@ -1562,7 +1641,19 @@ impl Config {
15621641
.into_iter()
15631642
.map(|v| (v.id(), Some(v.description())))
15641643
.collect(),
1565-
".session" => map_completion_values(self.list_sessions()),
1644+
".session" => {
1645+
if args[0].starts_with("_/") {
1646+
map_completion_values(
1647+
self.list_autoname_sessions()
1648+
.iter()
1649+
.rev()
1650+
.map(|v| format!("_/{}", v))
1651+
.collect::<Vec<String>>(),
1652+
)
1653+
} else {
1654+
map_completion_values(self.list_sessions())
1655+
}
1656+
}
15661657
".rag" => map_completion_values(Self::list_rags()),
15671658
".agent" => map_completion_values(list_agents()),
15681659
".starter" => match &self.agent {
@@ -1772,6 +1863,9 @@ impl Config {
17721863
}
17731864
if let Some(session) = &self.session {
17741865
output.insert("session", session.name().to_string());
1866+
if let Some(autoname) = session.autoname() {
1867+
output.insert("session_autoname", autoname.to_string());
1868+
}
17751869
output.insert("dirty", session.dirty().to_string());
17761870
let (tokens, percent) = session.tokens_usage();
17771871
output.insert("consume_tokens", tokens.to_string());

src/config/role.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use serde_json::Value;
1111
pub const SHELL_ROLE: &str = "%shell%";
1212
pub const EXPLAIN_SHELL_ROLE: &str = "%explain-shell%";
1313
pub const CODE_ROLE: &str = "%code%";
14+
pub const CREATE_TITLE_ROLE: &str = "%create-title%";
1415

1516
pub const INPUT_PLACEHOLDER: &str = "__INPUT__";
1617

0 commit comments

Comments
 (0)