diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d43e391..b89af95c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.2.2] - 2026-01-22 + +### Fixed +- Session save no longer panics on serialization errors +- Web search regex patterns are now cached for better performance +- Improved panic messages for regex compilation failures + +## [0.2.1] - 2026-01-22 + +### Fixed +- Resolve clippy warnings for Rust 1.92 + ## [0.2.0] - 2026-01-20 ### Changed @@ -99,7 +111,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hooks system and config profiles - Example skills and launch assets -[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.0...HEAD +[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.2...HEAD +[0.2.2]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.1...v0.2.2 +[0.2.1]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.2.0...v0.2.1 [0.2.0]: https://github.com/Hmbown/DeepSeek-TUI/releases/tag/v0.2.0 [0.0.2]: https://github.com/Hmbown/DeepSeek-TUI/releases/tag/v0.0.2 [0.0.1]: https://github.com/Hmbown/DeepSeek-CLI/releases/tag/v0.0.1 diff --git a/Cargo.lock b/Cargo.lock index 383ef131..9391eb3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -646,7 +646,7 @@ dependencies = [ [[package]] name = "deepseek-tui" -version = "0.2.1" +version = "0.2.2" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index 3cd0220e..f7a5a3c4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "deepseek-tui" -version = "0.2.1" +version = "0.2.2" edition = "2024" description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting" license = "MIT" diff --git a/src/commands/session.rs b/src/commands/session.rs index f05c45ea..63040bd5 100644 --- a/src/commands/session.rs +++ b/src/commands/session.rs @@ -37,7 +37,11 @@ pub fn save(app: &mut App, path: Option<&str>) -> CommandResult { match std::fs::create_dir_all(&sessions_dir) { Ok(()) => { - match std::fs::write(&save_path, serde_json::to_string_pretty(&session).unwrap()) { + let json = match serde_json::to_string_pretty(&session) { + Ok(j) => j, + Err(e) => return CommandResult::error(format!("Failed to serialize session: {e}")), + }; + match std::fs::write(&save_path, json) { Ok(()) => { app.current_session_id = Some(session.metadata.id.clone()); CommandResult::message(format!( diff --git a/src/core/tool_parser.rs b/src/core/tool_parser.rs index 87be2b34..3b1624b5 100644 --- a/src/core/tool_parser.rs +++ b/src/core/tool_parser.rs @@ -53,7 +53,8 @@ static THINKING_REGEX: OnceLock = OnceLock::new(); fn get_tool_call_regex() -> &'static Regex { TOOL_CALL_REGEX.get_or_init(|| { // Match [TOOL_CALL] ... [/TOOL_CALL] blocks - Regex::new(r"(?s)\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]").unwrap() + Regex::new(r"(?s)\[TOOL_CALL\]\s*(.*?)\s*\[/TOOL_CALL\]") + .expect("TOOL_CALL regex pattern is valid") }) } @@ -61,21 +62,22 @@ fn get_xml_tool_call_regex() -> &'static Regex { XML_TOOL_CALL_REGEX.get_or_init(|| { // Match ... or similar XML patterns Regex::new(r"(?s)<(?:deepseek:)?tool_call[^>]*>\s*(.*?)\s*") - .unwrap() + .expect("XML tool_call regex pattern is valid") }) } fn get_invoke_regex() -> &'static Regex { INVOKE_REGEX.get_or_init(|| { // Match ... patterns - Regex::new(r#"(?s)]*>(.*?)"#).unwrap() + Regex::new(r#"(?s)]*>(.*?)"#) + .expect("invoke regex pattern is valid") }) } fn get_thinking_regex() -> &'static Regex { THINKING_REGEX.get_or_init(|| { // Match thinking blocks including partial closing tags - Regex::new(r"(?s)]*>").unwrap() + Regex::new(r"(?s)]*>").expect("thinking regex pattern is valid") }) } diff --git a/src/tools/web_search.rs b/src/tools/web_search.rs index ff86eca8..91d7e04b 100644 --- a/src/tools/web_search.rs +++ b/src/tools/web_search.rs @@ -8,8 +8,34 @@ use async_trait::async_trait; use regex::Regex; use serde::Serialize; use serde_json::{Value, json}; +use std::sync::OnceLock; use std::time::Duration; +// Cached regex patterns for HTML parsing +static TITLE_RE: OnceLock = OnceLock::new(); +static SNIPPET_RE: OnceLock = OnceLock::new(); +static TAG_RE: OnceLock = OnceLock::new(); + +fn get_title_re() -> &'static Regex { + TITLE_RE.get_or_init(|| { + Regex::new(r#"]*class=\"result__a\"[^>]*href=\"([^\"]+)\"[^>]*>(.*?)"#) + .expect("title regex pattern is valid") + }) +} + +fn get_snippet_re() -> &'static Regex { + SNIPPET_RE.get_or_init(|| { + Regex::new( + r#"]*class=\"result__snippet\"[^>]*>(.*?)|]*class=\"result__snippet\"[^>]*>(.*?)"#, + ) + .expect("snippet regex pattern is valid") + }) +} + +fn get_tag_re() -> &'static Regex { + TAG_RE.get_or_init(|| Regex::new(r"<[^>]+>").expect("tag regex pattern is valid")) +} + const DEFAULT_MAX_RESULTS: usize = 5; const MAX_RESULTS: usize = 10; const DEFAULT_TIMEOUT_MS: u64 = 15_000; @@ -140,12 +166,8 @@ impl ToolSpec for WebSearchTool { } fn parse_duckduckgo_results(html: &str, max_results: usize) -> Vec { - let title_re = - Regex::new(r#"]*class=\"result__a\"[^>]*href=\"([^\"]+)\"[^>]*>(.*?)"#).unwrap(); - let snippet_re = Regex::new( - r#"]*class=\"result__snippet\"[^>]*>(.*?)|]*class=\"result__snippet\"[^>]*>(.*?)"#, - ) - .unwrap(); + let title_re = get_title_re(); + let snippet_re = get_snippet_re(); let snippets: Vec = snippet_re .captures_iter(html) .filter_map(|cap| cap.get(1).or_else(|| cap.get(2))) @@ -202,8 +224,7 @@ fn normalize_text(text: &str) -> String { } fn strip_html_tags(text: &str) -> String { - let tag_re = Regex::new(r"<[^>]+>").unwrap(); - tag_re.replace_all(text, "").to_string() + get_tag_re().replace_all(text, "").to_string() } fn decode_html_entities(text: &str) -> String {