chore(release): prepare v0.8.38 (#1698)

This commit is contained in:
Hunter Bown
2026-05-15 18:08:58 -05:00
committed by GitHub
parent a528ea9824
commit 5401eaae08
18 changed files with 250 additions and 136 deletions
+25 -3
View File
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.38] - 2026-05-15
### Changed
- **Update guidance is clearer on the website.** The homepage and install page
@@ -39,6 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
default providers sync into the runtime config before the first request, and
reselecting the active provider from the picker keeps the current model
instead of falling back to the provider default (#1632).
- **OpenAI-compatible batch tool calls keep all start events.** Streaming
responses with multiple `tool_calls` in one assistant message now preserve
every tool-use block instead of pairing many tool results with only the last
tool start event (#1686).
- **Diagnostics tool schemas include an empty `required` list.** The built-in
`diagnostics` tool now sends `required: []` with its empty object schema so
DeepSeek no longer rejects it as a null required array (#1685).
- **Windows wheel-as-arrow scrolling works with mouse capture enabled.**
`composer_arrows_scroll` now defaults on for Windows terminals even when
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
@@ -51,6 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
blocks now return a failed tool result instead of a success, so repeated
blocked checklist/tool retries can trip the existing failure warning and halt
path instead of spinning indefinitely (#1574).
- **Denied tool approvals are scoped to the exact call.** Denying one
write/shell approval now caches the canonical argument fingerprint instead of
a lossy tool/prefix key, so later calls to the same tool with different
arguments can still be reviewed and approved (#1617).
### Thanks
@@ -59,13 +72,21 @@ terminal cleanup-guard idea harvested from #1630, and **imkingjh999
([@imkingjh999](https://github.com/imkingjh999))** for the provider/model
switching fixes harvested from #1642. Thanks to **Photo
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
picker catalog work harvested from #1201. Thanks to
picker catalog work harvested from #1201. Thanks to **hexin
([@h3c-hexin](https://github.com/h3c-hexin))** for the OpenAI batch tool-call
streaming fix in #1686. Thanks to **chennest
([@chennest](https://github.com/chennest))** for the diagnostics schema report
in #1685. Thanks to
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
composer scroll fix harvested from #1578, and **WuMing
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
PowerShell flicker fix harvested from #1591. Thanks to
**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop
report in #1574.
report in #1574. Thanks to **lalala
([@lalala-233](https://github.com/lalala-233))** for the approval denial
regression report in #1617, and **Nightt
([@nightt5879](https://github.com/nightt5879))** for the exact-call approval
key work harvested from #1624.
## [0.8.37] - 2026-05-14
@@ -4264,7 +4285,8 @@ Welcome — and thank you.
- Hooks system and config profiles
- Example skills and launch assets
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...HEAD
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...HEAD
[0.8.38]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...v0.8.38
[0.8.37]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.36...v0.8.37
[0.8.36]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...v0.8.36
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
Generated
+14 -14
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "deepseek-agent"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"deepseek-config",
"serde",
@@ -1168,7 +1168,7 @@ dependencies = [
[[package]]
name = "deepseek-app-server"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"axum",
@@ -1190,7 +1190,7 @@ dependencies = [
[[package]]
name = "deepseek-config"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"deepseek-secrets",
@@ -1202,7 +1202,7 @@ dependencies = [
[[package]]
name = "deepseek-core"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"chrono",
@@ -1220,7 +1220,7 @@ dependencies = [
[[package]]
name = "deepseek-execpolicy"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"deepseek-protocol",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deepseek-hooks"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"async-trait",
@@ -1243,7 +1243,7 @@ dependencies = [
[[package]]
name = "deepseek-mcp"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"serde",
@@ -1252,7 +1252,7 @@ dependencies = [
[[package]]
name = "deepseek-protocol"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"serde",
"serde_json",
@@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "deepseek-secrets"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"dirs",
"keyring",
@@ -1273,7 +1273,7 @@ dependencies = [
[[package]]
name = "deepseek-state"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"chrono",
@@ -1285,7 +1285,7 @@ dependencies = [
[[package]]
name = "deepseek-tools"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"async-trait",
@@ -1298,7 +1298,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"arboard",
@@ -1361,7 +1361,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-cli"
version = "0.8.37"
version = "0.8.38"
dependencies = [
"anyhow",
"chrono",
@@ -1386,7 +1386,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-core"
version = "0.8.37"
version = "0.8.38"
[[package]]
name = "deltae"
+1 -1
View File
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
resolver = "2"
[workspace.package]
version = "0.8.37"
version = "0.8.38"
edition = "2024"
# Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the
# codebase relies on extensively. Cargo enforces this so users on older
+1 -1
View File
@@ -7,5 +7,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
[dependencies]
deepseek-config = { path = "../config", version = "0.8.37" }
deepseek-config = { path = "../config", version = "0.8.38" }
serde.workspace = true
+9 -9
View File
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.37" }
deepseek-config = { path = "../config", version = "0.8.37" }
deepseek-core = { path = "../core", version = "0.8.37" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
deepseek-hooks = { path = "../hooks", version = "0.8.37" }
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
deepseek-state = { path = "../state", version = "0.8.37" }
deepseek-tools = { path = "../tools", version = "0.8.37" }
deepseek-agent = { path = "../agent", version = "0.8.38" }
deepseek-config = { path = "../config", version = "0.8.38" }
deepseek-core = { path = "../core", version = "0.8.38" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
deepseek-hooks = { path = "../hooks", version = "0.8.38" }
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
deepseek-state = { path = "../state", version = "0.8.38" }
deepseek-tools = { path = "../tools", version = "0.8.38" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+7 -7
View File
@@ -14,13 +14,13 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.37" }
deepseek-app-server = { path = "../app-server", version = "0.8.37" }
deepseek-config = { path = "../config", version = "0.8.37" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
deepseek-state = { path = "../state", version = "0.8.37" }
deepseek-agent = { path = "../agent", version = "0.8.38" }
deepseek-app-server = { path = "../app-server", version = "0.8.38" }
deepseek-config = { path = "../config", version = "0.8.38" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
deepseek-state = { path = "../state", version = "0.8.38" }
chrono.workspace = true
dirs.workspace = true
serde.workspace = true
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
[dependencies]
anyhow.workspace = true
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
dirs.workspace = true
serde.workspace = true
toml.workspace = true
+8 -8
View File
@@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.37" }
deepseek-config = { path = "../config", version = "0.8.37" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.37" }
deepseek-hooks = { path = "../hooks", version = "0.8.37" }
deepseek-mcp = { path = "../mcp", version = "0.8.37" }
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
deepseek-state = { path = "../state", version = "0.8.37" }
deepseek-tools = { path = "../tools", version = "0.8.37" }
deepseek-agent = { path = "../agent", version = "0.8.38" }
deepseek-config = { path = "../config", version = "0.8.38" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.38" }
deepseek-hooks = { path = "../hooks", version = "0.8.38" }
deepseek-mcp = { path = "../mcp", version = "0.8.38" }
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
deepseek-state = { path = "../state", version = "0.8.38" }
deepseek-tools = { path = "../tools", version = "0.8.38" }
serde_json.workspace = true
uuid.workspace = true
+1 -1
View File
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
serde.workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.37" }
deepseek-protocol = { path = "../protocol", version = "0.8.38" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+25 -3
View File
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.38] - 2026-05-15
### Changed
- **Update guidance is clearer on the website.** The homepage and install page
@@ -39,6 +41,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
default providers sync into the runtime config before the first request, and
reselecting the active provider from the picker keeps the current model
instead of falling back to the provider default (#1632).
- **OpenAI-compatible batch tool calls keep all start events.** Streaming
responses with multiple `tool_calls` in one assistant message now preserve
every tool-use block instead of pairing many tool results with only the last
tool start event (#1686).
- **Diagnostics tool schemas include an empty `required` list.** The built-in
`diagnostics` tool now sends `required: []` with its empty object schema so
DeepSeek no longer rejects it as a null required array (#1685).
- **Windows wheel-as-arrow scrolling works with mouse capture enabled.**
`composer_arrows_scroll` now defaults on for Windows terminals even when
mouse capture is enabled, so wheel events that arrive as arrow keys scroll the
@@ -51,6 +60,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
blocks now return a failed tool result instead of a success, so repeated
blocked checklist/tool retries can trip the existing failure warning and halt
path instead of spinning indefinitely (#1574).
- **Denied tool approvals are scoped to the exact call.** Denying one
write/shell approval now caches the canonical argument fingerprint instead of
a lossy tool/prefix key, so later calls to the same tool with different
arguments can still be reviewed and approved (#1617).
### Thanks
@@ -59,13 +72,21 @@ terminal cleanup-guard idea harvested from #1630, and **imkingjh999
([@imkingjh999](https://github.com/imkingjh999))** for the provider/model
switching fixes harvested from #1642. Thanks to **Photo
([@eng2007](https://github.com/eng2007))** for the provider-aware `/model`
picker catalog work harvested from #1201. Thanks to
picker catalog work harvested from #1201. Thanks to **hexin
([@h3c-hexin](https://github.com/h3c-hexin))** for the OpenAI batch tool-call
streaming fix in #1686. Thanks to **chennest
([@chennest](https://github.com/chennest))** for the diagnostics schema report
in #1685. Thanks to
**[@kunpeng-ai-lab](https://github.com/kunpeng-ai-lab)** for the Windows
composer scroll fix harvested from #1578, and **WuMing
([@asdfg314284230](https://github.com/asdfg314284230))** for the Windows
PowerShell flicker fix harvested from #1591. Thanks to
**[@maker316](https://github.com/maker316)** for the LoopGuard/checklist loop
report in #1574.
report in #1574. Thanks to **lalala
([@lalala-233](https://github.com/lalala-233))** for the approval denial
regression report in #1617, and **Nightt
([@nightt5879](https://github.com/nightt5879))** for the exact-call approval
key work harvested from #1624.
## [0.8.37] - 2026-05-14
@@ -4264,7 +4285,8 @@ Welcome — and thank you.
- Hooks system and config profiles
- Example skills and launch assets
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...HEAD
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...HEAD
[0.8.38]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.37...v0.8.38
[0.8.37]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.36...v0.8.37
[0.8.36]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...v0.8.36
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
+2 -2
View File
@@ -21,8 +21,8 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
deepseek-secrets = { path = "../secrets", version = "0.8.37" }
deepseek-tools = { path = "../tools", version = "0.8.37" }
deepseek-secrets = { path = "../secrets", version = "0.8.38" }
deepseek-tools = { path = "../tools", version = "0.8.38" }
schemaui = { version = "0.12.0", default-features = false, optional = true }
async-stream = "0.3.6"
async-trait = "0.1"
+106 -78
View File
@@ -10,8 +10,8 @@
//!
//! | Tool | Key |
//! |---------------|------------------------------------------|
//! | `apply_patch` | `patch:<hash of file paths>` |
//! | `exec_shell` | `shell:<command prefix (first 3 tokens)>` |
//! | file writes | `file:<tool_name>:<hash of args>` |
//! | shell tools | `shell:<tool_name>:<hash of args>` |
//! | `fetch_url` | `net:<hostname>` |
//! | everything else| `tool:<tool_name>:<hash of input>` |
//!
@@ -21,9 +21,11 @@
//! calls with the same fingerprint still prompt).
use std::collections::HashMap;
use std::fmt::Write as _;
use std::time::Instant;
use crate::command_safety::classify_command;
use serde_json::Value;
use sha2::{Digest, Sha256};
/// The fingerprint of a tool call — stable enough to match repeated
/// calls but specific enough to avoid privilege confusion.
@@ -113,97 +115,32 @@ impl ApprovalCache {
/// Build the approvalcache key for a tool call.
///
/// The key incorporates the tool name and a lossy digest of the
/// arguments so that the cache can distinguish `exec_shell "ls"`
/// from `exec_shell "rm -rf /"` while still recognising repeated
/// invocations of the same harmless command.
/// The key incorporates the tool name and a canonical digest of the
/// arguments so that denying one call suppresses exact retries, not later
/// invocations of the same tool with different parameters.
#[must_use]
pub fn build_approval_key(tool_name: &str, input: &serde_json::Value) -> ApprovalKey {
let fingerprint = match tool_name {
"apply_patch" => {
let paths_hash = hash_patch_paths(input);
format!("patch:{paths_hash}")
"apply_patch" | "write_file" | "edit_file" | "fim_edit" => {
format!("file:{tool_name}:{}", hash_json_value(input))
}
"exec_shell"
| "task_shell_start"
| "exec_shell_wait"
| "exec_shell_interact"
| "exec_wait"
| "exec_interact" => {
let prefix = command_prefix(input);
format!("shell:{prefix}")
format!("shell:{tool_name}:{}", hash_json_value(input))
}
"fetch_url" | "web.fetch" | "web_fetch" => {
let host = parse_host(input);
format!("net:{host}")
}
_ => {
let input_hash = hash_json_input(input);
format!("tool:{tool_name}:{input_hash}")
}
_ => format!("tool:{tool_name}:{}", hash_json_value(input)),
};
ApprovalKey(fingerprint)
}
/// Return the canonical command prefix for the shell command in `input`.
///
/// Uses [`classify_command`] from the arity dictionary so that
/// `auto_allow = ["git status"]` correctly matches `git status -s` and
/// `git status --porcelain` without also matching `git push`.
fn command_prefix(input: &serde_json::Value) -> String {
let cmd = input.get("command").and_then(|v| v.as_str()).unwrap_or("");
let tokens: Vec<&str> = cmd.split_whitespace().collect();
if tokens.is_empty() {
return "<empty>".to_string();
}
classify_command(&tokens)
}
/// Hash the sorted set of file paths referenced by a patch input.
fn hash_patch_paths(input: &serde_json::Value) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut paths: Vec<&str> = Vec::new();
if let Some(changes) = input.get("changes").and_then(|v| v.as_array()) {
for change in changes {
if let Some(path) = change.get("path").and_then(|v| v.as_str()) {
paths.push(path);
}
}
} else if let Some(patch_text) = input.get("patch").and_then(|v| v.as_str()) {
for line in patch_text.lines() {
if let Some(rest) = line.strip_prefix("+++ b/") {
paths.push(rest.trim());
}
}
}
paths.sort();
paths.dedup();
if paths.is_empty() {
return "no_files".to_string();
}
let mut hasher = DefaultHasher::new();
for path in &paths {
path.hash(&mut hasher);
}
format!("{:x}", hasher.finish())
}
fn hash_json_input(input: &serde_json::Value) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
serde_json::to_string(input)
.unwrap_or_default()
.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
/// Parse the host portion from a URL input.
fn parse_host(input: &serde_json::Value) -> String {
let url = input.get("url").and_then(|v| v.as_str()).unwrap_or("");
@@ -215,6 +152,64 @@ fn parse_host(input: &serde_json::Value) -> String {
}
}
fn hash_json_value(value: &Value) -> String {
let mut canonical = String::new();
push_canonical_json(value, &mut canonical);
let digest = Sha256::digest(canonical.as_bytes());
let mut short = String::with_capacity(16);
for byte in &digest[..8] {
write!(&mut short, "{byte:02x}").expect("writing to String cannot fail");
}
short
}
fn push_canonical_json(value: &Value, out: &mut String) {
match value {
Value::Null => out.push_str("null"),
Value::Bool(value) => {
out.push_str("bool:");
out.push_str(if *value { "true" } else { "false" });
}
Value::Number(value) => {
out.push_str("number:");
out.push_str(&value.to_string());
}
Value::String(value) => {
out.push_str("string:");
let encoded = serde_json::to_string(value).expect("serializing a string cannot fail");
out.push_str(&encoded);
}
Value::Array(items) => {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push(',');
}
push_canonical_json(item, out);
}
out.push(']');
}
Value::Object(map) => {
let mut entries = map.iter().collect::<Vec<_>>();
entries.sort_by_key(|(key, _)| *key);
out.push('{');
for (index, (key, value)) in entries.into_iter().enumerate() {
if index > 0 {
out.push(',');
}
let encoded_key =
serde_json::to_string(key).expect("serializing an object key cannot fail");
out.push_str(&encoded_key);
out.push(':');
push_canonical_json(value, out);
}
out.push('}');
}
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -258,10 +253,10 @@ mod tests {
}
#[test]
fn command_prefix_drops_flags() {
fn shell_keys_include_full_command_arguments() {
let key_a = build_approval_key("exec_shell", &json!({"command": "cargo build"}));
let key_b = build_approval_key("exec_shell", &json!({"command": "cargo build --release"}));
assert_eq!(key_a, key_b);
assert_ne!(key_a, key_b);
}
#[test]
@@ -277,6 +272,19 @@ mod tests {
assert_ne!(key_a, key_b);
}
#[test]
fn patch_keys_differ_by_body_for_same_path() {
let key_a = build_approval_key(
"apply_patch",
&json!({"changes": [{"path": "a.rs", "content": "x"}]}),
);
let key_b = build_approval_key(
"apply_patch",
&json!({"changes": [{"path": "a.rs", "content": "y"}]}),
);
assert_ne!(key_a, key_b);
}
#[test]
fn net_keys_differ_by_host() {
let key_a = build_approval_key("fetch_url", &json!({"url": "https://example.com"}));
@@ -299,4 +307,24 @@ mod tests {
let key_b = build_approval_key("edit_file", &input);
assert_eq!(key_a, key_b);
}
#[test]
fn input_hash_is_stable_across_object_key_order() {
let key_a = build_approval_key("write_file", &json!({"path": "a.txt", "content": "x"}));
let key_b = build_approval_key("write_file", &json!({"content": "x", "path": "a.txt"}));
assert_eq!(key_a, key_b);
}
#[test]
fn canonical_json_omits_trailing_commas() {
let mut canonical = String::new();
push_canonical_json(&json!({"b": [true, false], "a": {"x": 1}}), &mut canonical);
assert_eq!(
canonical,
r#"{"a":{"x":number:1},"b":[bool:true,bool:false]}"#
);
assert!(!canonical.contains(",]"));
assert!(!canonical.contains(",}"));
}
}
+8
View File
@@ -58,6 +58,7 @@ impl ToolSpec for DiagnosticsTool {
json!({
"type": "object",
"properties": {},
"required": [],
"additionalProperties": false
})
}
@@ -217,6 +218,13 @@ mod tests {
run(&["commit", "-q", "-m", "init"]);
}
#[test]
fn diagnostics_schema_has_empty_required_array() {
let schema = DiagnosticsTool.input_schema();
assert_eq!(schema["properties"], json!({}));
assert_eq!(schema["required"], json!([]));
}
#[tokio::test]
async fn diagnostics_runs_best_effort_outside_git_repo() {
let tmp = tempdir().expect("tempdir");
+11 -4
View File
@@ -150,6 +150,15 @@ const PERIODIC_FULL_REPAINT_EVERY_N: u64 = 50;
const TURN_META_PREFIX: &str = "<turn_meta>";
const SESSION_TITLE_MAX_CHARS: usize = 32;
fn is_session_approved_for_tool(app: &App, tool_name: &str, approval_key: &str) -> bool {
app.approval_session_approved.contains(approval_key)
|| app.approval_session_approved.contains(tool_name)
}
fn is_session_denied_for_key(app: &App, approval_key: &str) -> bool {
app.approval_session_denied.contains(approval_key)
}
fn sidebar_width_for_chat_area(app: &App, chat_width: u16) -> Option<u16> {
if app.sidebar_focus == SidebarFocus::Hidden || chat_width < SIDEBAR_VISIBLE_MIN_WIDTH {
return None;
@@ -1687,10 +1696,8 @@ async fn run_event_loop(
approval_key,
} => {
let session_approved =
app.approval_session_approved.contains(&approval_key)
|| app.approval_session_approved.contains(&tool_name);
let session_denied = app.approval_session_denied.contains(&approval_key)
|| app.approval_session_denied.contains(&tool_name);
is_session_approved_for_tool(app, &tool_name, &approval_key);
let session_denied = is_session_denied_for_key(app, &approval_key);
if session_denied {
// The user already said no to this exact tool /
// approval key in this session; auto-deny so the
+27
View File
@@ -1203,6 +1203,33 @@ fn create_test_app() -> App {
app
}
#[test]
fn session_denied_cache_matches_only_approval_key() {
let mut app = create_test_app();
app.approval_session_denied.insert("edit_file".to_string());
assert!(
!is_session_denied_for_key(&app, "file:edit_file:fresh"),
"a legacy tool-name entry must not deny a later fresh call"
);
app.approval_session_denied
.insert("file:edit_file:retry".to_string());
assert!(is_session_denied_for_key(&app, "file:edit_file:retry"));
}
#[test]
fn session_approved_cache_keeps_tool_name_session_grants() {
let mut app = create_test_app();
app.approval_session_approved
.insert("edit_file".to_string());
assert!(
is_session_approved_for_tool(&app, "edit_file", "file:edit_file:fresh"),
"approve-for-session should still cover future calls of the same tool"
);
}
fn create_test_options() -> TuiOptions {
TuiOptions {
model: "deepseek-v4-pro".to_string(),
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "deepseek-tui",
"version": "0.8.37",
"deepseekBinaryVersion": "0.8.37",
"version": "0.8.38",
"deepseekBinaryVersion": "0.8.38",
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
"author": "Hmbown",
"license": "MIT",