chore(release): start v0.8.35 branch

- Bump workspace, internal crate pins, npm wrapper metadata, generated facts, and docs from 0.8.34 to 0.8.35

- Clarify 60% manual compact guidance vs 80% opt-in automatic guardrail

- Expire completed live-tool rows and collapse stale running shell rows in the Tasks sidebar
This commit is contained in:
Hunter Bown
2026-05-13 13:36:15 -05:00
parent f0a4e25360
commit 0ab95aea1c
26 changed files with 381 additions and 109 deletions
+32 -9
View File
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.35] - 2026-05-13
A post-0.8.34 cleanup release focused on prompt hygiene, context-pressure
guidance, and keeping the next release branch clearly separated from the
already-published v0.8.34 tag.
### Changed
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% suggestion threshold.
- **Context-pressure guidance now has one split rule.** Manual
`/compact` suggestions start around 60% during sustained work, while
automatic replacement compaction remains an opt-in hard guardrail near
80% so DeepSeek V4 prefix-cache economics stay intact.
- **The Tasks sidebar now ages out stale live-tool noise.** Completed
active tool rows linger briefly and then leave the right rail; very old
running shell rows collapse to a single row instead of occupying the
whole Tasks panel.
### Fixed
- **`auto_compact` settings help now reports the real default**, which
has been off since v0.8.11 to avoid unnecessary cache-prefix rewrites.
## [0.8.34] - 2026-05-13
A polish, terminal-protocol, and internal-cleanup release. The model-facing
@@ -65,14 +95,6 @@ mega-files that had grown around the agent loop and TUI.
(v0.8.6 era), `PROMPT_ANALYSIS.md`, and the redundant
`DEPENDENCY_GRAPH.md` no longer ship in releases; `docs/ARCHITECTURE.md`
remains the canonical crate-layout reference.
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% threshold.
### Fixed
@@ -4078,7 +4100,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.34...HEAD
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...HEAD
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
[0.8.34]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.33...v0.8.34
[0.8.33]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.32...v0.8.33
[0.8.32]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.31...v0.8.32
Generated
+14 -14
View File
@@ -1160,7 +1160,7 @@ dependencies = [
[[package]]
name = "deepseek-agent"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"deepseek-config",
"serde",
@@ -1168,7 +1168,7 @@ dependencies = [
[[package]]
name = "deepseek-app-server"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"axum",
@@ -1190,7 +1190,7 @@ dependencies = [
[[package]]
name = "deepseek-config"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"deepseek-secrets",
@@ -1202,7 +1202,7 @@ dependencies = [
[[package]]
name = "deepseek-core"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"chrono",
@@ -1220,7 +1220,7 @@ dependencies = [
[[package]]
name = "deepseek-execpolicy"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"deepseek-protocol",
@@ -1229,7 +1229,7 @@ dependencies = [
[[package]]
name = "deepseek-hooks"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"async-trait",
@@ -1243,7 +1243,7 @@ dependencies = [
[[package]]
name = "deepseek-mcp"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"serde",
@@ -1252,7 +1252,7 @@ dependencies = [
[[package]]
name = "deepseek-protocol"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"serde",
"serde_json",
@@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "deepseek-secrets"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"dirs",
"keyring",
@@ -1273,7 +1273,7 @@ dependencies = [
[[package]]
name = "deepseek-state"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"chrono",
@@ -1285,7 +1285,7 @@ dependencies = [
[[package]]
name = "deepseek-tools"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"async-trait",
@@ -1298,7 +1298,7 @@ dependencies = [
[[package]]
name = "deepseek-tui"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"arboard",
@@ -1361,7 +1361,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-cli"
version = "0.8.34"
version = "0.8.35"
dependencies = [
"anyhow",
"chrono",
@@ -1386,7 +1386,7 @@ dependencies = [
[[package]]
name = "deepseek-tui-core"
version = "0.8.34"
version = "0.8.35"
[[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.34"
version = "0.8.35"
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
+18 -25
View File
@@ -241,34 +241,27 @@ deepseek --provider ollama --model deepseek-coder:1.3b
---
## What's New In v0.8.34
## What's New In v0.8.35
A polish, terminal-protocol, and internal-cleanup release. The
model-facing tool surface stays stable while v0.8.34 improves first-run
skills, terminal notifications, prompt-cache visibility, MCP transport
compatibility, and the maintainability of the largest TUI files.
A post-release cleanup branch for the `v0.8.34` line. It keeps the
model-facing surface stable while trimming first-turn context, clarifying
context-pressure behavior, and reducing sidebar noise during long runs.
[Full changelog](CHANGELOG.md).
- **Bundled DeepSeek-native workflow skills.** Fresh installs now get
first-party skills for delegation, skill creation, MCP/plugin setup,
documents, presentations, spreadsheets, PDFs, and Feishu/Lark.
- **User skills stay visible.** `/skills` separates user-created skills
from built-ins so workspace and global skills do not disappear behind
the bundled catalog.
- **MCP HTTP defaults are more compatible.** Streamable HTTP requests
default to `Accept: application/json, text/event-stream` and preserve
`Mcp-Session-Id` across requests.
- **Terminal notifications cover more terminals.** Kitty `OSC 99` and
Ghostty `OSC 777` join the existing notification paths.
- **Prefix-cache stability is visible.** The footer surfaces cache
stability so users can notice cache-busting changes before cost climbs.
- **`edit_file` handles typographic punctuation drift.** With
`fuzz: true`, smart quotes, en/em dashes, and non-breaking spaces no
longer prevent a safe replacement when the file uses ASCII punctuation.
- **Large internals are getting smaller.** Focused modules now own
auto-routing, Vim-mode handling, workspace context, streaming thinking,
notifications, file-picker relevance, formatting helpers, and key
shortcut predicates.
- **First-turn context is leaner.** Hidden tool/cache state is excluded
from the generated project pack, and `/context` now names prompt layers
instead of showing one opaque blob.
- **Prompt rules are de-conflicted.** Useful `deepseek` diagnostics are
allowed, simple one-step work no longer forces checklist ceremony, and
sustained sessions consistently suggest `/compact` around 60%.
- **Automatic compaction stays conservative.** The 80% threshold remains
an opt-in hard guardrail so DeepSeek V4 prefix-cache behavior is not
disturbed by default.
- **The Tasks sidebar settles down.** Completed live-tool rows expire
after a short linger, and very old running shell rows collapse instead
of filling the right rail.
- **`auto_compact` help is honest.** Settings now report the real default:
off.
---
+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.34" }
deepseek-config = { path = "../config", version = "0.8.35" }
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.34" }
deepseek-config = { path = "../config", version = "0.8.34" }
deepseek-core = { path = "../core", version = "0.8.34" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.34" }
deepseek-hooks = { path = "../hooks", version = "0.8.34" }
deepseek-mcp = { path = "../mcp", version = "0.8.34" }
deepseek-protocol = { path = "../protocol", version = "0.8.34" }
deepseek-state = { path = "../state", version = "0.8.34" }
deepseek-tools = { path = "../tools", version = "0.8.34" }
deepseek-agent = { path = "../agent", version = "0.8.35" }
deepseek-config = { path = "../config", version = "0.8.35" }
deepseek-core = { path = "../core", version = "0.8.35" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.35" }
deepseek-hooks = { path = "../hooks", version = "0.8.35" }
deepseek-mcp = { path = "../mcp", version = "0.8.35" }
deepseek-protocol = { path = "../protocol", version = "0.8.35" }
deepseek-state = { path = "../state", version = "0.8.35" }
deepseek-tools = { path = "../tools", version = "0.8.35" }
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.34" }
deepseek-app-server = { path = "../app-server", version = "0.8.34" }
deepseek-config = { path = "../config", version = "0.8.34" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.34" }
deepseek-mcp = { path = "../mcp", version = "0.8.34" }
deepseek-secrets = { path = "../secrets", version = "0.8.34" }
deepseek-state = { path = "../state", version = "0.8.34" }
deepseek-agent = { path = "../agent", version = "0.8.35" }
deepseek-app-server = { path = "../app-server", version = "0.8.35" }
deepseek-config = { path = "../config", version = "0.8.35" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.35" }
deepseek-mcp = { path = "../mcp", version = "0.8.35" }
deepseek-secrets = { path = "../secrets", version = "0.8.35" }
deepseek-state = { path = "../state", version = "0.8.35" }
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.34" }
deepseek-secrets = { path = "../secrets", version = "0.8.35" }
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.34" }
deepseek-config = { path = "../config", version = "0.8.34" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.34" }
deepseek-hooks = { path = "../hooks", version = "0.8.34" }
deepseek-mcp = { path = "../mcp", version = "0.8.34" }
deepseek-protocol = { path = "../protocol", version = "0.8.34" }
deepseek-state = { path = "../state", version = "0.8.34" }
deepseek-tools = { path = "../tools", version = "0.8.34" }
deepseek-agent = { path = "../agent", version = "0.8.35" }
deepseek-config = { path = "../config", version = "0.8.35" }
deepseek-execpolicy = { path = "../execpolicy", version = "0.8.35" }
deepseek-hooks = { path = "../hooks", version = "0.8.35" }
deepseek-mcp = { path = "../mcp", version = "0.8.35" }
deepseek-protocol = { path = "../protocol", version = "0.8.35" }
deepseek-state = { path = "../state", version = "0.8.35" }
deepseek-tools = { path = "../tools", version = "0.8.35" }
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.34" }
deepseek-protocol = { path = "../protocol", version = "0.8.35" }
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.34" }
deepseek-protocol = { path = "../protocol", version = "0.8.35" }
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.34" }
deepseek-protocol = { path = "../protocol", version = "0.8.35" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+32 -9
View File
@@ -7,6 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.8.35] - 2026-05-13
A post-0.8.34 cleanup release focused on prompt hygiene, context-pressure
guidance, and keeping the next release branch clearly separated from the
already-published v0.8.34 tag.
### Changed
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% suggestion threshold.
- **Context-pressure guidance now has one split rule.** Manual
`/compact` suggestions start around 60% during sustained work, while
automatic replacement compaction remains an opt-in hard guardrail near
80% so DeepSeek V4 prefix-cache economics stay intact.
- **The Tasks sidebar now ages out stale live-tool noise.** Completed
active tool rows linger briefly and then leave the right rail; very old
running shell rows collapse to a single row instead of occupying the
whole Tasks panel.
### Fixed
- **`auto_compact` settings help now reports the real default**, which
has been off since v0.8.11 to avoid unnecessary cache-prefix rewrites.
## [0.8.34] - 2026-05-13
A polish, terminal-protocol, and internal-cleanup release. The model-facing
@@ -65,14 +95,6 @@ mega-files that had grown around the agent loop and TUI.
(v0.8.6 era), `PROMPT_ANALYSIS.md`, and the redundant
`DEPENDENCY_GRAPH.md` no longer ship in releases; `docs/ARCHITECTURE.md`
remains the canonical crate-layout reference.
- **First-turn prompt context is leaner and easier to audit.** The
generated project context pack now ignores hidden tool/cache state,
balances top-level directories before descending, and `/context`
shows named prompt layers instead of a single opaque system blob.
- **Model-visible prompt policy de-conflicted.** The base and mode
prompts no longer forbid useful `deepseek` CLI diagnostics, no
longer require checklists for simple one-step work, and align
long-session compaction guidance around the 60% threshold.
### Fixed
@@ -4078,7 +4100,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.34...HEAD
[Unreleased]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.35...HEAD
[0.8.35]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.34...v0.8.35
[0.8.34]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.33...v0.8.34
[0.8.33]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.32...v0.8.33
[0.8.32]: https://github.com/Hmbown/DeepSeek-TUI/compare/v0.8.31...v0.8.32
+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.34" }
deepseek-tools = { path = "../tools", version = "0.8.34" }
deepseek-secrets = { path = "../secrets", version = "0.8.35" }
deepseek-tools = { path = "../tools", version = "0.8.35" }
schemaui = { version = "0.12.0", default-features = false, optional = true }
async-stream = "0.3.6"
async-trait = "0.1"
+5 -1
View File
@@ -53,7 +53,11 @@ impl Default for CompactionConfig {
// v0.8.11: 50K was a 128K-era leftover that biased every
// unconfigured caller toward "compact almost immediately on V4."
// Bumped to 800K (80% of V4's 1M window) so the dead-code
// default no longer lies. Real call sites override this via
// default matches the hard automatic compaction guardrail. This
// is intentionally later than the model-visible 60% "suggest
// /compact during sustained work" guidance; automatic replacement
// compaction rewrites the cacheable prefix and remains opt-in.
// Real call sites override this via
// `compaction_threshold_for_model_and_effort`.
token_threshold: 800_000,
model: DEFAULT_TEXT_MODEL.to_string(),
+8 -2
View File
@@ -7,8 +7,14 @@ pub const THRESHOLDS: [(f32, &str); 3] = [
0.9,
"Context at 90%: stop and write relay to .deepseek/handoff.md now",
),
(0.8, "Context at 80%: draft relay to .deepseek/handoff.md"),
(0.7, "Context at 70%: consider wrapping current sub-task"),
(
0.8,
"Context at 80%: urgent hard-limit pressure; compact or write relay now",
),
(
0.6,
"Context at 60%: prepare relay or suggest /compact for sustained work",
),
];
#[allow(dead_code)]
pub fn threshold_message(ratio: f32) -> Option<&'static str> {
+4 -2
View File
@@ -263,7 +263,9 @@ fn deepseek_context_window_hint(model_lower: &str) -> Option<u32> {
/// Derive a compaction token threshold from model context window.
///
/// Keeps headroom for tool outputs and assistant completion by defaulting to 80%
/// of known context windows.
/// of known context windows. This is the hard automatic compaction threshold
/// used only when `auto_compact` is enabled; model-facing guidance still
/// suggests manual `/compact` earlier (~60%) during sustained work.
#[must_use]
pub fn compaction_threshold_for_model(model: &str) -> usize {
let Some(window) = context_window_for_model(model) else {
@@ -279,7 +281,7 @@ pub fn compaction_threshold_for_model(model: &str) -> usize {
/// Replacement-style compaction rewrites the stable prefix, which works against
/// DeepSeek V4 prefix-cache economics. Reasoning effort must not lower V4's
/// automatic replacement threshold; V4-family models use the same late
/// 80%-of-window guard as `compaction_threshold_for_model`.
/// 80%-of-window hard guard as `compaction_threshold_for_model`.
#[must_use]
pub fn compaction_threshold_for_model_and_effort(
model: &str,
+1 -1
View File
@@ -702,7 +702,7 @@ impl Settings {
vec![
(
"auto_compact",
"Auto-compact near context limit: on/off (default on)",
"Auto-compact near the hard context limit: on/off (default off)",
),
("calm_mode", "Calmer UI defaults: on/off"),
(
+7
View File
@@ -987,6 +987,10 @@ pub struct App {
/// virtual index can shift (orphan completions push real cells in
/// between). Migrated into `tool_details_by_cell` on flush.
pub active_tool_details: HashMap<String, ToolDetailRecord>,
/// Completion timestamps for entries still living inside `active_cell`.
/// The transcript keeps completed entries until turn flush, but the
/// sidebar can use these timestamps to let settled live rows expire.
pub active_tool_entry_completed_at: HashMap<usize, Instant>,
/// Active exploring cell entry index (within `active_cell.entries`).
/// `None` once the active cell flushes or no exploring entry exists.
pub exploring_cell: Option<usize>,
@@ -1563,6 +1567,7 @@ impl App {
active_cell: None,
active_cell_revision: 0,
active_tool_details: HashMap::new(),
active_tool_entry_completed_at: HashMap::new(),
exploring_cell: None,
exploring_entries: HashMap::new(),
ignored_tool_calls: HashSet::new(),
@@ -2360,6 +2365,7 @@ impl App {
self.exploring_cell = None;
self.exploring_entries.clear();
self.active_tool_details.clear();
self.active_tool_entry_completed_at.clear();
self.streaming_thinking_active_entry = None;
self.bump_active_cell_revision();
return;
@@ -2375,6 +2381,7 @@ impl App {
let base_index = self.history.len();
let mut details = std::mem::take(&mut self.active_tool_details);
self.active_tool_entry_completed_at.clear();
for (tool_id, detail) in details.drain() {
self.tool_details_by_cell
.entry(self.tool_cells.get(&tool_id).copied().unwrap_or(base_index))
+177 -8
View File
@@ -5,6 +5,7 @@
//! reads from `App` snapshots; mutation lives in the main app loop.
use std::fmt::Write;
use std::time::Duration;
use ratatui::{
Frame,
@@ -31,6 +32,8 @@ use super::ui::truncate_line_to_width;
/// does not prematurely hide the session+agents breakdown.
const COST_EQ_TOLERANCE: f64 = 1e-6;
const RECENT_TOOL_SCAN_LIMIT: usize = 24;
const ACTIVE_TOOL_COMPLETED_ROW_TTL: Duration = Duration::from_secs(8);
const ACTIVE_TOOL_STALE_RUNNING_ROW_TTL: Duration = Duration::from_secs(600);
pub fn render_sidebar(f: &mut Frame, area: Rect, app: &App) {
if area.width < 24 || area.height < 8 {
@@ -660,14 +663,80 @@ fn active_tool_rows(app: &App) -> Vec<SidebarToolRow> {
let Some(active) = app.active_cell.as_ref() else {
return Vec::new();
};
let rows: Vec<SidebarToolRow> = active
.entries()
.iter()
.filter_map(sidebar_tool_row_from_cell)
.collect();
let mut rows: Vec<SidebarToolRow> = Vec::new();
let mut stale_running: Vec<SidebarToolRow> = Vec::new();
for (entry_idx, cell) in active.entries().iter().enumerate() {
let Some(row) = sidebar_tool_row_from_cell(cell) else {
continue;
};
match active_tool_row_visibility(app, entry_idx, &row) {
ActiveToolRowVisibility::Visible => rows.push(row),
ActiveToolRowVisibility::StaleRunning => stale_running.push(row),
ActiveToolRowVisibility::Hidden => {}
}
}
if !stale_running.is_empty() {
rows.push(collapsed_stale_running_row(stale_running));
}
editorial_tool_rows(rows, usize::MAX)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ActiveToolRowVisibility {
Visible,
StaleRunning,
Hidden,
}
fn active_tool_row_visibility(
app: &App,
entry_idx: usize,
row: &SidebarToolRow,
) -> ActiveToolRowVisibility {
if row.status == ToolStatus::Running {
return if row
.duration_ms
.is_some_and(|ms| ms >= duration_ms(ACTIVE_TOOL_STALE_RUNNING_ROW_TTL))
{
ActiveToolRowVisibility::StaleRunning
} else {
ActiveToolRowVisibility::Visible
};
}
let Some(completed_at) = app.active_tool_entry_completed_at.get(&entry_idx) else {
return ActiveToolRowVisibility::Hidden;
};
if completed_at.elapsed() <= ACTIVE_TOOL_COMPLETED_ROW_TTL {
ActiveToolRowVisibility::Visible
} else {
ActiveToolRowVisibility::Hidden
}
}
fn collapsed_stale_running_row(rows: Vec<SidebarToolRow>) -> SidebarToolRow {
let count = rows.len();
let oldest_ms = rows
.iter()
.filter_map(|row| row.duration_ms)
.max()
.unwrap_or_default();
let first_summary = rows
.iter()
.find_map(|row| (!row.summary.trim().is_empty()).then(|| row.summary.clone()))
.unwrap_or_else(|| "open Activity Detail".to_string());
SidebarToolRow {
name: if count == 1 {
"run".to_string()
} else {
format!("run x{count}")
},
status: ToolStatus::Running,
summary: format!("long-running · {first_summary}"),
duration_ms: (oldest_ms > 0).then_some(oldest_ms),
}
}
fn recent_tool_rows(app: &App, limit: usize) -> Vec<SidebarToolRow> {
let rows: Vec<SidebarToolRow> = app
.history
@@ -1014,6 +1083,10 @@ fn format_duration_ms(ms: u64) -> String {
}
}
fn duration_ms(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
fn render_sidebar_subagents(f: &mut Frame, area: Rect, app: &App) {
if area.height < 3 {
return;
@@ -1489,9 +1562,10 @@ fn render_sidebar_section(
#[cfg(test)]
mod tests {
use super::{
AutoSidebarPanel, AutoSidebarState, SidebarAgentRow, SidebarSubagentSummary,
SidebarWorkChecklistItem, SidebarWorkStrategyStep, SidebarWorkSummary, auto_sidebar_panels,
subagent_panel_lines, task_panel_lines, work_panel_empty_hint, work_panel_lines,
ACTIVE_TOOL_COMPLETED_ROW_TTL, ACTIVE_TOOL_STALE_RUNNING_ROW_TTL, AutoSidebarPanel,
AutoSidebarState, SidebarAgentRow, SidebarSubagentSummary, SidebarWorkChecklistItem,
SidebarWorkStrategyStep, SidebarWorkSummary, auto_sidebar_panels, subagent_panel_lines,
task_panel_lines, work_panel_empty_hint, work_panel_lines,
};
use crate::config::Config;
use crate::palette::PaletteMode;
@@ -1504,6 +1578,7 @@ mod tests {
};
use ratatui::text::Line;
use std::path::PathBuf;
use std::time::{Duration, Instant};
fn create_test_app() -> App {
let options = TuiOptions {
@@ -1728,6 +1803,100 @@ mod tests {
);
}
#[test]
fn tasks_panel_expires_completed_active_tool_rows() {
let mut app = create_test_app();
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "read_file".to_string(),
status: ToolStatus::Success,
input_summary: Some("src/main.rs".to_string()),
output: Some("done".to_string()),
prompts: None,
spillover_path: None,
output_summary: Some("done".to_string()),
is_diff: false,
})),
);
app.active_cell = Some(active);
app.active_tool_entry_completed_at.insert(
0,
Instant::now() - ACTIVE_TOOL_COMPLETED_ROW_TTL - Duration::from_secs(1),
);
let text = lines_to_text(&task_panel_lines(&app, 64, 8));
assert!(
!text.iter().any(|line| line.contains("[x] read_file")),
"expired completed active row should leave the sidebar: {text:?}"
);
}
#[test]
fn tasks_panel_lingers_fresh_completed_active_tool_rows() {
let mut app = create_test_app();
let mut active = ActiveCell::new();
active.push_tool(
"tool-1",
HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
name: "read_file".to_string(),
status: ToolStatus::Success,
input_summary: Some("src/main.rs".to_string()),
output: Some("done".to_string()),
prompts: None,
spillover_path: None,
output_summary: Some("done".to_string()),
is_diff: false,
})),
);
app.active_cell = Some(active);
app.active_tool_entry_completed_at.insert(0, Instant::now());
let text = lines_to_text(&task_panel_lines(&app, 64, 8));
assert!(
text.iter().any(|line| line.contains("[x] read_file")),
"fresh completed active row should linger briefly: {text:?}"
);
}
#[test]
fn tasks_panel_collapses_stale_running_tool_rows() {
let mut app = create_test_app();
let mut active = ActiveCell::new();
for (idx, command) in ["long one", "long two"].into_iter().enumerate() {
active.push_tool(
format!("shell-{idx}"),
HistoryCell::Tool(ToolCell::Exec(ExecCell {
command: command.to_string(),
status: ToolStatus::Running,
output: None,
started_at: Some(
Instant::now() - ACTIVE_TOOL_STALE_RUNNING_ROW_TTL - Duration::from_secs(1),
),
duration_ms: None,
source: ExecSource::Assistant,
interaction: None,
output_summary: None,
})),
);
}
app.active_cell = Some(active);
let text = lines_to_text(&task_panel_lines(&app, 80, 8));
assert!(
text.iter().any(|line| line.contains("[~] run x2")),
"stale running rows should collapse into one sidebar row: {text:?}"
);
assert!(
!text.iter().any(|line| line.contains("long two")),
"second stale command should not take another row: {text:?}"
);
}
#[test]
fn tasks_panel_does_not_double_count_running_shell_job_as_live_and_background() {
let mut app = create_test_app();
+44
View File
@@ -53,6 +53,7 @@ pub(super) fn handle_tool_call_started(
// starts in a single ExploringCell entry.
let active = app.active_cell.as_mut().expect("active_cell just ensured");
let entry_idx = active.ensure_exploring();
app.active_tool_entry_completed_at.remove(&entry_idx);
let inner = active
.append_to_exploring(
id.clone(),
@@ -281,6 +282,7 @@ fn push_active_tool_cell(
}
let active = app.active_cell.as_mut().expect("active_cell just ensured");
let entry_idx = active.push_tool(tool_id.to_string(), cell);
app.active_tool_entry_completed_at.remove(&entry_idx);
let virtual_index = app.history.len() + entry_idx;
register_tool_cell(app, tool_id, tool_name, input, virtual_index);
app.mark_history_updated();
@@ -490,6 +492,7 @@ pub(super) fn handle_tool_call_complete(
}
}
}
refresh_active_tool_completion_timestamp(app, cell_index);
return;
}
@@ -646,6 +649,7 @@ pub(super) fn handle_tool_call_complete(
if let Some(active) = app.active_cell.as_mut() {
active.bump_revision();
}
refresh_active_tool_completion_timestamp(app, cell_index);
}
// #455 (observer-only): fire `tool_call_after` hooks once the
@@ -668,6 +672,46 @@ pub(super) fn handle_tool_call_complete(
}
}
fn refresh_active_tool_completion_timestamp(app: &mut App, cell_index: usize) {
if cell_index < app.history.len() {
return;
}
let entry_idx = cell_index - app.history.len();
let Some(cell) = app.cell_at_virtual_index(cell_index) else {
app.active_tool_entry_completed_at.remove(&entry_idx);
return;
};
if history_cell_has_running_tool(cell) {
app.active_tool_entry_completed_at.remove(&entry_idx);
} else {
app.active_tool_entry_completed_at
.entry(entry_idx)
.or_insert_with(Instant::now);
}
}
fn history_cell_has_running_tool(cell: &HistoryCell) -> bool {
let HistoryCell::Tool(tool) = cell else {
return false;
};
match tool {
ToolCell::Exec(exec) => exec.status == ToolStatus::Running,
ToolCell::Exploring(explore) => explore
.entries
.iter()
.any(|entry| entry.status == ToolStatus::Running),
ToolCell::PlanUpdate(plan) => plan.status == ToolStatus::Running,
ToolCell::PatchSummary(patch) => patch.status == ToolStatus::Running,
ToolCell::Review(review) => review.status == ToolStatus::Running,
ToolCell::DiffPreview(_) => false,
ToolCell::Mcp(mcp) => mcp.status == ToolStatus::Running,
ToolCell::ViewImage(_) => false,
ToolCell::WebSearch(search) => search.status == ToolStatus::Running,
ToolCell::Generic(generic) => generic.status == ToolStatus::Running,
}
}
/// Build a finalized standalone history cell for a tool completion whose
/// start was never registered (orphan). This preserves the contract that
/// every tool result is visible somewhere; the alternative (silently
+1
View File
@@ -6192,6 +6192,7 @@ fn apply_loaded_session(app: &mut App, config: &Config, session: &SavedSession)
app.tool_details_by_cell.clear();
app.active_cell = None;
app.active_tool_details.clear();
app.active_tool_entry_completed_at.clear();
app.active_cell_revision = app.active_cell_revision.wrapping_add(1);
app.exploring_cell = None;
app.exploring_entries.clear();
+1 -1
View File
@@ -7,7 +7,7 @@ Current boundary note (v0.8.6):
- Other workspace crates are being split out incrementally, but they are not yet the sole runtime source of truth.
- The LSP subsystem (`crates/tui/src/lsp/`) is fully wired into the engine's post-tool-execution path
(`core/engine/lsp_hooks.rs`), providing inline diagnostics after every edit_file/apply_patch/write_file.
- The swarm agent system was removed in v0.8.5. The active v0.8.34 orchestration surface is persistent sub-agent sessions (`agent_open` / `agent_eval` / `agent_close`) and persistent RLM sessions (`rlm_open` / `rlm_eval` / `rlm_configure` / `rlm_close`).
- The swarm agent system was removed in v0.8.5. The active v0.8.35 orchestration surface is persistent sub-agent sessions (`agent_open` / `agent_eval` / `agent_close`) and persistent RLM sessions (`rlm_open` / `rlm_eval` / `rlm_configure` / `rlm_close`).
No model-visible swarm tool remains in the active codebase.
## High-Level Overview
+2 -2
View File
@@ -15,7 +15,7 @@ chosen over the available shell equivalent. Companion to `crates/tui/src/prompts
for the same backing operation are a model trap — the LLM will alternate
between them and the cache hit rate suffers.
## Current surface (v0.8.34)
## Current surface (v0.8.35)
### File operations
@@ -269,7 +269,7 @@ rg -n '"handle_read"|"rlm_open"|"rlm_eval"|"rlm_configure"|"rlm_close"|"agent_op
rg -n 'handle_read|rlm_open|rlm_eval|rlm_configure|rlm_close|agent_open|agent_eval|agent_close' docs crates/tui/src/prompts crates/tui/src/tools
```
The canonical v0.8.34 live names are:
The canonical v0.8.35 live names are:
- `handle_read`
- `rlm_open`, `rlm_eval`, `rlm_configure`, `rlm_close`
+2 -2
View File
@@ -1,7 +1,7 @@
{
"name": "deepseek-tui",
"version": "0.8.34",
"deepseekBinaryVersion": "0.8.34",
"version": "0.8.35",
"deepseekBinaryVersion": "0.8.35",
"description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.",
"author": "Hmbown",
"license": "MIT",
+1 -1
View File
@@ -19,7 +19,7 @@ export interface RepoFacts {
export const FACTS: RepoFacts = {
"generatedAt": "2026-05-13T06:15:45.167Z",
"version": "0.8.34",
"version": "0.8.35",
"crates": [
"agent",
"app-server",