diff --git a/CHANGELOG.md b/CHANGELOG.md index 82b0ce2e..1a2cad5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index cacb5036..882f8491 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index c1e3e08a..20c227d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/README.md b/README.md index abeb7759..1ef47c5f 100644 --- a/README.md +++ b/README.md @@ -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. --- diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 5deb9e28..6d848674 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -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 diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml index 33cbed9f..bf2ec57c 100644 --- a/crates/app-server/Cargo.toml +++ b/crates/app-server/Cargo.toml @@ -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 diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index fa6cb1fc..3c175c88 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -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 diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 148db37c..2e7a4d81 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -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 diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index b70ee484..9edbf8e7 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -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 diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml index 691f8c03..1b95a475 100644 --- a/crates/execpolicy/Cargo.toml +++ b/crates/execpolicy/Cargo.toml @@ -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 diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index 901f71ff..db2063a3 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -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 diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 07525aef..25dcefef 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -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 diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 82b0ce2e..1a2cad5a 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -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 diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 25c949ba..28887b54 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -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" diff --git a/crates/tui/src/compaction.rs b/crates/tui/src/compaction.rs index d9e29c04..8ffd73ac 100644 --- a/crates/tui/src/compaction.rs +++ b/crates/tui/src/compaction.rs @@ -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(), diff --git a/crates/tui/src/handoff.rs b/crates/tui/src/handoff.rs index ae64fcb9..5098ea0e 100644 --- a/crates/tui/src/handoff.rs +++ b/crates/tui/src/handoff.rs @@ -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> { diff --git a/crates/tui/src/models.rs b/crates/tui/src/models.rs index 5aae3bb0..a5f52c6d 100644 --- a/crates/tui/src/models.rs +++ b/crates/tui/src/models.rs @@ -263,7 +263,9 @@ fn deepseek_context_window_hint(model_lower: &str) -> Option { /// 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, diff --git a/crates/tui/src/settings.rs b/crates/tui/src/settings.rs index 0b65f932..c849b73d 100644 --- a/crates/tui/src/settings.rs +++ b/crates/tui/src/settings.rs @@ -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"), ( diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index ae104996..6b030262 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -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, + /// 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, /// Active exploring cell entry index (within `active_cell.entries`). /// `None` once the active cell flushes or no exploring entry exists. pub exploring_cell: Option, @@ -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)) diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index 9e01b21b..e8f74d29 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -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 { let Some(active) = app.active_cell.as_ref() else { return Vec::new(); }; - let rows: Vec = active - .entries() - .iter() - .filter_map(sidebar_tool_row_from_cell) - .collect(); + let mut rows: Vec = Vec::new(); + let mut stale_running: Vec = 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 { + 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 { let rows: Vec = 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(); diff --git a/crates/tui/src/tui/tool_routing.rs b/crates/tui/src/tui/tool_routing.rs index 6f7d631b..d11f0a97 100644 --- a/crates/tui/src/tui/tool_routing.rs +++ b/crates/tui/src/tui/tool_routing.rs @@ -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 diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 44dadd61..9b22bb8a 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -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(); diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7e7a4e4f..7b07741e 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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 diff --git a/docs/TOOL_SURFACE.md b/docs/TOOL_SURFACE.md index 9984519b..b1305c89 100644 --- a/docs/TOOL_SURFACE.md +++ b/docs/TOOL_SURFACE.md @@ -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` diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json index 6afbc722..476826a6 100644 --- a/npm/deepseek-tui/package.json +++ b/npm/deepseek-tui/package.json @@ -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", diff --git a/web/lib/facts.generated.ts b/web/lib/facts.generated.ts index 8d538409..7533be5f 100644 --- a/web/lib/facts.generated.ts +++ b/web/lib/facts.generated.ts @@ -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",