Commit Graph

666 Commits

Author SHA1 Message Date
Hunter Bown 02fc16e10f style: clippy sweep across community PRs (-D warnings)
13 clippy errors had accumulated from squash-merged community PRs:
collapsible-if (10), needless-late-init (1), derivable-impls (1),
sort-unstable hint (1). All auto-fixable mechanical lints — no
behaviour change. Required to satisfy CI's
`cargo clippy --workspace --all-targets --all-features --locked
-- -D warnings` gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:15:16 -05:00
Hunter Bown 6d0c708307 Merge branch 'main' into feat/v0.8.12 2026-05-05 02:08:25 -05:00
Hunter Bown f779c7de6e fix(security): refuse to auto-recover checkpoint from another workspace
Reported by @Hmbown: launching `deepseek` from any directory silently
auto-recovered the most recent interrupted session, even if that
session originated in a completely different workspace. Tools then
operated on file paths from the prior workspace while the status bar
showed the *current* workspace name — a confusing trust-boundary
violation that also leaks api_messages, working_set entries, and any
secrets the prior session had accumulated into a new terminal that
was never meant to see them.

`try_recover_checkpoint()` now compares the saved session's workspace
to `std::env::current_dir()` (canonicalised, with a strict-equality
fallback when canonicalisation fails — e.g. the original workspace
was deleted) and only auto-recovers on a match.

On a mismatch the checkpoint is still persisted as a regular session
and cleared, so the user can recover it explicitly via
`deepseek resume <ID>` after `cd`-ing back to the original workspace —
no data is lost. A one-line stderr notice points at the resume command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:08:02 -05:00
Hunter Bown a6d9d7cf5b fix(security): refuse to auto-recover checkpoint from another workspace
Reported by @Hmbown: launching `deepseek` from any directory silently
auto-recovered the most recent interrupted session, even if that
session originated in a completely different workspace. Tools then
operated on file paths from the prior workspace while the status bar
showed the *current* workspace name — a confusing trust-boundary
violation that also leaks api_messages, working_set entries, and any
secrets the prior session had accumulated into a new terminal that
was never meant to see them.

`try_recover_checkpoint()` now compares the saved session's workspace
to `std::env::current_dir()` (canonicalised, with a strict-equality
fallback when canonicalisation fails — e.g. the original workspace
was deleted) and only auto-recovers on a match.

On a mismatch the checkpoint is still persisted as a regular session
and cleared, so the user can recover it explicitly via
`deepseek resume <ID>` after `cd`-ing back to the original workspace —
no data is lost. A one-line stderr notice points at the resume command.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:07:58 -05:00
Hunter Bown 546ef939bd docs+ci(v0.8.12): Resume by UUID, triage workflows, CHANGELOG refresh
- README Usage block now documents `deepseek resume <SESSION_ID>` and
  `deepseek fork <SESSION_ID>`. Both commands have existed since v0.7
  but were undiscoverable; #682 reported "no way to resume."
