From 6c25a18b423b789c97d8a0408eee7134b7a55b9d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 10 May 2026 08:41:04 -0500 Subject: [PATCH] chore(release): bump to v0.8.27, add CHANGELOG --- .claude/HANDOFF_v0.8.27_user_issues.md | 642 +++++++++++++++++++++++++ CHANGELOG.md | 90 ++++ Cargo.toml | 2 +- crates/agent/Cargo.toml | 2 +- crates/app-server/Cargo.toml | 18 +- crates/cli/Cargo.toml | 14 +- crates/config/Cargo.toml | 2 +- crates/config/src/lib.rs | 4 +- crates/core/Cargo.toml | 16 +- crates/execpolicy/Cargo.toml | 2 +- crates/hooks/Cargo.toml | 2 +- crates/tools/Cargo.toml | 2 +- crates/tui/Cargo.toml | 4 +- crates/tui/src/client.rs | 6 +- crates/tui/src/config.rs | 2 +- crates/tui/src/session_manager.rs | 1 + crates/tui/src/tui/active_cell.rs | 6 +- crates/tui/src/tui/history.rs | 72 +-- crates/tui/src/tui/sidebar.rs | 9 +- crates/tui/src/tui/tool_routing.rs | 17 +- crates/tui/src/tui/transcript.rs | 2 +- crates/tui/src/tui/ui.rs | 14 +- crates/tui/src/tui/ui/tests.rs | 52 +- crates/tui/src/tui/widgets/mod.rs | 12 +- npm/deepseek-tui/package.json | 4 +- web/lib/community-agent-tasks.ts | 7 + web/lib/community-agent.ts | 12 +- web/lib/deepseek.ts | 12 +- web/lib/facts.generated.ts | 4 +- web/wrangler.jsonc | 3 +- 30 files changed, 895 insertions(+), 140 deletions(-) create mode 100644 .claude/HANDOFF_v0.8.27_user_issues.md diff --git a/.claude/HANDOFF_v0.8.27_user_issues.md b/.claude/HANDOFF_v0.8.27_user_issues.md new file mode 100644 index 00000000..8b1da1e1 --- /dev/null +++ b/.claude/HANDOFF_v0.8.27_user_issues.md @@ -0,0 +1,642 @@ +# v0.8.27 — User-Issue Strategy Handoff + +**Audience:** the AI agent picking up post-v0.8.26 user-bug work. +**Scope:** the issues filed by users in the 24–48 hours after v0.8.26 +shipped, plus older issues with concrete fix shapes that didn't make +v0.8.26. +**This is layered on top of the in-flight v0.8.27 cycle** — there are +already 16 community-PR commits on `work/v0.8.27`. Don't start over; +add to it. + +--- + +## Where you are + +- **Working tree:** `/Volumes/VIXinSSD/whalebro/deepseek-tui` +- **Active branch:** `work/v0.8.27` (off main at v0.8.26 tip) +- **Already on the branch:** 16 commits — community PRs (#1316, #1317, + #1181, #1203, #1140, #1247, #1223, #1185, #1220, #1233, #1235, + #1197, plus a trackpad scroll fix and a card-rail UI tweak) +- **Reference docs:** `.claude/HANDOFF_v0.8.26_security.md` for the + release flow steps 7–11 (same shape applies for v0.8.27) +- **Issue board:** GitHub `Hmbown/DeepSeek-TUI` + +The previous agent only did community-PR cherry-picks. The strategic +bug-fix work in this document is **not started**. Assume zero +overlap. + +--- + +## Hard rules + +1. **STOP and ask Hunter** before merging the v0.8.27 PR, tagging, + or publishing to crates.io / npm / Homebrew. +2. No `--no-verify`, no `--no-gpg-sign`, no force push. +3. Don't leak `.private/` content into PRs / CHANGELOG / release notes. +4. v0.8.27 is **NOT** a security release. If a new GHSA arrives mid- + cycle, branch v0.8.28 — don't bundle. +5. Time-box thorny items at 30 min. Defer to v0.8.28 instead of + sinking the cycle. + +--- + +## P0 — ship these, they fix real user pain + +### 1. Cross-terminal flicker (#1119, #1352, #1356, #1363, #1366, #1260, #1295) + +**The most-reported bug since v0.8.26 shipped.** Five Ghostty / VSCode- +terminal reports in 24 hours plus the existing Windows ones. **Same +root cause, single fix.** + +**Diagnosis.** v0.8.22 added a viewport-reset escape sequence to fix +viewport drift after focus/resize. The sequence is: + +``` +\x1b[r set scroll region to entire screen +\x1b[?6l reset DECOM origin mode +\x1b[H cursor home +\x1b[2J erase entire screen +\x1b[3J erase saved lines +``` + +This fires on every redraw. `\x1b[2J\x1b[3J` is destructive — full +clear. Terminals that don't optimize differential redraws (Ghostty, +VSCode terminal in some configurations, Win10 conhost) blank-then- +repaint every frame, producing visible flicker. + +The smoking-gun datapoint is **#1356**: "doesn't flicker on M4 Air, +doesn't flicker for Claude Code / Codex / Gemini CLIs in the same +VSCode terminal." Other CLIs use the alt-screen buffer's natural +double-buffering and don't emit a destructive reset every frame. + +**Strategy — pick #1; #2 is the fallback.** + +#### 1.A — Replace destructive reset with lighter sequence (~30 min) + +In `crates/tui/src/tui/ui.rs` (search for the const that holds the +reset sequence — likely named `VIEWPORT_RESET` or similar, check the +v0.8.22 / v0.8.24 commits that mention `recover_terminal_modes` or +`FocusGained`): + +```rust +// before +const VIEWPORT_RESET: &str = "\x1b[r\x1b[?6l\x1b[H\x1b[2J\x1b[3J"; + +// after — drop the destructive 2J/3J; alt-screen buffer's existing +// double-buffering handles the redraw without screen blanking. +const VIEWPORT_RESET: &str = "\x1b[r\x1b[?6l\x1b[H"; +``` + +Add a regression test that asserts the constant doesn't contain +`2J` or `3J` (the destructive parts). The viewport-drift fix that +the original sequence was added to address came from #1041 / similar +— verify it's still working with the lighter sequence by manual +smoke on macOS Terminal.app. + +#### 1.B — Audit redraw-rate and only emit on actual drift (~1 day) + +If 1.A reintroduces drift, fall back to this: track previous viewport +state and only emit the reset when drift is detected (post-resize, +post-focus-gain, post-pager-close). Search for where the constant is +emitted; if it's in the per-frame draw path, that's the bug. + +#### 1.C — Per-terminal opt-out as belt-and-suspenders + +Detect `TERM_PROGRAM=ghostty`, `TERM_PROGRAM=vscode` (also covers +VSCode terminal), and known-flaky `TERM` values. Skip the reset +entirely on those. Document this as a fallback in the commit message; +prefer 1.A as the primary fix. + +**Action:** +1. File a tracking issue "Cross-terminal flicker survey" linking all + seven reports (#1119, #1352, #1356, #1363, #1366, #1260, #1295). +2. Apply fix 1.A. +3. Manual smoke on macOS Terminal.app + iTerm2 + Ghostty (Hunter has + Ghostty available; ask him). +4. Comment on each linked issue: "Fixed in v0.8.27 — please update + and reopen if you still see flicker." + +--- + +### 2. Long-text wrap (#1344, #1351, possibly #1359) + +**Diagnosis.** v0.8.25 fixed long markdown **table cells** (`wrap_cell_text` +helper in `markdown_render.rs`). Long **paragraphs** and long **input +lines** still clip at viewport width on some terminals, instead of +wrapping. #1344 reports both directions; #1351 reports the same +symptom plus a separate "table content shows `...`" issue. #1359 is +"VSCode terminal won't wrap" — possibly the same root cause if VSCode +reports terminal size differently. + +**Strategy.** + +1. Reproduce at narrow width: `COLUMNS=60 deepseek` → paste a 200- + character input line, ask for a 200-character paragraph response. + Confirm both clip rather than wrap. +2. Trace the wrap paths: + - `crates/tui/src/tui/markdown_render.rs::render_message` → + `render_line_with_links` → `wrap_text` (paragraphs) + - `crates/tui/src/tui/composer.rs` (or wherever the input box + renders) — likely has its own wrap logic that diverged +3. Unify on `wrap_text` from `markdown_render`. The composer should + use the same width-aware wrapper as the transcript. +4. Add snapshot tests for both surfaces at widths 40, 60, 80, 120. +5. For VSCode-terminal-specific size detection issues (#1359), verify + `crossterm::terminal::size()` returns the right value when run + inside VSCode terminal. If wrong, look at `--columns` override. + +**Cost:** 3-4 hours including tests. + +--- + +### 3. Pager copy-out (#1354) + +**Diagnosis.** When users hit `Alt+V` (tool details) or `Ctrl+O` +(thinking content), they get a pager view. The pager intercepts mouse +capture, so terminal-native selection is disabled inside it. There's +no in-app copy keybinding. Result: users can see the content but +can't copy it. High-frustration UX gap — pager users are usually +specifically there to copy something out. + +**Strategy.** Add a `c` (or `y`, vi-style) keybinding inside the +pager view that copies the entire visible content to clipboard, with +a status confirmation toast. + +In `crates/tui/src/tui/views/pager.rs` (or wherever `PagerView` is +defined — search for `impl ModalView for PagerView`): + +```rust +// inside handle_key, somewhere with the existing Esc/q/PgUp/PgDn handlers +KeyCode::Char('c') | KeyCode::Char('y') => { + let text = self.body_text(); // whatever method gives the full body + if app.clipboard.write_text(&text).is_ok() { + app.status_message = Some("Pager content copied".to_string()); + } else { + app.status_message = Some("Copy failed".to_string()); + } + return Vec::new(); +} +``` + +Also surface the keybinding in the pager footer: append `[c copy]` +to the existing affordance line. + +Add a regression test that constructs a pager, sends `c`, and +asserts the clipboard mock saw the body text. + +**Cost:** ~45 minutes. + +--- + +## P1 — should ship; clear shape, real impact + +### 4. Ctrl+C context-sensitive (#1337, #1367) + +**Diagnosis.** Two related issues: +- **#1337:** Windows users expect `Ctrl+C` to copy (legacy Windows + convention). Our binding is exit. They lose work copying. +- **#1367:** Users don't know how to interrupt a long-running task. + `Esc` works but isn't discoverable. + +**Strategy — context-sensitive Ctrl+C (resolves both):** + +Three branches based on app state: + +| State | Ctrl+C behavior | +|---|---| +| **Selection active** | Copy + clear selection. No exit. | +| **Turn in progress** | Interrupt the turn (same as Esc). No exit. | +| **Idle, no selection** | First press: status hint "Press Ctrl+C again to exit". Second press within 2s: exit. | + +This pattern is well-precedented (htop, less, tmux) and addresses +both issues in one change. Mirror Vim's "are you sure" pattern for +the idle case. + +In `crates/tui/src/tui/ui.rs::handle_key_event`, find the +`KeyCode::Char('c')` + `KeyModifiers::CONTROL` arm: + +```rust +KeyCode::Char('c') if m.contains(KeyModifiers::CONTROL) => { + // Branch 1: selection active → copy + if app.viewport.transcript_selection.is_active() { + copy_active_selection(app); + app.viewport.transcript_selection.clear(); + return Vec::new(); + } + // Branch 2: turn in progress → interrupt + if app.is_loading { + // existing interrupt logic — same code path as Esc + return interrupt_current_turn(app); + } + // Branch 3: idle → first press shows hint, second press within 2s exits + let now = Instant::now(); + let recent_ctrl_c = app.last_ctrl_c.is_some_and(|t| now.duration_since(t) < Duration::from_secs(2)); + if recent_ctrl_c { + return vec![ViewEvent::Exit]; + } + app.last_ctrl_c = Some(now); + app.status_message = Some("Press Ctrl+C again to exit".to_string()); + Vec::new() +} +``` + +Plus the discoverability hint for #1367: status bar during streaming +shows `[Esc cancel · Ctrl+C twice exit]`. + +**Cost:** ~2 hours including tests for each branch. + +--- + +### 5. `notify` tool (#1322) + +**Diagnosis.** Model-triggerable desktop notifications. Long agent +runs would benefit from an "I'm done, look at me" pop-up. Other +tools (Claude Code) have this. + +**Strategy.** Add a built-in `notify` tool spec. + +1. Add `notify-rust` to `crates/tui/Cargo.toml` (already cross- + platform: macOS Notification Center, Linux libnotify, Windows toast). +2. New tool in `crates/tui/src/tools/notify.rs`: + ```rust + pub struct NotifyTool; + + #[async_trait] + impl ToolSpec for NotifyTool { + fn name(&self) -> &'static str { "notify" } + fn description(&self) -> &'static str { + "Display a desktop notification to the user. Use sparingly — only when a long-running task completes or needs the user's attention." + } + fn input_schema(&self) -> Value { /* {title: required, body: optional} */ } + fn capabilities(&self) -> Vec { + vec![ToolCapability::RequiresApproval] + } + fn approval_requirement(&self) -> ApprovalRequirement { + ApprovalRequirement::Auto // notifications are low-risk + } + async fn execute(&self, input: Value, _ctx: &ToolContext) -> Result { + // truncate title to ~60 chars, body to ~200 + // skip if app is currently focused (don't notify about + // the thing the user is watching) — read from + // app.focus_state if available + // call notify_rust::Notification::new()... + } + } + ``` +3. Wire up in `tool_setup.rs` (probably register conditional on a + `Feature::DesktopNotifications` feature flag, default-on). +4. Add config opt-out: `[tools.notify] enabled = false`. + +Auto-suppress when terminal is focused — the user is watching, no +notification needed. + +**Cost:** ~3-4 hours. + +--- + +### 6. `/skills --remote` diagnostic (#1329) + +**Diagnosis.** "Failed to fetch" with no details. Could be TLS, +network policy, auth, rate limit. Bare error → undiagnosable. + +**Strategy.** First fix is observability — surface the underlying +error chain. + +In `crates/tui/src/commands/skills.rs` (or wherever `--remote` is +handled), find the `.unwrap_err()` or `.context(...)` that's +collapsing the chain: + +```rust +// before +return Err(anyhow!("Failed to fetch")); + +// after +return Err(err.context("Failed to fetch remote skills")); +// or, when surfacing to the user: +return CommandResult::error(format!("Failed to fetch remote skills:\n{err:#}")); +``` + +Mirror the v0.8.23 #1244 fix shape (alternate `{err:#}` formatting +for the full anyhow chain). + +Once the underlying error is visible, the actual bug becomes +diagnosable. Likely either a TLS issue (rustls vs system trust store) +or the network policy blocking the registry endpoint. + +**Cost:** ~30 minutes for the diagnostic improvement. + +--- + +### 7. MCP lazy reload on config change (#1267 part 2) + +**Diagnosis.** v0.8.26 fixed the diagnostic side (stderr capture). +The "auto-reload after config edit" piece is still missing — users +have to manually run `/mcp reload` after editing `~/.deepseek/config.toml`. + +**Strategy — lazy hash check (no file watcher).** File watchers add +long-lived tasks and have edge cases on remote / network filesystems. +A lazy hash compare is bounded and cheap. + +In `crates/tui/src/mcp.rs::McpPool`: + +```rust +pub struct McpPool { + // ... existing fields + config_hash: u64, // hash of mcp config at last (re)connection +} + +impl McpPool { + fn current_config_hash(&self, config: &McpConfig) -> u64 { + let mut hasher = std::hash::DefaultHasher::new(); + // hash the relevant fields: servers map, timeouts, sandbox_mode + config.hash(&mut hasher); + hasher.finish() + } + + pub async fn get_or_connect(&mut self, server: &str, config: &McpConfig) -> Result<&mut McpConnection> { + let new_hash = self.current_config_hash(config); + if new_hash != self.config_hash { + self.reload_all(config).await?; + self.config_hash = new_hash; + } + // existing get_or_connect logic + } +} +``` + +`McpConfig` and adjacent types may need `Hash` derived. If hashing +the whole config tree is expensive, hash just the `[mcp_servers]` +section + `sandbox_mode`. + +**Cost:** ~2 hours including tests. + +--- + +## P2 — nice-to-have if time permits + +### 8. Layout overlap (#1357) + +**Diagnosis.** Input box and inline runtime hint ("Cache: 99% hit | +hit X | miss Y") render in adjacent rects but one isn't clearing its +area properly when the other expands. + +**Strategy.** Inspect `crates/tui/src/tui/ui.rs::render` — find the +composer's reserved-rows calculation. It probably doesn't account for +the hint line on resize / long-content. Fix the rect math. + +**Cost:** ~2 hours (1 to repro, 1 to fix). + +### 9. `/skills` filter argument (#1318) + +**Diagnosis.** v0.8.26 added inter-row spacing (#1328 from @reidliu41). +Reporter may want more. + +**Strategy.** +1. Comment on #1318 asking if v0.8.26's spacing is enough. +2. If not, add `/skills ` arg → filter to skills whose names + start with ``. Mirror how `/help ` works. + +**Cost:** Triage ping; 30 min if filter wanted. + +### 10. Status comments on partial fixes (#1112, #1267, #1318) + +Three issues that are partly addressed and need the reporter to +confirm: + +- **#1112** — 1.2 TB snapshots. Cap added in v0.8.24. Comment: + "500 MB cap added in v0.8.24. Are you still seeing growth above + that? If so, please share `du -sh ~/.deepseek/snapshots`." +- **#1267** — macOS Seatbelt blocks npx MCP. Already commented during + v0.8.26 cycle. Don't re-comment. +- **#1318** — `/skills` crowded. Comment: "v0.8.26 added inter-row + spacing (#1328). Does this resolve it for you?" + +**Cost:** ~5 minutes total. + +--- + +## P3 — investigate or defer + +### #1338 — Enter mid-run crashes Windows TUI + +**Defer unless you have Windows.** Add stack capture so the next +reporter gets actionable output: + +```rust +// in main.rs — add panic hook that logs to ~/.deepseek/last-panic.log +std::panic::set_hook(Box::new(|info| { + let _ = std::fs::write( + dirs::home_dir().unwrap_or_default().join(".deepseek/last-panic.log"), + format!("{info}\n{}", std::backtrace::Backtrace::capture()), + ); +})); +``` + +**Cost:** 30 min for the diagnostic; actual fix needs Windows VM. + +### #1062 — Capacity-memory checkpoint cross-session recovery + +Old, complex. Don't pull into v0.8.27. Needs scope conversation with +Hunter. + +### #1067 — glibc version required (older Linux distros) + +Static-link the deepseek binary or add a musl build to release.yml. +**v0.8.27 candidate if anyone has time** — purely a build-config +change. + +### #1364 — Hooks mutation rights + turn-end event + +**Defer to v0.9.0.** Real ask — Claude Code hooks have this. Worth +doing as part of a hooks-v2 task. Out of scope for a polish release. + +### #1343 — Desktop GUI + +**Defer.** Recurring request. v0.9.x territory at the earliest. +Comment with roadmap status if not already. + +--- + +## Issues to close as fixed in v0.8.26 + +These need a comment + close. Already verified by the previous agent: + +| # | Title | Fixed by | +|---|---|---| +| #1163 | Mouse drag-select / copy doesn't auto-scroll | PR #1239 | +| #1169 | Selection crosses sidebar | Mouse-capture default-on for WT | +| #1255 | Win10 conversation can't scroll | Mouse-capture default-on | +| #1292 | Mac trackpad text selection broken | Drag-select rewrite | +| #1298 | Wheel scrolls input history not transcript | Mouse-capture default-on | +| #1308 | base_url for ollama/vllm ignored | Config-load warning | +| #1331 | Mouse wheel changed in v0.8.24 | Mouse-capture default-on | + +**Action:** Run through with this comment template (translated for +zh-CN issues #1255, #1292): + +``` +Fixed in [v0.8.26](https://github.com/Hmbown/DeepSeek-TUI/releases/tag/v0.8.26). +Please update with: + +- npm: `npm install -g deepseek-tui@latest` +- brew: `brew upgrade deepseek-tui` +- cargo: `cargo install --force deepseek-tui-cli` + +Reopen if you still hit it. Thanks for the report! +``` + +--- + +## Workflow + +### Step 1 — Branch state confirmation + +```bash +cd /Volumes/VIXinSSD/whalebro/deepseek-tui +git checkout work/v0.8.27 +git pull origin work/v0.8.27 || true +git log --oneline main..HEAD | head -20 +``` + +You should see ~16 commits already on the branch. Add to it; don't +restart. + +### Step 2 — Tackle in priority order (P0 → P3) + +For each item: + +1. Read the issue thread on GitHub. Note any reporter clarifications. +2. Implement per the strategy above. +3. Add tests (TDD where the strategy specifies; verification snapshot + otherwise). +4. After each commit: + ```bash + cargo fmt --all + cargo clippy -p deepseek-tui --all-targets --all-features --locked -- -D warnings + cargo test -p deepseek-tui --bin deepseek-tui --all-features --locked --no-fail-fast \ + 2>&1 | grep "test result:" | tail -3 + ``` +5. Add a CHANGELOG entry under `## [0.8.27]` `### Fixed` or `### Added`, + crediting the issue number and original reporter. + +The known-flaky test is +`mcp_connection_supports_streamable_http_event_stream_responses` — +passes in isolation, intermittent under load. Don't chase. + +### Step 3 — Issue triage pass + +After each P0/P1 fix lands, close the corresponding issue with a +comment template. Don't wait until the end of the cycle — closing as +you go keeps the issue list visibly responsive. + +### Step 4 — Bump version when ready + +```bash +sed -i '' 's|^version = "0.8.26"|version = "0.8.27"|' Cargo.toml +find crates -maxdepth 2 -name Cargo.toml -exec sed -i '' \ + 's|version = "0.8.26"|version = "0.8.27"|g' {} + +sed -i '' 's|"version": "0.8.26"|"version": "0.8.27"|' \ + npm/deepseek-tui/package.json +sed -i '' 's|"deepseekBinaryVersion": "0.8.26"|"deepseekBinaryVersion": "0.8.27"|' \ + npm/deepseek-tui/package.json +cargo update --workspace --offline +./scripts/release/check-versions.sh +``` + +Add `## [0.8.27] - YYYY-MM-DD` heading at the top of CHANGELOG.md. + +### Step 5 — Full preflight + install + +```bash +cargo fmt --all -- --check +cargo clippy --workspace --all-targets --all-features --locked -- -D warnings +cargo test --workspace --all-features --locked --no-fail-fast \ + 2>&1 | grep "test result:" | tail -10 +./scripts/release/check-versions.sh +./scripts/release/publish-crates.sh dry-run +cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui +node scripts/release/npm-wrapper-smoke.js + +cargo install --path crates/cli --force --locked +cargo install --path crates/tui --force --locked +deepseek --version # confirm: deepseek 0.8.27 () +``` + +### Step 6 — STOP-FOR-MAINTAINER + +Push the branch and open the release PR. Hand back to Hunter: + +- PR number + link +- Bullet list of all P0/P1/P2 items completed +- Items deferred (P3 items) with reason +- Preflight summary +- "deepseek 0.8.27 installed at ~/.cargo/bin/, ready for testing" +- Issues closed with v0.8.26 fixed-in comment + +WAIT for Hunter's "go" before merging, tagging, or publishing. + +### Step 7 — Release flow + +Same as v0.8.26 — see `.claude/HANDOFF_v0.8.26_security.md` steps 8–11. +Concretely: merge PR → auto-tag fires → release.yml builds matrix + +GitHub Release → crates.io publish → npm publish → Homebrew formula +update → verify GHCR → README post-merge bookkeeping. + +**No GHSA flow this cycle.** If a new advisory comes in, branch +v0.8.28 — don't bundle. + +### Step 8 — CNB mirror (new for v0.8.27) + +After GitHub Release is live: + +```bash +# If CNB_TOKEN is in repo secrets, the GitHub Action handles it +# automatically on tag push. Verify: +# https://cnb.cool/deepseek-tui.com/DeepSeek-TUI/-/tags + +# Otherwise (one-time bring-up was done manually) push from local: +git remote add cnb https://@cnb.cool/deepseek-tui.com/DeepSeek-TUI 2>/dev/null || true +git push cnb v0.8.27 main +``` + +Add a banner to README.md and README.zh-CN.md if not already there: + +``` +> 🇨🇳 国内镜像 / Mainland China mirror: +> https://cnb.cool/deepseek-tui.com/DeepSeek-TUI +> Issues and PRs: please use GitHub. +``` + +--- + +## Quality bar + +Apply to every change: + +- CI green (modulo documented flaky) +- No new `unwrap()` / `expect()` outside test code +- No new external network surfaces without `validate_network_policy` +- New env vars or config keys → `config.example.toml` entry + CHANGELOG note +- Behavior changes user-visible → CHANGELOG entry calling out the change + +When in doubt, defer to v0.8.28. A clean release of 8 P0/P1 items beats +a cluttered release of 15 with one regression. + +--- + +## Output expectation + +Realistic v0.8.27 landing zone on top of the existing 16 commits: + +- **All 7 closable v0.8.26 issues** closed with comments +- **P0 #1, #2, #3** fully shipped (flicker, wrap, pager copy) +- **P1 #4, #5, #6, #7** at least 2 of 4 shipped +- **P2 #8, #9, #10** at least the comment-pings +- **CNB mirror** wired in + +That's a substantial v0.8.27 that respects the "post-v0.8.26 inflow" +framing. Users see real responsiveness to their reports. + +If at any point something looks materially harder than this document +suggests, STOP and surface to Hunter with the specifics. Don't +freelance scope. diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b572b5e..01925982 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,96 @@ 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.27] - 2026-05-10 + +A substantial polish release bundling 17 community PRs and a small +internal fix. Big thanks to every contributor below. + +### Added + +- **Unified `/mode` command** (#1247) — `/mode [agent|plan|yolo|1|2|3]` + replaces the separate `/agent`, `/plan`, and `/yolo` commands. Running + `/mode` without arguments opens a picker modal. The legacy aliases + (`/yolo`, `/agent`, `/plan`) are kept as compatibility shorthands. + Thanks **@reidliu41**. +- **`/status` runtime diagnostics** (#1223) — shows version, provider, + model, workspace, mode, permissions, context-window usage, cache + hit/miss, and session cost. Previously `/status` was an alias for + `/statusline` (footer config); that alias is now `/statusline` only. + Thanks **@reidliu41**. +- **`/feedback` command** (#1185) — opens the matching GitHub issue + template (bug report, feature request) in the browser. Security + vulnerability reports route through the project's security policy + page first. Thanks **@reidliu41**. +- **Session artifact metadata** (#1220) — large tool outputs spilled to + the session artifacts directory are now tracked in a durable metadata + index, so saved sessions retain references across save/restore cycles. + Thanks **@THINKER-ONLY**. +- **Subagent results are self-reports** (#1140) — the compacted result + summary now notes that child-agent outputs are unverified self-reports. + The parent model should verify side effects with tools like `read_file` + or `list_dir` before claiming success. Thanks **@THINKER-ONLY**. +- **Global AGENTS.md fallback** (#1197) — when the workspace and its + parents don't provide project instructions, the TUI now loads + `~/.deepseek/AGENTS.md` before falling back to auto-generated + instructions. Repo-local context still takes priority. + Thanks **@manaskarra**. +- **`--yolo` forwarded from CLI to TUI** (#1233) — the `deepseek --yolo` + flag now propagates through the dispatcher to the TUI binary via + `DEEPSEEK_YOLO=true`. Previously the flag set `yolo` in the CLI + process but the TUI session started in its default mode. + Thanks **@fuleinist**. +- **`composer_arrows_scroll` config** (#1211) — a new + `tui.composer_arrows_scroll` option (default `false`) makes plain + Up/Down arrow keys scroll the transcript when the composer is empty, + instead of navigating input history. Helpful for terminals that map + trackpad gestures to arrow keys. Thanks **@lbcheng888**. +- **Session cost persistence** (#1192) — accumulated costs (session + + sub-agents, both USD and CNY) and the displayed-cost high-water mark + now survive session save/restore, so the monotonic cost guarantee + (#244) holds across restarts. Thanks **@lbcheng888**. +- **Provider-aware model picker and provider persistence** (#1320) — + switching providers now persists the choice to + `~/.deepseek/settings.toml` so it survives restarts. The model + picker hides DeepSeek-specific models when a non-DeepSeek provider + is active. `OPENAI_MODEL` env var now overrides the per-provider + model rather than the global `default_text_model`. Bailian / ZhiPu + Coding Plan endpoints are now supported. + Thanks **@imkingjh999**. +- **HTTP User-Agent header** (#1320) — all outbound API requests now + carry `deepseek-tui/{version}` in the User-Agent, matching the format + `fetch_url` already uses. Thanks **@imkingjh999**. + +### Fixed + +- **HTTP 400 quota errors retried** (#1203) — some OpenAI-compatible + gateways return quota/rate-limit errors as HTTP 400 instead of 429. + These are now classified as retryable `RateLimited` errors. + Thanks **@dst1213**. +- **Explicit hidden/ignored file completions** (#1270) — when the user + types an explicit path starting with `.` (e.g., `.deepseek/commands/`), + the file-completion system now surfaces hidden and gitignored entries + while still respecting `.deepseekignore`. Thanks **@SamhandsomeLee**. + +### Changed + +- **Windows mouse capture docs** (#1181) — the `--mouse-capture` help + text and the configuration docs now mention scrollbar dragging and + note that raw terminal selection on Windows may cross the sidebar. + Thanks **@Oliver-ZPLiu**. +- **README zh-CN sync** (#1235) — the Chinese README's quickstart section + now shows `deepseek run pr ` instead of the outdated + `deepseek pr `. Thanks **@whtis**. +- **Tool output render perf** (#1098) — tool output summaries and the + "is this a diff?" check are now pre-computed once at cell creation + instead of re-parsed every frame. Tool output cells also got a visual + card-rail (`╭ │ ╰`) for clearer grouping. Thanks **@lbcheng888**. + +### Internal + +- Test coverage for approval decision branches (@tuohai666, #1316) +- Test coverage for hook event dispatch paths (@tuohai666, #1317) + ## [0.8.26] - 2026-05-09 A security + polish release. Two responsibly-disclosed issues were diff --git a/Cargo.toml b/Cargo.toml index a993404c..2e26a6b6 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.26" +version = "0.8.27" 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/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index 4475f1d5..a1b88d9d 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.26" } +deepseek-config = { path = "../config", version = "0.8.27" } serde.workspace = true diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml index 1e342820..d19667a8 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.26" } -deepseek-config = { path = "../config", version = "0.8.26" } -deepseek-core = { path = "../core", version = "0.8.26" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } -deepseek-hooks = { path = "../hooks", version = "0.8.26" } -deepseek-mcp = { path = "../mcp", version = "0.8.26" } -deepseek-protocol = { path = "../protocol", version = "0.8.26" } -deepseek-state = { path = "../state", version = "0.8.26" } -deepseek-tools = { path = "../tools", version = "0.8.26" } +deepseek-agent = { path = "../agent", version = "0.8.27" } +deepseek-config = { path = "../config", version = "0.8.27" } +deepseek-core = { path = "../core", version = "0.8.27" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.27" } +deepseek-hooks = { path = "../hooks", version = "0.8.27" } +deepseek-mcp = { path = "../mcp", version = "0.8.27" } +deepseek-protocol = { path = "../protocol", version = "0.8.27" } +deepseek-state = { path = "../state", version = "0.8.27" } +deepseek-tools = { path = "../tools", version = "0.8.27" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 7d66b6d2..15b6c5be 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.26" } -deepseek-app-server = { path = "../app-server", version = "0.8.26" } -deepseek-config = { path = "../config", version = "0.8.26" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } -deepseek-mcp = { path = "../mcp", version = "0.8.26" } -deepseek-secrets = { path = "../secrets", version = "0.8.26" } -deepseek-state = { path = "../state", version = "0.8.26" } +deepseek-agent = { path = "../agent", version = "0.8.27" } +deepseek-app-server = { path = "../app-server", version = "0.8.27" } +deepseek-config = { path = "../config", version = "0.8.27" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.27" } +deepseek-mcp = { path = "../mcp", version = "0.8.27" } +deepseek-secrets = { path = "../secrets", version = "0.8.27" } +deepseek-state = { path = "../state", version = "0.8.27" } chrono.workspace = true dirs.workspace = true serde.workspace = true diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index c61e5139..c2a06885 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.26" } +deepseek-secrets = { path = "../secrets", version = "0.8.27" } dirs.workspace = true serde.workspace = true toml.workspace = true diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index 44ce2f83..4e9b19f2 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -962,9 +962,7 @@ impl ConfigToml { .clone() .or_else(|| env.sandbox_mode.clone()) .or_else(|| self.sandbox_mode.clone()); - let yolo = cli - .yolo - .or(env.yolo); + let yolo = cli.yolo.or(env.yolo); ResolvedRuntimeOptions { provider, diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index 5c1ac9bf..39b056ad 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.26" } -deepseek-config = { path = "../config", version = "0.8.26" } -deepseek-execpolicy = { path = "../execpolicy", version = "0.8.26" } -deepseek-hooks = { path = "../hooks", version = "0.8.26" } -deepseek-mcp = { path = "../mcp", version = "0.8.26" } -deepseek-protocol = { path = "../protocol", version = "0.8.26" } -deepseek-state = { path = "../state", version = "0.8.26" } -deepseek-tools = { path = "../tools", version = "0.8.26" } +deepseek-agent = { path = "../agent", version = "0.8.27" } +deepseek-config = { path = "../config", version = "0.8.27" } +deepseek-execpolicy = { path = "../execpolicy", version = "0.8.27" } +deepseek-hooks = { path = "../hooks", version = "0.8.27" } +deepseek-mcp = { path = "../mcp", version = "0.8.27" } +deepseek-protocol = { path = "../protocol", version = "0.8.27" } +deepseek-state = { path = "../state", version = "0.8.27" } +deepseek-tools = { path = "../tools", version = "0.8.27" } serde_json.workspace = true uuid.workspace = true diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml index 58aea3dd..9c1e55fb 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.26" } +deepseek-protocol = { path = "../protocol", version = "0.8.27" } serde.workspace = true diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index b84f4d17..eed01d21 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.26" } +deepseek-protocol = { path = "../protocol", version = "0.8.27" } reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 3de98881..b29bc4da 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.26" } +deepseek-protocol = { path = "../protocol", version = "0.8.27" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 0e9beb30..303883c7 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.26" } -deepseek-tools = { path = "../tools", version = "0.8.26" } +deepseek-secrets = { path = "../secrets", version = "0.8.27" } +deepseek-tools = { path = "../tools", version = "0.8.27" } 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/client.rs b/crates/tui/src/client.rs index 2422cb7e..bcae0826 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -514,7 +514,11 @@ impl DeepSeekClient { let headers = build_default_headers(api_key, extra_headers)?; let mut builder = reqwest::Client::builder() .default_headers(headers) - .user_agent(concat!("Mozilla/5.0 (compatible; deepseek-tui/", env!("CARGO_PKG_VERSION"), "; +https://github.com/Hmbown/DeepSeek-TUI)")) + .user_agent(concat!( + "Mozilla/5.0 (compatible; deepseek-tui/", + env!("CARGO_PKG_VERSION"), + "; +https://github.com/Hmbown/DeepSeek-TUI)" + )) .connect_timeout(Duration::from_secs(30)) .tcp_keepalive(Some(Duration::from_secs(30))) .http2_keep_alive_interval(Some(Duration::from_secs(15))) diff --git a/crates/tui/src/config.rs b/crates/tui/src/config.rs index 89bc7573..ee5603f2 100644 --- a/crates/tui/src/config.rs +++ b/crates/tui/src/config.rs @@ -2380,7 +2380,7 @@ fn normalize_model_for_provider(provider: ApiProvider, model: &str) -> Option bool { +pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool { matches!(provider, ApiProvider::Openai | ApiProvider::Ollama) } diff --git a/crates/tui/src/session_manager.rs b/crates/tui/src/session_manager.rs index 8243f109..4b9a8ca8 100644 --- a/crates/tui/src/session_manager.rs +++ b/crates/tui/src/session_manager.rs @@ -683,6 +683,7 @@ pub fn create_saved_session_with_id_and_mode( model: model.to_string(), workspace: workspace.to_path_buf(), mode: mode.map(str::to_string), + cost: SessionCostSnapshot::default(), }, messages: capped_messages, system_prompt: merge_truncation_note( diff --git a/crates/tui/src/tui/active_cell.rs b/crates/tui/src/tui/active_cell.rs index f16367b5..dc1eca0f 100644 --- a/crates/tui/src/tui/active_cell.rs +++ b/crates/tui/src/tui/active_cell.rs @@ -335,7 +335,7 @@ mod tests { duration_ms: None, source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, })) } @@ -356,8 +356,8 @@ mod tests { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })) } diff --git a/crates/tui/src/tui/history.rs b/crates/tui/src/tui/history.rs index 4a7599db..b1584371 100644 --- a/crates/tui/src/tui/history.rs +++ b/crates/tui/src/tui/history.rs @@ -2258,11 +2258,11 @@ fn wrap_card_rail(mut lines: Vec>) -> Vec> { } for (i, line) in lines.iter_mut().enumerate() { let rail = if i == 0 { - "\u{256D} " // ╭ + "\u{256D} " // ╭ } else if i == n - 1 { - "\u{2570} " // ╰ + "\u{2570} " // ╰ } else { - "\u{2502} " // │ + "\u{2502} " // │ }; line.spans.insert(0, Span::raw(rail)); } @@ -3154,8 +3154,8 @@ mod tests { spillover_path: Some(PathBuf::from( "/Users/dev/.deepseek/tool_outputs/call-abc12.txt", )), - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(120, true, super::RenderMode::Live); let joined: String = lines @@ -3184,8 +3184,8 @@ mod tests { output: Some("output".to_string()), prompts: None, spillover_path: Some(PathBuf::from("/tmp/spill.txt")), - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(120, true, super::RenderMode::Transcript); let joined: String = lines @@ -3208,8 +3208,8 @@ mod tests { output: Some("contents".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Live); let joined: String = lines @@ -3230,8 +3230,8 @@ mod tests { output: Some("output".to_string()), prompts: None, spillover_path: Some(PathBuf::from(long_path)), - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(40, true, super::RenderMode::Live); let annotation_line = lines @@ -3342,8 +3342,8 @@ mod tests { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Live); assert_eq!(lines.len(), 1); @@ -3364,8 +3364,8 @@ mod tests { ), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Transcript); // Transcript mode emits header + name kv + (no args, output present) @@ -3384,8 +3384,8 @@ mod tests { output: Some("first line\nsecond line\nthird line".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Live); assert!( @@ -3640,7 +3640,7 @@ mod tests { duration_ms: None, source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, })); let animated = cell.lines_with_options(80, TranscriptRenderOptions::default()); @@ -3857,7 +3857,7 @@ mod tests { duration_ms: Some(10), source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, }; let header = &cell.lines_with_motion(80, true)[0]; let visible: String = header @@ -3887,7 +3887,7 @@ mod tests { duration_ms: None, source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, }; let header = &cell.lines_with_motion(80, true)[0]; @@ -3912,8 +3912,8 @@ mod tests { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Live); let header_visible: String = lines[0] @@ -3941,8 +3941,8 @@ mod tests { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }; let lines = cell.lines_with_mode(80, true, super::RenderMode::Live); let header_visible: String = lines[0] @@ -4187,7 +4187,7 @@ mod tests { duration_ms: Some(42), source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, }; let lines = cell.lines_with_motion(80, true); @@ -4410,8 +4410,8 @@ mod tests { "Diff this commit against main".to_string(), ]), spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })); let text = lines_text(&cell.lines(80)); @@ -4437,8 +4437,8 @@ mod tests { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })); let text = lines_text(&cell.lines(80)); assert!(text.contains("query: foo")); @@ -4462,8 +4462,8 @@ mod tests { output: Some(diff_stat.to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })); let transcript_text = lines_text(&cell.transcript_lines(80)); @@ -4514,8 +4514,8 @@ mod tests { output: Some(output), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })); let live = cell.lines_with_options(80, TranscriptRenderOptions::default()); @@ -4595,8 +4595,10 @@ mod tests { lines_text(&cell.lines_with_options(80, TranscriptRenderOptions::default())); // Live mode: one-line summary + expand affordance. - assert!(live_text.contains("Alt+V for details"), - "live view must show expand affordance: {live_text}"); + assert!( + live_text.contains("Alt+V for details"), + "live view must show expand affordance: {live_text}" + ); // The pre-computed summary captures the first meaningful content. assert!( live_text.contains("Error:") || live_text.contains("fatal:"), diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index 16202c5f..c4305345 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -683,13 +683,18 @@ fn render_context_panel(f: &mut Frame, area: Rect, app: &App) { // Only show the additive breakdown when it matches the displayed // total; when the high-water mark is in effect (post-reconciliation), // the breakdown would not sum to the displayed value (#244). - let cost_line = if (displayed_total - real_total).abs() < COST_EQ_TOLERANCE { + let cost_line = if (total_cost - real_total).abs() < COST_EQ_TOLERANCE { format!( "cost: {} (session {} + agents {})", app.format_cost_amount(total_cost), app.format_cost_amount(session_cost), app.format_cost_amount(agent_cost) - ), + ) + } else { + format!("cost: {}", app.format_cost_amount(total_cost),) + }; + lines.push(Line::from(Span::styled( + cost_line, Style::default().fg(palette::TEXT_MUTED), ))); diff --git a/crates/tui/src/tui/tool_routing.rs b/crates/tui/src/tui/tool_routing.rs index 838b430b..b6e6dc50 100644 --- a/crates/tui/src/tui/tool_routing.rs +++ b/crates/tui/src/tui/tool_routing.rs @@ -624,15 +624,12 @@ pub(super) fn handle_tool_call_complete( match result.as_ref() { Ok(tool_result) => { generic.output = Some(tool_result.content.clone()); - generic.output_summary = - Some(summarize_tool_output(&tool_result.content)); - generic.is_diff = - output_looks_like_diff(&tool_result.content); + generic.output_summary = Some(summarize_tool_output(&tool_result.content)); + generic.is_diff = output_looks_like_diff(&tool_result.content); } Err(err) => { generic.output = Some(err.to_string()); - generic.output_summary = - Some(summarize_tool_output(&err.to_string())); + generic.output_summary = Some(summarize_tool_output(&err.to_string())); generic.is_diff = false; } } @@ -715,12 +712,8 @@ fn push_orphan_tool_completion( .and_then(|m| m.get("spillover_path")) .and_then(serde_json::Value::as_str) .map(std::path::PathBuf::from); - let output_summary = output - .as_deref() - .map(|o| summarize_tool_output(o)); - let is_diff = output - .as_deref() - .is_some_and(|o| output_looks_like_diff(o)); + let output_summary = output.as_deref().map(|o| summarize_tool_output(o)); + let is_diff = output.as_deref().is_some_and(|o| output_looks_like_diff(o)); app.add_message(HistoryCell::Tool(ToolCell::Generic(GenericToolCell { name: name.to_string(), status, diff --git a/crates/tui/src/tui/transcript.rs b/crates/tui/src/tui/transcript.rs index 0a0a1794..bf1f6198 100644 --- a/crates/tui/src/tui/transcript.rs +++ b/crates/tui/src/tui/transcript.rs @@ -564,7 +564,7 @@ mod tests { duration_ms: None, source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, })) } diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 574a6e0c..c9481883 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6279,10 +6279,16 @@ fn apply_loaded_session(app: &mut App, session: &SavedSession) -> bool { // the restored total with no regression. let total_restored_usd = session.metadata.cost.total_usd(); let total_restored_cny = session.metadata.cost.total_cny(); - app.session.displayed_cost_high_water = - session.metadata.cost.displayed_cost_high_water_usd.max(total_restored_usd); - app.session.displayed_cost_high_water_cny = - session.metadata.cost.displayed_cost_high_water_cny.max(total_restored_cny); + app.session.displayed_cost_high_water = session + .metadata + .cost + .displayed_cost_high_water_usd + .max(total_restored_usd); + app.session.displayed_cost_high_water_cny = session + .metadata + .cost + .displayed_cost_high_water_cny + .max(total_restored_cny); app.session.last_prompt_tokens = None; app.session.last_completion_tokens = None; app.session.last_prompt_cache_hit_tokens = None; diff --git a/crates/tui/src/tui/ui/tests.rs b/crates/tui/src/tui/ui/tests.rs index 4765158d..2bf4f048 100644 --- a/crates/tui/src/tui/ui/tests.rs +++ b/crates/tui/src/tui/ui/tests.rs @@ -246,8 +246,8 @@ fn selection_to_text_copies_rendered_transcript_block() { output: Some("tool output line".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), HistoryCell::Assistant { content: "copy assistant".to_string(), @@ -1197,7 +1197,7 @@ fn active_tool_status_label_summarizes_live_tool_group() { duration_ms: None, source: ExecSource::Assistant, interaction: None, - output_summary: None, + output_summary: None, })), ); active.push_tool( @@ -1209,8 +1209,8 @@ fn active_tool_status_label_summarizes_live_tool_group() { output: Some("done".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ); app.active_cell = Some(active); @@ -1237,8 +1237,8 @@ fn active_tool_status_label_counts_foreground_rlm_work() { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ); app.active_cell = Some(active); @@ -2502,8 +2502,8 @@ fn jump_to_adjacent_tool_cell_finds_next_and_previous() { output: Some("done".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), HistoryCell::Assistant { content: "ok".to_string(), @@ -2516,8 +2516,8 @@ fn jump_to_adjacent_tool_cell_finds_next_and_previous() { output: Some("...".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ]; app.mark_history_updated(); @@ -2574,8 +2574,8 @@ fn detail_target_prefers_visible_tool_card() { output: Some("done".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), HistoryCell::Assistant { content: "ok".to_string(), @@ -2588,8 +2588,8 @@ fn detail_target_prefers_visible_tool_card() { output: Some("...".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ]; app.tool_details_by_cell.insert( @@ -2681,8 +2681,8 @@ fn spillover_pager_section_returns_none_when_no_spillover() { output: Some("hi".to_string()), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }))]; app.resync_history_revisions(); assert!(spillover_pager_section(&app, 0).is_none()); @@ -2704,8 +2704,8 @@ fn spillover_pager_section_loads_file_when_present() { output: Some("(truncated head)".to_string()), prompts: None, spillover_path: Some(path.clone()), - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }))]; app.resync_history_revisions(); @@ -2729,8 +2729,8 @@ fn spillover_pager_section_returns_notice_when_file_missing() { output: Some("(truncated head)".to_string()), prompts: None, spillover_path: Some(bogus), - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }))]; app.resync_history_revisions(); @@ -2754,7 +2754,7 @@ fn terminal_pause_has_live_owner_only_for_running_exec_cells() { duration_ms: None, source: ExecSource::Assistant, interaction: Some("interactive".to_string()), - output_summary: None, + output_summary: None, })), ); app.active_cell = Some(active); @@ -2770,8 +2770,8 @@ fn terminal_pause_has_live_owner_only_for_running_exec_cells() { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ); app.active_cell = Some(active); @@ -2795,8 +2795,8 @@ fn active_rlm_task_entries_surface_foreground_rlm_work() { output: None, prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })), ); app.active_cell = Some(active); diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index afd7599d..ea4858b5 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -2687,8 +2687,8 @@ mod tests { output: Some("hello world ".repeat(420)), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })); for width in [40u16, 80, 111, 165] { let lines = cell.lines(width); @@ -2742,8 +2742,8 @@ mod tests { output: Some(output), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, }))); let height: u16 = 30; @@ -3122,8 +3122,8 @@ mod tests { output: Some(format!("found 12 matches in cell-{i}")), prompts: None, spillover_path: None, - output_summary: None, - is_diff: false, + output_summary: None, + is_diff: false, })) } else if i % 2 == 0 { HistoryCell::User { diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json index 8dd1d35c..5f42b535 100644 --- a/npm/deepseek-tui/package.json +++ b/npm/deepseek-tui/package.json @@ -1,7 +1,7 @@ { "name": "deepseek-tui", - "version": "0.8.26", - "deepseekBinaryVersion": "0.8.26", + "version": "0.8.27", + "deepseekBinaryVersion": "0.8.27", "description": "Install and run deepseek and deepseek-tui binaries from GitHub release artifacts.", "author": "Hmbown", "license": "MIT", diff --git a/web/lib/community-agent-tasks.ts b/web/lib/community-agent-tasks.ts index 79aaa939..19ccc13f 100644 --- a/web/lib/community-agent-tasks.ts +++ b/web/lib/community-agent-tasks.ts @@ -55,6 +55,7 @@ export async function runTriage(env: AgentEnv): Promise> headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "deepseek-tui-web", ...(env.GITHUB_TOKEN ? { Authorization: `Bearer ${env.GITHUB_TOKEN}` } : {}), }, } @@ -119,6 +120,7 @@ export async function runPrReview(env: AgentEnv): Promise> headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "deepseek-tui-web", ...(env.GITHUB_TOKEN ? { Authorization: `Bearer ${env.GITHUB_TOKEN}` } : {}), }, } @@ -264,6 +268,7 @@ export async function runDupes(env: AgentEnv): Promise> headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "deepseek-tui-web", ...(env.GITHUB_TOKEN ? { Authorization: `Bearer ${env.GITHUB_TOKEN}` } : {}), }, } @@ -325,6 +330,7 @@ export async function runDigest(env: AgentEnv): Promise> headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "deepseek-tui-web", ...(env.GITHUB_TOKEN ? { Authorization: `Bearer ${env.GITHUB_TOKEN}` } : {}), }, } @@ -335,6 +341,7 @@ export async function runDigest(env: AgentEnv): Promise> headers: { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", + "User-Agent": "deepseek-tui-web", ...(env.GITHUB_TOKEN ? { Authorization: `Bearer ${env.GITHUB_TOKEN}` } : {}), }, } diff --git a/web/lib/community-agent.ts b/web/lib/community-agent.ts index d7f0be07..481c2954 100644 --- a/web/lib/community-agent.ts +++ b/web/lib/community-agent.ts @@ -9,10 +9,9 @@ * - Cites specific files / line numbers / linked issues when discussing code. * - Always ends with the draft disclaimer. */ -const BASE = process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com"; -const MODEL = process.env.DEEPSEEK_MODEL ?? "deepseek-v4-flash"; - const MAX_OUTPUT_TOKENS = 2_000; +const FALLBACK_BASE = "https://api.deepseek.com"; +const FALLBACK_MODEL = "deepseek-v4-flash"; interface ChatMessage { role: "system" | "user" | "assistant"; @@ -47,17 +46,20 @@ export async function agentChat( apiKey: string, jsonMode = false ): Promise<{ content: string; usage: { input: number; output: number } }> { - const res = await fetch(`${BASE}/v1/chat/completions`, { + const base = process.env.DEEPSEEK_BASE_URL ?? FALLBACK_BASE; + const model = process.env.DEEPSEEK_MODEL ?? FALLBACK_MODEL; + const res = await fetch(`${base}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ - model: MODEL, + model, messages, temperature: 0.3, max_tokens: MAX_OUTPUT_TOKENS, + reasoning_effort: "high", ...(jsonMode ? { response_format: { type: "json_object" } } : {}), }), }); diff --git a/web/lib/deepseek.ts b/web/lib/deepseek.ts index 876fbab2..bfa4f233 100644 --- a/web/lib/deepseek.ts +++ b/web/lib/deepseek.ts @@ -1,7 +1,7 @@ import type { CuratedDispatch, FeedItem, RepoStats } from "./types"; -const BASE = process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com"; -const MODEL = process.env.DEEPSEEK_MODEL ?? "deepseek-v4-flash"; +const FALLBACK_BASE = "https://api.deepseek.com"; +const FALLBACK_MODEL = "deepseek-v4-flash"; interface ChatMessage { role: "system" | "user" | "assistant"; @@ -13,16 +13,20 @@ interface ChatResponse { } export async function chat(messages: ChatMessage[], apiKey: string, jsonMode = false): Promise { - const res = await fetch(`${BASE}/v1/chat/completions`, { + const base = process.env.DEEPSEEK_BASE_URL ?? FALLBACK_BASE; + const model = process.env.DEEPSEEK_MODEL ?? FALLBACK_MODEL; + const res = await fetch(`${base}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, body: JSON.stringify({ - model: MODEL, + model, messages, temperature: 0.4, + max_tokens: 4096, + reasoning_effort: "high", ...(jsonMode ? { response_format: { type: "json_object" } } : {}), }), }); diff --git a/web/lib/facts.generated.ts b/web/lib/facts.generated.ts index ec92ffe0..b14a58f1 100644 --- a/web/lib/facts.generated.ts +++ b/web/lib/facts.generated.ts @@ -18,8 +18,8 @@ export interface RepoFacts { } export const FACTS: RepoFacts = { - "generatedAt": "2026-05-08T19:10:07.269Z", - "version": "0.8.22", + "generatedAt": "2026-05-10T13:27:03.912Z", + "version": "0.8.26", "crates": [ "agent", "app-server", diff --git a/web/wrangler.jsonc b/web/wrangler.jsonc index a43077cc..9f30d6d8 100644 --- a/web/wrangler.jsonc +++ b/web/wrangler.jsonc @@ -21,7 +21,8 @@ ], "vars": { "GITHUB_REPO": "Hmbown/deepseek-tui", - "DEEPSEEK_MODEL": "deepseek-v4-flash" + "DEEPSEEK_MODEL": "deepseek-v4-flash", + "DEEPSEEK_BASE_URL": "https://gateway.ai.cloudflare.com/v1/cf50f793171d7cb3b2ce23368b69cdcb/deepseek-tui-web/deepseek" }, "triggers": { "crons": [