chore(release): prepare v0.8.40
This commit is contained in:
+58
-1
@@ -5,6 +5,62 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.40] - 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- **WSL2 and headless Linux startup no longer blocks on clipboard init.** The
|
||||
TUI now defers clipboard initialization so machines without an X server can
|
||||
reach the first frame instead of hanging on a blank screen (#1773, #1772).
|
||||
- **Windows alt-screen output stays clean when `RUST_LOG` is set.** Runtime
|
||||
tracing is routed away from the interactive buffer so logs no longer leak
|
||||
into the TUI display (#1774, #1776).
|
||||
- **OpenAI-compatible custom model names are preserved.** Non-DeepSeek
|
||||
providers now pass explicit model names through instead of rewriting them to
|
||||
a DeepSeek default (#1714, #1740).
|
||||
- **DeepSeek reasoning replay works through OpenAI-compatible endpoints.**
|
||||
DeepSeek models selected under the generic `openai` provider now replay
|
||||
prior `reasoning_content` consistently and classify streamed reasoning the
|
||||
same way the replay path does (#1694, #1739, #1743).
|
||||
- **Thinking-only turns no longer disappear.** If a clean turn ends with
|
||||
thinking but no final answer text, the UI now surfaces a clear status instead
|
||||
of silently ending the turn (#1727, #1742).
|
||||
- **Windows `cmd /C` preserves quoted shell arguments.** Commands such as
|
||||
`git commit -m "feat: complete sub-pages"` now round-trip through the Windows
|
||||
shell wrapper without losing the quoted message (#1691, #1744).
|
||||
- **Home/End are line-local inside multiline composer drafts.** The keys now
|
||||
jump to the current input line boundary before falling back to transcript
|
||||
navigation (#1748, #1749).
|
||||
- **Ctrl+C restores the canceled prompt reliably.** Canceling a streaming turn
|
||||
puts the submitted prompt back in the composer and suppresses late stream
|
||||
events from drawing stale output (#1757, #1764).
|
||||
- **Compaction recovers from cache-aligned summary context overflow.** When a
|
||||
cache-preserving summary request itself exceeds the provider context window,
|
||||
compaction retries with the bounded formatted summary path instead of failing
|
||||
with a 400 "compression command failed" style error.
|
||||
- **Terminal sub-agent sessions expose full transcript handles.** Completed
|
||||
and canceled child agents now store the full child message transcript behind
|
||||
`transcript_handle`, so the parent can inspect details with `handle_read`
|
||||
instead of relying only on a lossy summary (#1738).
|
||||
- **Repeated shell wait rows collapse in the Tasks sidebar.** Multiple live
|
||||
`task_shell_wait` polls for the same background job now render as one row
|
||||
with an explicit collapsed-wait count, reducing the stuck-task appearance
|
||||
tracked for v0.8.40 (#1737).
|
||||
|
||||
### Thanks
|
||||
|
||||
Thanks to **jayzhu ([@zlh124](https://github.com/zlh124))** for the WSL2
|
||||
startup report and clipboard-init fix in #1772/#1773. Thanks to **Paulo Aboim
|
||||
Pinto ([@aboimpinto](https://github.com/aboimpinto))** for the Windows
|
||||
alt-screen logging report and fix in #1774/#1776, and for the Home/End
|
||||
composer work in #1748/#1749. Thanks to **Zhongyue Lin
|
||||
([@LeoLin990405](https://github.com/LeoLin990405))** for the provider model
|
||||
passthrough, reasoning replay, thinking-only turn, and Windows quoting fixes
|
||||
in #1740, #1743, #1742, and #1744. Thanks to **Nightt
|
||||
([@nightt5879](https://github.com/nightt5879))** for the Ctrl+C prompt restore
|
||||
fix in #1764. Thanks to **Bevis** and the community reports that surfaced the
|
||||
compaction failure mode addressed in this release.
|
||||
|
||||
## [0.8.39] - 2026-05-17
|
||||
|
||||
### Fixed
|
||||
@@ -4364,7 +4420,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.39...HEAD
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.40...HEAD
|
||||
[0.8.40]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.39...v0.8.40
|
||||
[0.8.39]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...v0.8.39
|
||||
[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
|
||||
|
||||
Generated
+14
-14
@@ -1160,7 +1160,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-agent"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"deepseek-config",
|
||||
"serde",
|
||||
@@ -1168,7 +1168,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-app-server"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
@@ -1190,7 +1190,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-config"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-secrets",
|
||||
@@ -1202,7 +1202,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-core"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1220,7 +1220,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-execpolicy"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deepseek-protocol",
|
||||
@@ -1229,7 +1229,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-hooks"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1243,7 +1243,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-mcp"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
@@ -1252,7 +1252,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-protocol"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -1260,7 +1260,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-secrets"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"keyring",
|
||||
@@ -1273,7 +1273,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-state"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1285,7 +1285,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tools"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -1298,7 +1298,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
@@ -1361,7 +1361,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-cli"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
@@ -1386,7 +1386,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui-core"
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
|
||||
[[package]]
|
||||
name = "deltae"
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.8.39"
|
||||
version = "0.8.40"
|
||||
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
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-config = { path = "../config", version = "0.8.40" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-config = { path = "../config", version = "0.8.39" }
|
||||
deepseek-core = { path = "../core", version = "0.8.39" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.39" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.39" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.39" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.39" }
|
||||
deepseek-state = { path = "../state", version = "0.8.39" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.39" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.40" }
|
||||
deepseek-config = { path = "../config", version = "0.8.40" }
|
||||
deepseek-core = { path = "../core", version = "0.8.40" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.40" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.40" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.40" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.40" }
|
||||
deepseek-state = { path = "../state", version = "0.8.40" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.40" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.39" }
|
||||
deepseek-config = { path = "../config", version = "0.8.39" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.39" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.39" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.39" }
|
||||
deepseek-state = { path = "../state", version = "0.8.39" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.40" }
|
||||
deepseek-app-server = { path = "../app-server", version = "0.8.40" }
|
||||
deepseek-config = { path = "../config", version = "0.8.40" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.40" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.40" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.40" }
|
||||
deepseek-state = { path = "../state", version = "0.8.40" }
|
||||
chrono.workspace = true
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.40" }
|
||||
dirs.workspace = true
|
||||
serde.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-config = { path = "../config", version = "0.8.39" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.39" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.39" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.39" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.39" }
|
||||
deepseek-state = { path = "../state", version = "0.8.39" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.39" }
|
||||
deepseek-agent = { path = "../agent", version = "0.8.40" }
|
||||
deepseek-config = { path = "../config", version = "0.8.40" }
|
||||
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.40" }
|
||||
deepseek-hooks = { path = "../hooks", version = "0.8.40" }
|
||||
deepseek-mcp = { path = "../mcp", version = "0.8.40" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.40" }
|
||||
deepseek-state = { path = "../state", version = "0.8.40" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.40" }
|
||||
serde_json.workspace = true
|
||||
uuid.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.40" }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.40" }
|
||||
reqwest.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -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.39" }
|
||||
deepseek-protocol = { path = "../protocol", version = "0.8.40" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
tokio.workspace = true
|
||||
|
||||
+58
-1
@@ -5,6 +5,62 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.8.40] - 2026-05-18
|
||||
|
||||
### Fixed
|
||||
|
||||
- **WSL2 and headless Linux startup no longer blocks on clipboard init.** The
|
||||
TUI now defers clipboard initialization so machines without an X server can
|
||||
reach the first frame instead of hanging on a blank screen (#1773, #1772).
|
||||
- **Windows alt-screen output stays clean when `RUST_LOG` is set.** Runtime
|
||||
tracing is routed away from the interactive buffer so logs no longer leak
|
||||
into the TUI display (#1774, #1776).
|
||||
- **OpenAI-compatible custom model names are preserved.** Non-DeepSeek
|
||||
providers now pass explicit model names through instead of rewriting them to
|
||||
a DeepSeek default (#1714, #1740).
|
||||
- **DeepSeek reasoning replay works through OpenAI-compatible endpoints.**
|
||||
DeepSeek models selected under the generic `openai` provider now replay
|
||||
prior `reasoning_content` consistently and classify streamed reasoning the
|
||||
same way the replay path does (#1694, #1739, #1743).
|
||||
- **Thinking-only turns no longer disappear.** If a clean turn ends with
|
||||
thinking but no final answer text, the UI now surfaces a clear status instead
|
||||
of silently ending the turn (#1727, #1742).
|
||||
- **Windows `cmd /C` preserves quoted shell arguments.** Commands such as
|
||||
`git commit -m "feat: complete sub-pages"` now round-trip through the Windows
|
||||
shell wrapper without losing the quoted message (#1691, #1744).
|
||||
- **Home/End are line-local inside multiline composer drafts.** The keys now
|
||||
jump to the current input line boundary before falling back to transcript
|
||||
navigation (#1748, #1749).
|
||||
- **Ctrl+C restores the canceled prompt reliably.** Canceling a streaming turn
|
||||
puts the submitted prompt back in the composer and suppresses late stream
|
||||
events from drawing stale output (#1757, #1764).
|
||||
- **Compaction recovers from cache-aligned summary context overflow.** When a
|
||||
cache-preserving summary request itself exceeds the provider context window,
|
||||
compaction retries with the bounded formatted summary path instead of failing
|
||||
with a 400 "compression command failed" style error.
|
||||
- **Terminal sub-agent sessions expose full transcript handles.** Completed
|
||||
and canceled child agents now store the full child message transcript behind
|
||||
`transcript_handle`, so the parent can inspect details with `handle_read`
|
||||
instead of relying only on a lossy summary (#1738).
|
||||
- **Repeated shell wait rows collapse in the Tasks sidebar.** Multiple live
|
||||
`task_shell_wait` polls for the same background job now render as one row
|
||||
with an explicit collapsed-wait count, reducing the stuck-task appearance
|
||||
tracked for v0.8.40 (#1737).
|
||||
|
||||
### Thanks
|
||||
|
||||
Thanks to **jayzhu ([@zlh124](https://github.com/zlh124))** for the WSL2
|
||||
startup report and clipboard-init fix in #1772/#1773. Thanks to **Paulo Aboim
|
||||
Pinto ([@aboimpinto](https://github.com/aboimpinto))** for the Windows
|
||||
alt-screen logging report and fix in #1774/#1776, and for the Home/End
|
||||
composer work in #1748/#1749. Thanks to **Zhongyue Lin
|
||||
([@LeoLin990405](https://github.com/LeoLin990405))** for the provider model
|
||||
passthrough, reasoning replay, thinking-only turn, and Windows quoting fixes
|
||||
in #1740, #1743, #1742, and #1744. Thanks to **Nightt
|
||||
([@nightt5879](https://github.com/nightt5879))** for the Ctrl+C prompt restore
|
||||
fix in #1764. Thanks to **Bevis** and the community reports that surfaced the
|
||||
compaction failure mode addressed in this release.
|
||||
|
||||
## [0.8.39] - 2026-05-17
|
||||
|
||||
### Fixed
|
||||
@@ -4364,7 +4420,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.39...HEAD
|
||||
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.40...HEAD
|
||||
[0.8.40]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.39...v0.8.40
|
||||
[0.8.39]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.38...v0.8.39
|
||||
[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
|
||||
|
||||
@@ -21,8 +21,8 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
anyhow = "1.0.100"
|
||||
arboard = "3.4"
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.39" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.39" }
|
||||
deepseek-secrets = { path = "../secrets", version = "0.8.40" }
|
||||
deepseek-tools = { path = "../tools", version = "0.8.40" }
|
||||
schemaui = { version = "0.12.0", default-features = false, optional = true }
|
||||
async-stream = "0.3.6"
|
||||
async-trait = "0.1"
|
||||
|
||||
@@ -2716,12 +2716,8 @@ mod tests {
|
||||
]
|
||||
});
|
||||
|
||||
let result = sanitize_thinking_mode_messages(
|
||||
&mut body,
|
||||
"gpt-4o",
|
||||
Some("max"),
|
||||
ApiProvider::Openai,
|
||||
);
|
||||
let result =
|
||||
sanitize_thinking_mode_messages(&mut body, "gpt-4o", Some("max"), ApiProvider::Openai);
|
||||
|
||||
assert!(result.is_none());
|
||||
let assistant = body["messages"]
|
||||
|
||||
@@ -1155,7 +1155,20 @@ async fn create_summary(
|
||||
build_formatted_summary_request(model, messages, limits)
|
||||
};
|
||||
|
||||
let response = client.create_message(request).await?;
|
||||
let mut telemetry_cache_aligned = used_cache_aligned;
|
||||
let response = match client.create_message(request).await {
|
||||
Ok(response) => response,
|
||||
Err(err) if used_cache_aligned && is_context_window_error(&err) => {
|
||||
logging::warn(format!(
|
||||
"Cache-aligned compaction summary exceeded the model context window ({err}); \
|
||||
retrying with bounded formatted summary input"
|
||||
));
|
||||
telemetry_cache_aligned = false;
|
||||
let fallback_request = build_formatted_summary_request(model, messages, limits);
|
||||
client.create_message(fallback_request).await?
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
};
|
||||
// Compaction summary calls are billed by DeepSeek; route the
|
||||
// tokens through the side-channel so the dashboard total
|
||||
// matches the website (#526).
|
||||
@@ -1168,7 +1181,7 @@ async fn create_summary(
|
||||
// `RUST_LOG=compaction=debug` (the module-path form
|
||||
// `deepseek_tui::compaction=debug` does NOT match — `EnvFilter`
|
||||
// matches the explicit target string when one is set).
|
||||
log_summary_cache_telemetry(used_cache_aligned, &response.usage);
|
||||
log_summary_cache_telemetry(telemetry_cache_aligned, &response.usage);
|
||||
|
||||
// Extract text from response
|
||||
let summary = response
|
||||
@@ -1184,6 +1197,22 @@ async fn create_summary(
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
fn is_context_window_error(e: &anyhow::Error) -> bool {
|
||||
let text = e.to_string();
|
||||
if crate::error_taxonomy::classify_error_message(&text)
|
||||
!= crate::error_taxonomy::ErrorCategory::InvalidInput
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let lower = text.to_lowercase();
|
||||
lower.contains("context")
|
||||
|| lower.contains("token")
|
||||
|| lower.contains("prompt is too long")
|
||||
|| lower.contains("requested")
|
||||
|| lower.contains("maximum")
|
||||
}
|
||||
|
||||
/// Cache-hit percentage for a compaction summary call.
|
||||
///
|
||||
/// Denominator is `input_tokens` (the total prompt size), not
|
||||
@@ -1806,6 +1835,50 @@ mod tests {
|
||||
assert!((summary_cache_hit_percent(50, 0) - 0.0).abs() < f64::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_window_errors_are_detected_for_summary_fallback() {
|
||||
for msg in [
|
||||
"HTTP 400 Bad Request: maximum context length is 1000000 tokens",
|
||||
"invalid_request_error: prompt is too long for the current model",
|
||||
"You requested 1000001 tokens but the maximum is 1000000",
|
||||
"request exceeds context window",
|
||||
] {
|
||||
assert!(
|
||||
is_context_window_error(&anyhow::anyhow!(msg)),
|
||||
"expected context-window detection for `{msg}`",
|
||||
);
|
||||
}
|
||||
|
||||
assert!(!is_context_window_error(&anyhow::anyhow!(
|
||||
"Invalid request: missing required field"
|
||||
)));
|
||||
assert!(!is_context_window_error(&anyhow::anyhow!(
|
||||
"503 Service Unavailable"
|
||||
)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formatted_summary_request_bounds_large_input() {
|
||||
let messages = (0..90)
|
||||
.map(|idx| {
|
||||
msg(
|
||||
"user",
|
||||
&format!("turn {idx}: {}", "中文上下文 ".repeat(1_000)),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let limits = summary_input_limits_for_model("deepseek-v4-pro");
|
||||
|
||||
let request = build_formatted_summary_request("deepseek-v4-pro", &messages, limits);
|
||||
|
||||
assert_eq!(request.messages.len(), 1);
|
||||
let ContentBlock::Text { text, .. } = &request.messages[0].content[0] else {
|
||||
panic!("expected summary text request");
|
||||
};
|
||||
assert!(text.contains("characters omitted before summary"));
|
||||
assert!(text.chars().count() <= limits.input_max_chars + 2_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cache_aligned_summary_request_preserves_message_prefix() {
|
||||
let messages = vec![
|
||||
|
||||
@@ -2392,7 +2392,9 @@ fn apply_env_overrides(config: &mut Config) {
|
||||
.providers
|
||||
.get_or_insert_with(ProvidersConfig::default);
|
||||
let entry = match provider {
|
||||
ApiProvider::Deepseek | ApiProvider::DeepseekCN => unreachable!("DeepSeek providers are handled in the if branch above (issue #1714)"),
|
||||
ApiProvider::Deepseek | ApiProvider::DeepseekCN => unreachable!(
|
||||
"DeepSeek providers are handled in the if branch above (issue #1714)"
|
||||
),
|
||||
ApiProvider::NvidiaNim => &mut providers.nvidia_nim,
|
||||
ApiProvider::Openai => &mut providers.openai,
|
||||
ApiProvider::Atlascloud => &mut providers.atlascloud,
|
||||
|
||||
@@ -2045,11 +2045,7 @@ fn should_emit_thinking_only_status(
|
||||
steers_pending: bool,
|
||||
holding_for_subagents: bool,
|
||||
) -> bool {
|
||||
tool_uses_empty
|
||||
&& turn_error_is_none
|
||||
&& !cancelled
|
||||
&& !steers_pending
|
||||
&& !holding_for_subagents
|
||||
tool_uses_empty && turn_error_is_none && !cancelled && !steers_pending && !holding_for_subagents
|
||||
}
|
||||
|
||||
/// Resolve an `"auto"` reasoning-effort tier to a concrete value.
|
||||
|
||||
@@ -200,10 +200,7 @@ fn push_shell_args(cmd: &mut Command, program: &str, args: &[String]) {
|
||||
.and_then(|s| s.to_str())
|
||||
.map(|s| s.eq_ignore_ascii_case("cmd"))
|
||||
.unwrap_or(false);
|
||||
if is_cmd
|
||||
&& args.len() == 2
|
||||
&& args[0].eq_ignore_ascii_case("/C")
|
||||
{
|
||||
if is_cmd && args.len() == 2 && args[0].eq_ignore_ascii_case("/C") {
|
||||
cmd.raw_arg(&args[0]);
|
||||
cmd.raw_arg(&args[1]);
|
||||
} else {
|
||||
|
||||
@@ -819,7 +819,11 @@ fn test_list_jobs_cleans_up_completed_old_processes() {
|
||||
#[test]
|
||||
fn issue_1691_quoted_commit_message_round_trips() {
|
||||
let cmd = r#"git commit -m "feat: complete sub-pages""#;
|
||||
let spec = CommandSpec::shell(cmd, std::path::PathBuf::from("/tmp"), Duration::from_secs(5));
|
||||
let spec = CommandSpec::shell(
|
||||
cmd,
|
||||
std::path::PathBuf::from("/tmp"),
|
||||
Duration::from_secs(5),
|
||||
);
|
||||
|
||||
#[cfg(not(windows))]
|
||||
{
|
||||
|
||||
@@ -1624,6 +1624,7 @@ async fn subagent_session_projection(
|
||||
timed_out: bool,
|
||||
context: &ToolContext,
|
||||
) -> SubAgentSessionProjection {
|
||||
let transcript_session_id = format!("agent:{}", snapshot.agent_id);
|
||||
let transcript_payload = json!({
|
||||
"kind": "subagent_session_snapshot",
|
||||
"agent_id": snapshot.agent_id.clone(),
|
||||
@@ -1639,11 +1640,22 @@ async fn subagent_session_projection(
|
||||
});
|
||||
let transcript_handle = {
|
||||
let mut store = context.runtime.handle_store.lock().await;
|
||||
store.insert_json(
|
||||
format!("agent:{}", snapshot.agent_id),
|
||||
"transcript",
|
||||
transcript_payload,
|
||||
)
|
||||
let full_transcript_lookup = VarHandle {
|
||||
kind: "var_handle".to_string(),
|
||||
session_id: transcript_session_id.clone(),
|
||||
name: "full_transcript".to_string(),
|
||||
type_name: String::new(),
|
||||
length: 0,
|
||||
repr_preview: String::new(),
|
||||
sha256: String::new(),
|
||||
};
|
||||
if snapshot.status != SubAgentStatus::Running
|
||||
&& let Some(record) = store.get(&full_transcript_lookup)
|
||||
{
|
||||
record.handle.clone()
|
||||
} else {
|
||||
store.insert_json(transcript_session_id, "transcript", transcript_payload)
|
||||
}
|
||||
};
|
||||
|
||||
SubAgentSessionProjection {
|
||||
@@ -2160,7 +2172,7 @@ impl ToolSpec for AgentEvalTool {
|
||||
}
|
||||
|
||||
fn description(&self) -> &'static str {
|
||||
"Fetch or wait on a child sub-agent session. Optionally deliver a message/items to a running session, then return the latest session projection. With block=true (default), waits for the session to reach a terminal boundary; block=false is a non-blocking status fetch."
|
||||
"Fetch or wait on a child sub-agent session. Optionally deliver a message/items to a running session, then return the latest session projection. With block=true (default), waits for the session to reach a terminal boundary; block=false is a non-blocking status fetch. Terminal projections expose a handle_read-compatible transcript_handle for the full child transcript."
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> Value {
|
||||
@@ -3292,6 +3304,36 @@ fn subagent_failed_sentinel(agent_id: &str, _err: &str) -> String {
|
||||
format!("<deepseek:subagent.done>{payload}</deepseek:subagent.done>")
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn insert_subagent_full_transcript_handle(
|
||||
runtime: &SubAgentRuntime,
|
||||
agent_id: &str,
|
||||
agent_type: &SubAgentType,
|
||||
assignment: &SubAgentAssignment,
|
||||
status: &SubAgentStatus,
|
||||
result: Option<&String>,
|
||||
messages: &[Message],
|
||||
steps_taken: u32,
|
||||
duration_ms: u64,
|
||||
fork_context: bool,
|
||||
) -> VarHandle {
|
||||
let payload = json!({
|
||||
"kind": "subagent_full_transcript",
|
||||
"agent_id": agent_id,
|
||||
"agent_type": agent_type.as_str(),
|
||||
"status": subagent_status_name(status),
|
||||
"context_mode": if fork_context { "forked" } else { "fresh" },
|
||||
"fork_context": fork_context,
|
||||
"result": result,
|
||||
"steps_taken": steps_taken,
|
||||
"duration_ms": duration_ms,
|
||||
"assignment": assignment,
|
||||
"messages": messages,
|
||||
});
|
||||
let mut store = runtime.context.runtime.handle_store.lock().await;
|
||||
store.insert_json(format!("agent:{agent_id}"), "full_transcript", payload)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments, clippy::too_many_lines)]
|
||||
async fn run_subagent(
|
||||
runtime: &SubAgentRuntime,
|
||||
@@ -3362,6 +3404,21 @@ async fn run_subagent(
|
||||
agent_id: agent_id.clone(),
|
||||
});
|
||||
}
|
||||
let status = SubAgentStatus::Cancelled;
|
||||
let duration_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
insert_subagent_full_transcript_handle(
|
||||
runtime,
|
||||
&agent_id,
|
||||
&agent_type,
|
||||
&assignment,
|
||||
&status,
|
||||
None,
|
||||
&messages,
|
||||
steps,
|
||||
duration_ms,
|
||||
fork_context_enabled,
|
||||
)
|
||||
.await;
|
||||
return Ok(SubAgentResult {
|
||||
name: agent_id.clone(),
|
||||
agent_id: agent_id.clone(),
|
||||
@@ -3376,10 +3433,10 @@ async fn run_subagent(
|
||||
assignment: assignment.clone(),
|
||||
model: runtime.model.clone(),
|
||||
nickname: None,
|
||||
status: SubAgentStatus::Cancelled,
|
||||
status,
|
||||
result: None,
|
||||
steps_taken: steps,
|
||||
duration_ms: u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX),
|
||||
duration_ms,
|
||||
from_prior_session: false,
|
||||
});
|
||||
}
|
||||
@@ -3443,6 +3500,21 @@ async fn run_subagent(
|
||||
agent_id: agent_id.clone(),
|
||||
});
|
||||
}
|
||||
let status = SubAgentStatus::Cancelled;
|
||||
let duration_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
insert_subagent_full_transcript_handle(
|
||||
runtime,
|
||||
&agent_id,
|
||||
&agent_type,
|
||||
&assignment,
|
||||
&status,
|
||||
None,
|
||||
&messages,
|
||||
steps,
|
||||
duration_ms,
|
||||
fork_context_enabled,
|
||||
)
|
||||
.await;
|
||||
return Ok(SubAgentResult {
|
||||
name: agent_id.clone(),
|
||||
agent_id: agent_id.clone(),
|
||||
@@ -3452,11 +3524,10 @@ async fn run_subagent(
|
||||
assignment: assignment.clone(),
|
||||
model: runtime.model.clone(),
|
||||
nickname: None,
|
||||
status: SubAgentStatus::Cancelled,
|
||||
status,
|
||||
result: None,
|
||||
steps_taken: steps,
|
||||
duration_ms: u64::try_from(started_at.elapsed().as_millis())
|
||||
.unwrap_or(u64::MAX),
|
||||
duration_ms,
|
||||
from_prior_session: false,
|
||||
});
|
||||
}
|
||||
@@ -3582,6 +3653,21 @@ async fn run_subagent(
|
||||
}
|
||||
|
||||
release_resident_leases_for(&agent_id);
|
||||
let status = SubAgentStatus::Completed;
|
||||
let duration_ms = u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX);
|
||||
insert_subagent_full_transcript_handle(
|
||||
runtime,
|
||||
&agent_id,
|
||||
&agent_type,
|
||||
&assignment,
|
||||
&status,
|
||||
final_result.as_ref(),
|
||||
&messages,
|
||||
steps,
|
||||
duration_ms,
|
||||
fork_context_enabled,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(SubAgentResult {
|
||||
name: agent_id.clone(),
|
||||
@@ -3597,10 +3683,10 @@ async fn run_subagent(
|
||||
assignment,
|
||||
model: runtime.model.clone(),
|
||||
nickname: None,
|
||||
status: SubAgentStatus::Completed,
|
||||
status,
|
||||
result: final_result,
|
||||
steps_taken: steps,
|
||||
duration_ms: u64::try_from(started_at.elapsed().as_millis()).unwrap_or(u64::MAX),
|
||||
duration_ms,
|
||||
from_prior_session: false,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -358,6 +358,38 @@ async fn session_projection_exposes_forked_prefix_cache_contract() {
|
||||
assert_eq!(projection.transcript_handle.name, "transcript");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn terminal_session_projection_prefers_full_transcript_handle() {
|
||||
let mut snapshot = make_snapshot(SubAgentStatus::Completed);
|
||||
snapshot.result = Some("done".to_string());
|
||||
|
||||
let ctx = ToolContext::new(".");
|
||||
let full_handle = {
|
||||
let mut store = ctx.runtime.handle_store.lock().await;
|
||||
store.insert_json(
|
||||
"agent:agent_test",
|
||||
"full_transcript",
|
||||
json!({
|
||||
"kind": "subagent_full_transcript",
|
||||
"agent_id": "agent_test",
|
||||
"messages": [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": [
|
||||
{ "type": "text", "text": "complete child output" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}),
|
||||
)
|
||||
};
|
||||
|
||||
let projection = subagent_session_projection(snapshot, false, &ctx).await;
|
||||
|
||||
assert_eq!(projection.transcript_handle, full_handle);
|
||||
assert_eq!(projection.transcript_handle.name, "full_transcript");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delegate_defaults_to_fork_context() {
|
||||
let input = with_default_fork_context(json!({ "prompt": "review current work" }), true);
|
||||
|
||||
@@ -1096,6 +1096,7 @@ fn editorial_tool_rows(rows: Vec<SidebarToolRow>, limit: usize) -> Vec<SidebarTo
|
||||
let mut candidates: Vec<Candidate> = Vec::new();
|
||||
let mut low_value_groups: Vec<(usize, SidebarToolRow, usize)> = Vec::new();
|
||||
let mut ci_poll_groups: Vec<(usize, SidebarToolRow, usize)> = Vec::new();
|
||||
let mut shell_wait_groups: Vec<(usize, SidebarToolRow, usize, String)> = Vec::new();
|
||||
let mut seen_success: Vec<String> = Vec::new();
|
||||
|
||||
for (order, mut row) in rows.into_iter().enumerate() {
|
||||
@@ -1118,6 +1119,22 @@ fn editorial_tool_rows(rows: Vec<SidebarToolRow>, limit: usize) -> Vec<SidebarTo
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_shell_wait_poll_row(&row) {
|
||||
let key = shell_wait_poll_key(&row);
|
||||
if let Some((_, grouped, count, _)) = shell_wait_groups
|
||||
.iter_mut()
|
||||
.find(|(_, _, _, existing_key)| existing_key == &key)
|
||||
{
|
||||
*count += 1;
|
||||
if !row.summary.trim().is_empty() {
|
||||
grouped.summary = row.summary;
|
||||
}
|
||||
} else {
|
||||
shell_wait_groups.push((order, row, 1, key));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if is_low_value_tool(&row.name) && row.status == ToolStatus::Success {
|
||||
if let Some((_, grouped, count)) = low_value_groups
|
||||
.iter_mut()
|
||||
@@ -1165,6 +1182,20 @@ fn editorial_tool_rows(rows: Vec<SidebarToolRow>, limit: usize) -> Vec<SidebarTo
|
||||
});
|
||||
}
|
||||
|
||||
for (order, mut row, count, key) in shell_wait_groups {
|
||||
if count > 1 {
|
||||
row.summary = compact_join([
|
||||
format!("{key} \u{00B7} {count} waits collapsed"),
|
||||
row.summary.clone(),
|
||||
]);
|
||||
}
|
||||
candidates.push(Candidate {
|
||||
rank: tool_row_rank(&row),
|
||||
order,
|
||||
row,
|
||||
});
|
||||
}
|
||||
|
||||
for (order, mut row, count) in low_value_groups {
|
||||
if count > 1 {
|
||||
row.name = format!("{} x{count}", row.name);
|
||||
@@ -1199,6 +1230,27 @@ fn is_ci_poll_row(row: &SidebarToolRow) -> bool {
|
||||
row.name.starts_with("gh pr checks") || row.name.starts_with("gh run watch")
|
||||
}
|
||||
|
||||
fn is_shell_wait_poll_row(row: &SidebarToolRow) -> bool {
|
||||
row.status == ToolStatus::Running && row.name == "wait shell job"
|
||||
}
|
||||
|
||||
fn shell_wait_poll_key(row: &SidebarToolRow) -> String {
|
||||
const MARKER: &str = "task_id:";
|
||||
if let Some((_, rest)) = row.summary.split_once(MARKER) {
|
||||
let task_id = rest
|
||||
.trim_start()
|
||||
.split(|ch: char| ch.is_whitespace() || ch == ',' || ch == '\u{00B7}')
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
.trim();
|
||||
if !task_id.is_empty() {
|
||||
return task_id.to_string();
|
||||
}
|
||||
}
|
||||
|
||||
normalize_activity_text(&row.summary)
|
||||
}
|
||||
|
||||
fn normalize_activity_text(text: &str) -> String {
|
||||
text.split_whitespace().collect::<Vec<_>>().join(" ")
|
||||
}
|
||||
@@ -2359,6 +2411,42 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tasks_panel_collapses_repeated_shell_waits_for_same_job() {
|
||||
let mut app = create_test_app();
|
||||
let mut active = ActiveCell::new();
|
||||
for id in ["shell-wait-1", "shell-wait-2"] {
|
||||
active.push_tool(
|
||||
id,
|
||||
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
|
||||
name: "task_shell_wait".to_string(),
|
||||
status: ToolStatus::Running,
|
||||
input_summary: Some("task_id: shell_33a08c3c".to_string()),
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: Some("Background task running (no new output).".to_string()),
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
app.active_cell = Some(active);
|
||||
|
||||
let text = lines_to_text(&task_panel_lines(&app, 100, 8));
|
||||
|
||||
assert_eq!(
|
||||
text.iter()
|
||||
.filter(|line| line.contains("[~] wait shell job"))
|
||||
.count(),
|
||||
1,
|
||||
"duplicate waits for the same shell job should collapse: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
text.iter().any(|line| line.contains("2 waits collapsed")),
|
||||
"collapsed row should explain why only one wait is visible: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_empty_state_says_no_agents() {
|
||||
let summary = SidebarSubagentSummary::default();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "deepseek-tui",
|
||||
"version": "0.8.39",
|
||||
"deepseekBinaryVersion": "0.8.39",
|
||||
"version": "0.8.40",
|
||||
"deepseekBinaryVersion": "0.8.40",
|
||||
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
|
||||
"author": "Hmbown",
|
||||
"license": "MIT",
|
||||
|
||||
Reference in New Issue
Block a user