chore: release v0.2.2

- Fix session save serialization error handling
- Cache web_search regex patterns with OnceLock for performance
- Improve panic messages in tool_parser regex compilation
- Add retroactive CHANGELOG entry for v0.2.1

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Hunter Bown
2026-01-22 12:57:13 -06:00
parent 5092f67557
commit 5d3408f632
6 changed files with 57 additions and 16 deletions
+15 -1
View File
@@ -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
Generated
+1 -1
View File
@@ -646,7 +646,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
version = "0.2.1"
version = "0.2.2"
dependencies = [
"anyhow",
"arboard",
+1 -1
View File
@@ -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"
+5 -1
View File
@@ -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!(
+6 -4
View File
@@ -53,7 +53,8 @@ static THINKING_REGEX: OnceLock<Regex> = 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 <deepseek:tool_call>...</deepseek:tool_call> or similar XML patterns
Regex::new(r"(?s)<(?:deepseek:)?tool_call[^>]*>\s*(.*?)\s*</(?:deepseek:)?tool_call>")
.unwrap()
.expect("XML tool_call regex pattern is valid")
})
}
fn get_invoke_regex() -> &'static Regex {
INVOKE_REGEX.get_or_init(|| {
// Match <invoke name="tool_name">...</invoke> patterns
Regex::new(r#"(?s)<invoke\s+name\s*=\s*"([^"]+)"[^>]*>(.*?)</invoke>"#).unwrap()
Regex::new(r#"(?s)<invoke\s+name\s*=\s*"([^"]+)"[^>]*>(.*?)</invoke>"#)
.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)</?(?:think|thinking)[^>]*>").unwrap()
Regex::new(r"(?s)</?(?:think|thinking)[^>]*>").expect("thinking regex pattern is valid")
})
}
+29 -8
View File
@@ -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<Regex> = OnceLock::new();
static SNIPPET_RE: OnceLock<Regex> = OnceLock::new();
static TAG_RE: OnceLock<Regex> = OnceLock::new();
fn get_title_re() -> &'static Regex {
TITLE_RE.get_or_init(|| {
Regex::new(r#"<a[^>]*class=\"result__a\"[^>]*href=\"([^\"]+)\"[^>]*>(.*?)</a>"#)
.expect("title regex pattern is valid")
})
}
fn get_snippet_re() -> &'static Regex {
SNIPPET_RE.get_or_init(|| {
Regex::new(
r#"<a[^>]*class=\"result__snippet\"[^>]*>(.*?)</a>|<div[^>]*class=\"result__snippet\"[^>]*>(.*?)</div>"#,
)
.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<WebSearchEntry> {
let title_re =
Regex::new(r#"<a[^>]*class=\"result__a\"[^>]*href=\"([^\"]+)\"[^>]*>(.*?)</a>"#).unwrap();
let snippet_re = Regex::new(
r#"<a[^>]*class=\"result__snippet\"[^>]*>(.*?)</a>|<div[^>]*class=\"result__snippet\"[^>]*>(.*?)</div>"#,
)
.unwrap();
let title_re = get_title_re();
let snippet_re = get_snippet_re();
let snippets: Vec<String> = 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 {