chore(release): bump to v0.8.27, add CHANGELOG

This commit is contained in:
Hunter Bown
2026-05-10 08:41:04 -05:00
parent cb084d1564
commit 6c25a18b42
30 changed files with 895 additions and 140 deletions
+642
View File
@@ -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 2448 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 711 (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<ToolCapability> {
vec![ToolCapability::RequiresApproval]
}
fn approval_requirement(&self) -> ApprovalRequirement {
ApprovalRequirement::Auto // notifications are low-risk
}
async fn execute(&self, input: Value, _ctx: &ToolContext) -> Result<ToolResult, ToolError> {
// 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 <prefix>` arg → filter to skills whose names
start with `<prefix>`. Mirror how `/help <topic>` 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 (<sha>)
```
### 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 811.
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://<token>@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.
+90
View File
@@ -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 <N>` instead of the outdated
`deepseek pr <N>`. 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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -7,5 +7,5 @@ repository.workspace = true
description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture"
[dependencies]
deepseek-config = { path = "../config", version = "0.8.26" }
deepseek-config = { path = "../config", version = "0.8.27" }
serde.workspace = true
+9 -9
View File
@@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect
anyhow.workspace = true
axum.workspace = true
clap.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.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
+7 -7
View File
@@ -14,13 +14,13 @@ path = "src/main.rs"
anyhow.workspace = true
clap.workspace = true
clap_complete.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.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
+1 -1
View File
@@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite
[dependencies]
anyhow.workspace = true
deepseek-secrets = { path = "../secrets", version = "0.8.26" }
deepseek-secrets = { path = "../secrets", version = "0.8.27" }
dirs.workspace = true
serde.workspace = true
toml.workspace = true
+1 -3
View File
@@ -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,
+8 -8
View File
@@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture"
[dependencies]
anyhow.workspace = true
chrono.workspace = true
deepseek-agent = { path = "../agent", version = "0.8.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
+1 -1
View File
@@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace
[dependencies]
anyhow.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.26" }
deepseek-protocol = { path = "../protocol", version = "0.8.27" }
serde.workspace = true
+1 -1
View File
@@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc
anyhow.workspace = true
async-trait.workspace = true
chrono.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.26" }
deepseek-protocol = { path = "../protocol", version = "0.8.27" }
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
+1 -1
View File
@@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral
[dependencies]
anyhow.workspace = true
async-trait.workspace = true
deepseek-protocol = { path = "../protocol", version = "0.8.26" }
deepseek-protocol = { path = "../protocol", version = "0.8.27" }
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
+2 -2
View File
@@ -21,8 +21,8 @@ path = "src/main.rs"
[dependencies]
anyhow = "1.0.100"
arboard = "3.4"
deepseek-secrets = { path = "../secrets", version = "0.8.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"
+5 -1
View File
@@ -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)))
+1 -1
View File
@@ -2380,7 +2380,7 @@ fn normalize_model_for_provider(provider: ApiProvider, model: &str) -> Option<St
normalize_model_name(model).map(|normalized| model_for_provider(provider, normalized))
}
fn provider_passes_model_through(provider: ApiProvider) -> bool {
pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
matches!(provider, ApiProvider::Openai | ApiProvider::Ollama)
}
+1
View File
@@ -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(
+3 -3
View File
@@ -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,
}))
}
+37 -35
View File
@@ -2258,11 +2258,11 @@ fn wrap_card_rail(mut lines: Vec<Line<'static>>) -> Vec<Line<'static>> {
}
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:"),
+7 -2
View File
@@ -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),
)));
+5 -12
View File
@@ -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,
+1 -1
View File
@@ -564,7 +564,7 @@ mod tests {
duration_ms: None,
source: ExecSource::Assistant,
interaction: None,
output_summary: None,
output_summary: None,
}))
}
+10 -4
View File
@@ -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;
+26 -26
View File
@@ -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);
+6 -6
View File
@@ -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 {
+2 -2
View File
@@ -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",
+7
View File
@@ -55,6 +55,7 @@ export async function runTriage(env: AgentEnv): Promise<Record<string, unknown>>
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<Record<string, unknown
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}` } : {}),
},
}
@@ -142,6 +144,7 @@ export async function runPrReview(env: AgentEnv): Promise<Record<string, unknown
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}` } : {}),
},
});
@@ -200,6 +203,7 @@ export async function runStale(env: AgentEnv): Promise<Record<string, unknown>>
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<Record<string, unknown>>
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<Record<string, unknown>>
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<Record<string, unknown>>
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}` } : {}),
},
}
+7 -5
View File
@@ -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" } } : {}),
}),
});
+8 -4
View File
@@ -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<string> {
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" } } : {}),
}),
});
+2 -2
View File
@@ -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",
+2 -1
View File
@@ -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": [