- New GitHub Actions for issue triage (#688):
    * triage.yml      — keyword-driven auto-labeller (bug / feat / docs /
                        question, area:* by file-path mention, os:*,
                        lang:zh on CJK titles). Only adds labels that
                        already exist on the repo so it can't create
                        noise unilaterally.
    * stale.yml       — 14 d stale → 7 d close on `needs-info` issues
                        only; PRs untouched; respects pinned/keep-open/
                        bug/security exemptions.
    * spam-lockdown.yml — auto-closes promotional issues from accounts
                          <30 days old. Pure github-script (no
                          third-party action) so the matching rules
                          stay readable.
- CHANGELOG (v0.8.12) updated: README install rewrite (#672), Scoop
  (#696), pricing extension (#692), Resume docs surface, and the
  cargo-install-on-stable fix from the previous commit. Lease
  "pending" caveat removed since it's now actually fixed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:03:16 -05:00
Hunter Bown 3fedb980c7 fix(subagents): replace 'pending' lease placeholder with real agent id (#660)
Resident-file leases were stamped as "pending" at spawn time because the
agent id is only assigned by the manager later. The release function
introduced in 2ee926924 matches by agent id, so it could never find
those entries and leases would persist for the lifetime of the process.

After spawn returns the real agent id, replace any "pending" entry with
it so the existing release-on-terminal-state path actually fires.

Resolves the documented v0.8.12 caveat noted in the CHANGELOG. Closes
the loop with PR #694, which proposed a release-by-file-path API but
did not address the placeholder problem itself.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 02:00:43 -05:00
woyxiang 48d529fa93 docs: add Scoop installation instructions for Windows 2026-05-05 01:59:18 -05:00
wangfeng 327a9af1be docs: update pricing/discount info — promotion period extended (closes #691) 2026-05-05 01:59:07 -05:00
Hunter Bown a9dcf2b6e6 style: cargo fmt sweep across community PRs
47 fmt drifts had accumulated from the squash-merged community PRs on
this branch (#653, #654, #655, #645, #658, #668, #659, #661, #660,
#667, #656). Pure formatting — no behavioural changes — applied via
`cargo fmt --all` to satisfy CI's `cargo fmt --all -- --check` gate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:57:25 -05:00
Hunter Bown 668f2c37f5 fix(build): stable-Rust compatibility + silence deferred-code warnings
The match guard at tui/ui.rs:1603 used `&& let Some(...) = ...` inside an
`if` guard, which requires the `if_let_guard` nightly feature on Rust
< 1.94. Reported by an external user attempting `cargo install
deepseek-tui` on stable rustc — it failed with E0658.

Rewrite as a plain match guard with a nested `if let` inside the arm
body so the language-picker hotkeys compile on every supported rustc.

Workspace also now declares `rust-version = "1.88"` to match the
codebase's actual reliance on `let_chains` in if/while conditions, so
users on too-old toolchains see a clear cargo error instead of a
confusing rustc one.

`AGENTS.md` and `CLAUDE.md` gain a "stable Rust only" section
documenting the trap and how to rewrite around it.

Also annotate the deferred `TuiPrefs` (#657) and `handoff::THRESHOLDS`
(#667) APIs with `#[allow(dead_code)]` so CI's `-D warnings` flag stays
green while the call sites are staged for v0.8.13.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:57:14 -05:00
Hunter Bown da266e2105 Merge branch 'main' into feat/v0.8.12 2026-05-05 01:50:50 -05:00
Hunter Bown 3d289e5fdd docs: clarify Node.js is not a runtime dependency (closes #672)
Replace the prerequisite block that told readers to install Node before
anything else (which contradicted the lede claim that the binary needs
no Node runtime) with a multi-path install section. npm, cargo, and
direct download are presented side-by-side; the npm path is documented
as a thin installer that downloads the prebuilt binary, not as a
runtime dependency.

Same change applied to README.zh-CN.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 01:50:36 -05:00
Hunter Bown 2f499858dd docs(release): add v0.8.12 CHANGELOG + update README What's New 2026-05-05 01:02:27 -05:00
Hunter Bown 239e5925a1 fix(test): use fully-qualified ratatui::style::Color::Reset
The test module couldn't resolve Color via the file-level import in
Rust 2024 edition cfg(test) context. Use the full path instead.
2026-05-05 00:58:07 -05:00
Hunter Bown 2ee826924a fix(subagents): release resident file leases on agent completion (#660)
Moved RESIDENT_LEASES from block-scoped static to module level so the
release function can access it. Added release_resident_leases_for()
called at all three terminal transitions:
- Cancelled (cancel_agent)
- Failed (update_failed)
- Completed (run_subagent_turn result construction)

Previously leases were acquired but never released, causing false
conflict warnings on subsequent spawns targeting the same file.
2026-05-05 00:54:34 -05:00
Hunter Bown b51fa6bc91 feat(sandbox): wire create_backend() into Engine::build_tool_context (#645)
The sandbox backend infrastructure was complete but the engine never
called create_backend(), leaving the feature dead. Now:
- Engine::new() creates the backend from api_config (non-fatal on error)
- build_tool_context() attaches it to the ToolContext
- exec_shell checks context.sandbox_backend and routes accordingly
2026-05-05 00:51:24 -05:00
Hunter Bown c0e27485a8 fix: remove dead config fields — prefer_handoff (#667) and use_terminal_colors (#671)
Neither field had any code path that read it. Shipping config knobs
that do nothing trains users to mistrust config. Remove until the
implementation exists.
2026-05-05 00:44:13 -05:00
Hunter Bown ca9fccc0da fix: address review findings — broken test, expect() panic, misleading docstring
#651: fix test assertion — section_bg now Color::Reset (was DEEPSEEK_INK)
#645: replace expect() with Result in OpenSandboxBackend::new()
#653: correct resolve_prefixes docstring to describe deny-always-wins
2026-05-05 00:42:42 -05:00
Hunter Bown 8785e6865e chore(release): bump version to 0.8.12 2026-05-05 00:18:14 -05:00
Hunter Bown e25cb4e38b fix(build): add missing arity_dict field in ExecPolicyEngine::with_rulesets 2026-05-05 00:17:24 -05:00
Hunter Bown ff5c99965d feat(sandbox): pluggable SandboxBackend + Alibaba OpenSandbox adapter (#645) 2026-05-05 00:16:34 -05:00
Hunter Bown 8dca6deee2 feat(execpolicy): layered permission rulesets (#653) 2026-05-05 00:16:27 -05:00
Hunter Bown 685f9f97a3 feat(subagents): cache-aware resident file sub-agents for refactors (#660) 2026-05-05 00:15:27 -05:00
Hunter Bown 2cdf4a2560 feat(engine): context-limit handoff replaces routine compaction (#667) 2026-05-05 00:15:15 -05:00
Hunter Bown 937f5f33f3 feat(runtime): route large tool outputs through workshop to protect parent context (#658) 2026-05-05 00:14:16 -05:00
Hunter Bown b31bc4104b feat(config): separate tui.toml for theme and keybinds (#657) 2026-05-05 00:12:29 -05:00
Hunter Bown 1e968a3ec4 feat(tui): vim modal editing in composer (#659) 2026-05-05 00:12:28 -05:00
Hunter Bown caf1ac2a89 feat(skills): remote registry sync with /skills sync command (#654) 2026-05-05 00:12:17 -05:00
Hunter Bown 2928d20e09 feat(tools): FIM edit tool for V4 /beta endpoint (#668) 2026-05-05 00:12:17 -05:00
Hunter Bown 84ff982086 feat(lsp): auto-attach diagnostics to edit results (#656) 2026-05-05 00:12:09 -05:00
Hunter Bown fb2e61e123 feat(execpolicy): bash arity dictionary for command-prefix allow rules (#655) 2026-05-05 00:12:09 -05:00
Hunter Bown 95e92ef1fc feat(commands): unified slash-command namespace with template substitution (#661) 2026-05-05 00:12:08 -05:00
Hunter Bown f8f8160532 feat(engine): reasoning_effort auto mode (#669) 2026-05-05 00:11:56 -05:00
Hunter Bown c5c90da985 fix(notifications): actually call MessageBeep on Windows (#646) 2026-05-05 00:11:14 -05:00
Hunter Bown 7feadba939 fix(fork): optimize truncate_id to avoid unnecessary allocation (#649) 2026-05-05 00:11:08 -05:00
Hunter Bown 0370e45a97 fix(tui): replace hardcoded colors with Color::Reset + add use_terminal_colors config (#671) 2026-05-05 00:11:02 -05:00
Hunter Bown b750cef1b5 fix(theme): complete Color::Reset migration across all UI widgets (#651) 2026-05-05 00:10:24 -05:00
Hunter Bown 848725e65d docs: document zh-Hans locale activation (#652) 2026-05-05 00:08:39 -05:00
Hunter Bown faeda74d66 docs: add CODE_OF_CONDUCT.md (#686) 2026-05-04 23:58:49 -05:00
Hunter Bown cb2699de08 docs: add SECURITY.md with vulnerability reporting policy (#648) 2026-05-04 23:58:24 -05:00
Hunter Bown abc8751d81 docs(release): add v0.8.11 fix entries to CHANGELOG + update README What's New
- CHANGELOG: add YOLO sandbox, scroll lock, and capacity controller fixes
- README: replace v0.8.10 highlights with v0.8.11 cache-maxing + fixes
- Date corrected to 2026-05-04
2026-05-04 23:50:15 -05:00
Hunter Bown 4c783be52d fix(yolo): drop sandbox to DangerFullAccess — no guardrails as advertised
User report: YOLO mode was still routing shell commands through the
WorkspaceWrite sandbox, which intercepted legitimate outside-workspace
writes (package installs, sub-agent workspaces, package-manager state
under ~/.cache, brew, npm install -g, pipx, …) and forced approval
round-trips. That contradicts the YOLO contract — the user opted into
"no guardrails" and instead got a guardrail.

YOLO already auto-approves all tools and enables trust mode. The
sandbox was the last residual restriction. Drop it.

Change in `Engine::build_tool_context`: split the previously-merged
`AppMode::Agent | AppMode::Yolo` arm into two:

* **Agent** keeps `WorkspaceWrite { writable_roots, network_access:
  true, … }` — interactive mode with explicit per-tool approval, so
  the sandbox plus the approval flow form a defense-in-depth layer.
* **Yolo** uses `DangerFullAccess` — no sandbox. The user has
  opted into auto-approval + trust mode + no sandbox as one
  consistent posture.

Plan mode unchanged (read-only, no shell tool registered).

Updated `agent_and_yolo_modes_elevate_shell_sandbox_to_allow_network`
to pin the new YOLO contract: `DangerFullAccess` specifically, not
just "has network access."

Verified locally:

* `cargo fmt --all -- --check` clean.
* `cargo clippy --workspace --all-targets --all-features --locked
  -- -D warnings` clean.
* `cargo test --workspace --all-features --locked` — green
  (the snapshot::repo flake still flakes in batch but passes in
  isolation; unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:37:20 -05:00
Hunter Bown 5c72e5f463 fix(scroll): preserve user_scrolled_during_stream lock across resolve
Live repro: in a session producing content rapidly (sub-agent
running, multiple tool calls), the user scrolls up to read earlier
output. Their scroll position briefly takes effect, then snaps back
to the live tail when the next stream chunk arrives. Symptom is
"scrolling is broken / takes over instead of the transcript".

Root cause in `crates/tui/src/tui/widgets/mod.rs:188-210`:

* The user's mouse-scroll-up sets `transcript_scroll = at_line(N)`
  and `user_scrolled_during_stream = true`.
* During render, `resolve_top` clamps the state against
  `max_start = total_lines.saturating_sub(visible_lines)`. If
  `max_start < N` (transcript shrunk between scrolls and render —
  e.g., a sub-agent in-progress card collapsed into a smaller
  finished card, or the content briefly fits in one screen),
  `resolve_top` returns `Self::to_bottom()` (TAIL_SENTINEL).
* `is_at_tail()` on the post-resolve state returns `true`.
* The auto-clear at line 208 fires →
  `user_scrolled_during_stream = false`.
* Next `add_message` / sub-agent envelope sees `is_at_tail() &&
  !user_scrolled_during_stream` and calls `scroll_to_bottom()`. The
  user is yanked off their position mid-read.

`scrolled_by` has the same trapdoor: when `total_lines <= visible_
lines` it returns `to_bottom()` regardless of scroll direction
(line 145-148 in scrolling.rs). A user scroll-up while content
fits in one screen produces `to_bottom()` → `is_at_tail()` true →
auto-clear → next chunk yanks.

The fix
=======

Snapshot whether the user's PRIOR state was deliberately tail
(`is_at_tail()` BEFORE `resolve_top`), and only clear the lock
when:

1. Prior state was already TAIL_SENTINEL (deliberate, set by
   `scrolled_by` reaching `max_start` while scrolling DOWN, or by
   `scroll_to_bottom()`).
2. AND `total_lines > visible_lines` (so "tail" is meaningful —
   if the whole transcript fits, "is_at_tail" is trivially true
   and clearing the lock would yank the user back to bottom on
   the next chunk despite their explicit scroll-up).

This preserves all the legitimate clear paths:
* `TurnComplete` event clears the lock at the per-turn boundary
  (`ui.rs:879`).
* User invokes `scroll_to_bottom()` explicitly via key/menu
  (`app.rs:2459`).
* User scrolls down enough that `scrolled_by` reaches `max_start`
  in a transcript with real scroll room — state goes through
  `to_bottom()` BEFORE resolve, so `was_explicit_tail = true` and
  the lock clears.

What it stops:
* Render-time resolve clamping `at_line(N)` to tail when content
  shrunk doesn't quietly revoke the user's intent.
* `scrolled_by` collapsing a scroll-up to `to_bottom()` when
  content briefly fits in one screen no longer triggers the
  auto-clear (the prior state wasn't tail).

Verified locally:

* `cargo fmt --all -- --check` clean.
* `cargo clippy --workspace --all-targets --all-features --locked
  -- -D warnings` clean.
* `cargo test --workspace --all-features --locked` — 2038 passed,
  2 ignored, 0 failed (a snapshot::repo flake unrelated to scroll;
  passes in isolation).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:35:39 -05:00
Hunter Bown 1131e7a7b0 fix(capacity): disable controller by default (silent transcript wipe)
User-facing repro:

* In YOLO mode at low context utilisation (~5%), the engine briefly
  showed `resetting plan` in the footer and the transcript area went
  mostly black. Tools kept running (Plan panel + sidebar still
  rendered), but the chat history above the latest turn was gone.

Root cause: the capacity controller's `VerifyAndReplan` action
(`crates/tui/src/core/engine/capacity_flow.rs::apply_verify_and_replan`)
runs `self.session.messages.clear()` and rebuilds from the canonical
state. The capacity controller fires this when its slack-based
`p_fail` calculation crosses the high-severe band — independently of
the `auto_compact` setting, independently of token utilisation.

The user opted out of auto-compaction in v0.8.11 (default
`auto_compact = false`, #665), explicitly trusting the model with
the full 1M-token V4 window. Auto-managing the prefix on their
behalf via the capacity controller contradicts that posture and
silently destroys both the user-visible transcript and V4's prefix
cache.

The fix
=======

Flip `CapacityControllerConfig::default().enabled` from `true` to
`false`. The controller's `observe_*` and `decide` methods already
short-circuit when `enabled` is false (`capacity.rs:255`,
`capacity.rs:396`), so the existing wiring becomes a no-op for the
default config — no need for defensive gating in
`capacity_flow.rs`.

Power users who want the controller can opt in via
`capacity.enabled = true` in `~/.deepseek/config.toml`. The slack
heuristics, model priors, cooldowns, and intervention paths all
remain in the codebase, ready to re-engage on opt-in. Nothing
deleted.

Tests
=====

* `default_controller_is_disabled_and_skips_observations` — pins
  the new default; `observe_pre_turn` returns `None`.
* `opt_in_controller_observes_and_decides` — confirms `enabled =
  true` rearms the controller end-to-end.
* `app_config_without_capacity_uses_default_disabled` — pins that
  loading a config with no `[capacity]` section produces
  `enabled = false`.
* `capacity_disabled_by_default_keeps_messages_intact` — direct
  regression for the user-reported symptom: with default config,
  even a forced error-escalation checkpoint cannot trigger
  `messages.clear()`. Asserts the transcript length is preserved.

Verified locally:

* `cargo fmt --all -- --check` clean.
* `cargo clippy --workspace --all-targets --all-features --locked
  -- -D warnings` clean.
* `cargo test --workspace --all-features --locked` — 2039 passed,
  2 ignored, 0 failed (one flake on `snapshot::repo::tests::
  restore_removes_files_added_after_target_snapshot` was filesystem-
  timing-dependent, passes on isolation re-run; unrelated to this
  change).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:24:24 -05:00
Hunter Bown d4e5ee4eff chore(release): date v0.8.11 CHANGELOG section 2026-05-04 23:07:57 -05:00
Hunter Bown 54ed8c1d2f Merge pull request #678 from Hmbown/chore/v0.8.11-release
chore(release): bump version to 0.8.11 + CHANGELOG
2026-05-04 23:07:44 -05:00
Hunter Bown 9366a7c5f3 chore(release): bump version to 0.8.11
Final step in the v0.8.11 patch release. Bumps the workspace
`Cargo.toml`, all 9 internal path-dep version pins, and
`npm/deepseek-tui/package.json` to **0.8.11**. `Cargo.lock`
regenerated alongside.

The v0.8.11 CHANGELOG entry already landed on `main` via the
cache-maxing overhaul PR (#684). This commit only stamps the
version. Together they ship:

* **Cache-maxing for V4 1M context** — engine no longer rebuilds the
  system prompt on every turn (#684's `Session::last_system_prompt_hash`),
  the volatile working-set summary moved out of the system prompt
  into per-turn `<turn_meta>` on the latest user message, the tool
  array is anchored with `cache_control: ephemeral`, and the
  `messages_with_turn_metadata` injection skips tool-result
  messages so the assistant→tool_result invariant stays intact.
* **500K compaction floor** — automatic compaction refuses below
  500K tokens via `MINIMUM_AUTO_COMPACTION_TOKENS`. Manual
  `/compact` bypasses (explicit user agency).
* **Token-only compaction trigger** — dropped
  `CompactionConfig::message_threshold` and the message-count
  branch in `should_compact`; that 128K-era heuristic only fired
  on long sessions of small messages, exactly the case where
  rewriting the V4 prefix cache is most wasteful.
* **Legacy 128K naming** — `DEFAULT_CONTEXT_WINDOW_TOKENS` →
  `LEGACY_DEEPSEEK_CONTEXT_WINDOW_TOKENS`.
* **`npm install` resilience** — `install.js` now retries with
  exponential backoff, enforces per-attempt timeout + 30 s stall
  detector, honors `HTTPS_PROXY` / `HTTP_PROXY` / `NO_PROXY` (pure
  Node, no new dependencies), and prints download progress to
  stderr. Driven by a community report that `npm install` took 18
  minutes through a CN npm mirror; the GitHub Releases binary
  fetch was the bottleneck and CN mirrors don't proxy GitHub.

Verified locally:
* cargo fmt --all -- --check                            ✓
* cargo clippy --workspace --all-targets --all-features
                --locked -- -D warnings                 ✓
* cargo test --workspace --all-features --locked        ✓
* parity gates (snapshot, parity_protocol, parity_state) ✓
* bash scripts/release/check-versions.sh                ✓
  (workspace=0.8.11, npm=0.8.11, lockfile in sync)
* node scripts/release/npm-wrapper-smoke.js             ✓

Reminder for the maintainer at release time: the npm publish is
manual and requires 2FA OTP on every publish. After this PR
merges and the GitHub Release is fully drafted by `auto-tag.yml`,
publish from a developer machine:

    cd npm/deepseek-tui
    npm publish --access public

The `prepublishOnly` hook checks all eight binaries plus the
SHA256 manifest are present on the GitHub Release before letting
`npm publish` proceed, so this must happen *after* the GitHub
Release is finalized.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:01:16 -05:00
Hunter Bown 8dfecfb5d9 Merge pull request #685 from Agent-Skill-007/main
Revise README for clarity and structure
2026-05-04 23:00:15 -05:00
Hunter Bown e9b2c2ca5d Merge pull request #684 from Hmbown/feat/v0.8.11-compaction-v4-overhaul
feat(v0.8.11): cache-maxing overhaul — per-turn rebuild gate + working_set extraction + tool anchor
2026-05-04 22:51:29 -05:00
Leon.C f387037f51 docs: add CODE_OF_CONDUCT.md
Add Contributor Covenant Code of Conduct v2.1 to establish community guidelines and set clear expectations for participant behavior.
2026-05-05 11:50:02 +08:00