#167: Fix all 7 clippy warnings — annotated SeamMetadata dead fields,
removed unused should_cycle calls, collapsed nested ifs, fixed
useless_format and nonminimal_bool.
#168: Wire TokenUsage mailbox drain to subagent_cost accumulator.
handle_subagent_mailbox now intercepts TokenUsage before routing to
cards, computes cost via calculate_turn_cost, and increments
app.subagent_cost in real time. Footer reflects live sub-agent spend.
Restored ArchivedContext variant to HistoryCell (corrupted by prior
apply_patch). Version bump to 0.7.2.
Refs: #166, #167, #168
Adds the core SeamManager struct (#159) that uses V4 Flash to produce
append-only <archived_context> XML blocks at 192K/384K/576K thresholds.
No messages are deleted — soft seams are navigational summaries that
preserve the V4 prefix cache.
- seam_manager.rs: Flash-driven soft seam production, recompaction,
and cycle briefing replacement
- config.rs: [context] table with L1/L2/L3/cycle thresholds,
verbatim window, seam model, and per-model overrides
- compaction.rs: pub exports for plan_compaction, KEEP_RECENT_MESSAGES,
and CompactionPlan fields so SeamManager can reuse pinning heuristics
- cycle_manager.rs: pub CYCLE_HANDOFF_TEMPLATE for Flash briefing use
- main.rs: mod seam_manager registration
All 1,570 tests pass. Engine wiring follows in a subsequent commit.
`run_interactive` now calls `session_manager::prune_workspace_snapshots_at_boot`
right after the system-skills installer, dropping any snapshot in the
side-git repo older than 7 days (default; configurable via the new
`[snapshots]` section in `config.example.toml`). The helper is
non-fatal: a missing `git` binary, read-only home, or absent snapshot
dir all log a single WARN (or DEBUG for the count of pruned commits)
and return, so the TUI keeps starting even when retention can't run.
Also document the snapshot subsystem in `config.example.toml` —
disk-footprint expectations, where the side repo lives, and how
`/restore` / `revert_turn` consume it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two user-facing entry points to the snapshot side-repo:
- `/restore [N]` (slash command) — `/restore` with no arg lists the
10 most recent snapshots so the user can see what's available.
`/restore N` restores the N-th most recent snapshot. Outside YOLO
or `/trust on`, the command refuses to mutate files and tells the
user how to opt in (no in-flow modal-confirm path inside slash
commands today; trust mode is the explicit gate).
- `revert_turn` (agent-callable tool) — `turn_offset` (default 1)
counts in `pre-turn:*` snapshots, so the model can say "undo my
last edit" without having to enumerate the history. Approval-gated
(`ApprovalRequirement::Required`) since it mutates the workspace,
and registered through `with_full_agent_surface` so children
inherit it just like every other agent-mode tool.
Tests for both surfaces use the process-wide env mutex
(`crate::test_support::lock_test_env`) plus an RAII `HOME` guard so
tempdir-based snapshot resolution stays inside the per-test sandbox
even when the runner threads multiple tests in parallel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wire `pre_turn_snapshot` and `post_turn_snapshot` helpers into
`core::turn`, then call them from `Engine::handle_send_message` —
pre-turn fires right after `turn_counter` is incremented, post-turn
fires right after `Event::TurnComplete` is emitted.
Both hooks are dispatched via `tokio::task::spawn_blocking` so the
agent loop never waits on the side-git commit, and helper failures are
swallowed at WARN log level so a busted disk or missing `git` binary
can never derail a turn (per the snapshot module's documented
non-fatal contract).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduce `crate::snapshot` — a per-workspace side-git repo that lives
under `~/.deepseek/snapshots/<project_hash>/<worktree_hash>/.git` and
captures the workspace into commits via `git add -A` + `git commit
--allow-empty`. The user's own `.git` is never touched: every git
invocation passes both `--git-dir` (side repo) and `--work-tree`
(workspace) together, which is the load-bearing safety invariant.
Module layout:
- `paths.rs` — resolves the side-repo dir; strips `.worktrees/<name>`
so worktrees of the same checkout share a project_hash but get
distinct worktree_hashes.
- `repo.rs` — `SnapshotRepo::open_or_init / snapshot / restore / list /
prune_older_than`. Shells out to system `git` (avoids `git2` LGPL
surface). Honors workspace `.gitignore` automatically.
- `prune.rs` — boot-time helper used by session_manager (next commit).
Default retention is 7 days.
Tests (real `git` invocations on tempdirs, env-mutating tests serialised
through the existing `crate::test_support::lock_test_env` mutex) cover:
snapshot creates a commit in the side repo only, restore reverts files,
list respects limit, prune drops aged commits, gitignore is honored,
and re-init is idempotent.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README: add a "Publishing your own skill" section explaining the
`github:owner/repo` install path, the multi-skill `skills/<name>/`
layout, and how to submit to the curated registry.
- config.example.toml: document `[skills] registry_url` /
`max_install_size_bytes` next to the existing `[network]` section so
users see the network-gate dependency in context.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slash-command surface for the community-skill installer:
- `/skill install <github:owner/repo|https://...|<registry-name>>` parses
the spec via `InstallSource::parse`, calls `install_with_registry`, and
surfaces `NeedsApproval`/`NetworkDenied` with actionable messages
pointing at `[network]` config (we deliberately don't dispatch a modal
from the sync slash-command path; the underlying installer returns the
outcome so a future approval wiring can reuse it).
- `/skill update <name>` re-fetches and prints "no upstream change" when
the checksum matches.
- `/skill uninstall <name>` and `/skill trust <name>` both refuse to
touch system skills (no `.installed-from` marker).
- `/skills --remote` (or `/skills remote`) fetches the curated registry
through the same network gate and prints `name — description (source)`.
Internals:
- Sub-command dispatch happens in `run_skill` before activation lookup,
so a user can't accidentally activate a skill literally named
`install`. Async install/update/uninstall plumbed through
`tokio::task::block_in_place` + `Handle::current().block_on`, matching
the existing pattern in `commands/cycle.rs`.
- `installer_settings` loads `Config` on demand — `App` doesn't carry a
`Config` reference, and the cost of a single TOML parse is negligible
next to the network round-trip the install will make.
Config:
- New `[skills]` section in both `crates/tui/src/config.rs::Config` and
the workspace `crates/config/src/lib.rs::ConfigToml` with
`registry_url` (default: bundled raw GitHub index) and
`max_install_size_bytes` (default: 5 MiB).
- `merge_config` propagates the new field, default impls cover the
unset case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `crates/tui/src/skills/install.rs` — async installer that pulls
user-authored skills from GitHub repos, raw tarball URLs, or a curated
`index.json` registry. The whole pipeline is gated by the per-domain
`NetworkPolicy` (#135), validated against path-traversal / size / symlink
attacks before any bytes hit the destination, and atomic-renamed into place
so a half-installed skill cannot survive a failure mid-extract.
Public surface:
- `InstallSource::{GitHubRepo,DirectUrl,Registry}` with `parse(spec)`.
- `install` / `install_with_registry` returning
`InstallOutcome::{Installed,NeedsApproval,NetworkDenied}`.
- `update` / `update_with_registry` returning
`UpdateResult::{NoChange,Updated,NeedsApproval,NetworkDenied}` — uses a
SHA-256 over the downloaded tarball to short-circuit no-op fetches.
- `uninstall` / `trust` — both refuse to touch directories without an
`.installed-from` marker, so the bundled `skill-creator` system skill is
protected.
- `fetch_registry` — typed loader for the curated `index.json`.
Validation hard rules (each covered by an integration test):
- `..` segments and absolute paths in tar entries are rejected.
- Symlinks / hardlinks in tar entries are rejected outright.
- Uncompressed total size is bounded by `max_size` (default 5 MiB).
- SKILL.md must exist at the archive root or under `skills/<name>/`.
- Frontmatter must carry both `name` and `description`.
- `install` with an existing destination requires `update = true`.
- `update` re-fetches and only replaces the on-disk install when the
checksum changes; no-change paths skip the rename entirely.
Adds `tar`, `flate2`, and `sha2` to `crates/tui/Cargo.toml` and propagates
the resulting lockfile drift to `Cargo.lock`.
Tests: 11 colocated unit tests in `install.rs` + 11 integration tests in
`crates/tui/tests/skill_install.rs` driving a `tiny_http`-based server so
the network gate, download cap, validation pipeline, and atomic rename
all run end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Inject LSP diagnostics as a synthetic user message after every successful
file edit (`edit_file`, `apply_patch`, `write_file`) so the agent sees
compile breaks before its next reasoning step. Largest agent-quality
lever in v0.7.0.
Pieces:
- `crates/tui/src/lsp/`: thin JSON-RPC stdio client (no `tower-lsp`),
per-language registry, diagnostics renderer producing the
`<diagnostics file="…">` block format. `LspManager` owns lazily
spawned per-language transports keyed by `Language`.
- `core/engine.rs`: hook on the success branch of the tool-result loop
derives the edited file path(s) per tool, queries the LspManager
with a 5 s timeout, and collects rendered blocks into
`pending_lsp_blocks`. The queue is flushed as a `text` content
block on the next request iteration so the model sees the
diagnostics before it streams its next turn.
- `[lsp]` config schema (`enabled`, `poll_after_edit_ms`,
`max_diagnostics_per_file`, `include_warnings`, optional
`servers` override) with built-in defaults for rust-analyzer,
gopls, pyright, typescript-language-server, and clangd.
- Failure modes are non-blocking by design: a missing LSP binary
logs a one-time warning and skips the hook; a crashed server or
poll timeout simply drops that turn's diagnostics. The agent's
work is never blocked.
Tests: 24 unit tests cover language detection, registry overrides,
filter/sort/truncate behavior, and the rendered block format. Three
engine-level tokio tests exercise the full path through a fake
transport (no real LSP server is ever spawned in CI).
Acceptance criteria (per #136):
- Edit introducing a type error -> next request body contains
`<diagnostics file="…">` block at the right line/col.
- `[lsp] enabled = false` -> no diagnostics injected.
- Snapshot test exercises full path with mock transport.
- LSP binary not on PATH -> one-time warning, agent proceeds.
- 5 s timeout, errors-only by default.
- Transports spawn lazily on first edit per language.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Connects the new BacktrackState to the live UI:
- App: holds a `backtrack: BacktrackState` and a new
`truncate_history_to(new_len)` helper that keeps `tool_cells`,
`tool_details_by_cell`, and the sub-agent card index consistent.
- live_transcript: gains a `Mode::BacktrackPreview { selected_idx }`
that highlights the Nth-from-tail HistoryCell::User with a `▶` marker
and reverse-video styling. Cache stays valid across mode flips —
decoration is applied post-wrap. Left/Right/Enter/Esc emit new
`ViewEvent::Backtrack{Step,Confirm,Cancel}` events.
- ui.rs: routes Esc through `BacktrackState::handle_esc` only when
no popup is open and not streaming, opens the preview overlay on
the second Esc, and on confirm trims `app.history` /
`app.api_messages` and refills the composer with the dropped user
input. Streaming and existing popup paths preserve their original
Esc behaviour.
- keybindings: documents the `Esc Esc` chord in the help catalog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces `tui::backtrack::BacktrackState` — a small Inactive/Primed/
Selecting state machine for the two-step Esc chord. The module owns
nothing beyond its phase enum; transcript snapshots, popup detection,
and fork side-effects all stay in the UI layer so the state machine is
trivially unit-testable.
`handle_esc(total_user_messages)` returns one of `None | Prime |
Cancel | OpenOverlay`, `step(Direction)` walks the selection in
`Selecting`, and `confirm()` yields the depth-from-tail and resets to
`Inactive`. 15 unit tests cover every transition including bounds
clamping, empty-transcript short-circuit, and defensive Esc routing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds `RuntimeThreadManager::fork_at_user_message(id, depth_from_tail)` —
a sibling of the existing `fork_thread` that drops every turn from the
Nth-from-tail user message onward and returns the dropped user input so
the caller can pre-populate the composer.
The existing `fork_thread` is left untouched. The new helper mirrors its
copy loop but stops short of the cutoff turn, emitting a
`thread.forked` event with backtrack provenance fields. Includes unit
tests covering depth=0, depth=1, out-of-range error, and source-thread
non-mutation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- crates/tui/src/tui/ui.rs — new Alt+V/Alt+Shift+V arm next to Alt+A/Y/P family, no empty-input gate
- crates/tui/src/tui/history.rs — 5 hint strings + 4 test assertions updated to "Alt+V for details"
- crates/tui/src/tui/keybindings.rs — entry under Submission so ? overlay lists it
- Bare 'v' handler unchanged (legacy muscle memory)
Adds `integration_mock_llm.rs` covering the LlmClient trait surface:
- streaming turn loop (text deltas + finish reason)
- reasoning-content replay across tool-call rounds (V4 §5.1.1, the
HTTP 400 path that broke v0.4.9-v0.5.1)
- tool-call round-trip with chunked input JSON
- multiple tool calls in one turn preserve event ordering
- compaction-style non-streaming `create_message`
- sub-agent style independent parent/child mocks
- capacity-gate observation of a captured request
Four full-engine tests are `#[ignore]`-marked as BLOCKED on the engine
refactor from concrete `Option<DeepSeekClient>` to `Arc<dyn LlmClient>`.
Once that wiring lands the ignored tests light up with no mock changes.
Adds:
- `tests/support/llm_client.rs` mirrors the trait so the mock can be
brought into the integration test via `#[path]` without dragging in
the rest of the binary's module tree
- `tests/fixtures/.gitkeep` so the `eval --record` output directory
rides the repo
- `tests/README.md` documents both the trait-level mocking strategy
and the `--record` fixture flow
- `record_flag_writes_one_jsonl_line_per_step` in `eval_harness.rs`
exercises the new `--record` flag end-to-end
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a queue-driven `MockLlmClient` that implements the `LlmClient` trait
by replaying canned per-turn `StreamEvent` vectors and capturing every
outgoing `MessageRequest`. The mock lives at the trait boundary so it
stays decoupled from the concrete reqwest plumbing inside `DeepSeekClient`,
and surfaces builders (`canned::*`) for the common event shapes (text
delta, thinking delta, tool_use start, input JSON delta, message delta).
Wires a new `--record <DIR>` flag into `deepseek eval` that appends one
JSON Lines fixture line per step to `<DIR>/<scenario>.jsonl`. The format
is documented at the top of `eval.rs` and is the storage shape the mock
will replay from.
`crates/tui/src/llm_client.rs` becomes `crates/tui/src/llm_client/mod.rs`
to host the new submodule cleanly. The trait shape is unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Threads the optional `NetworkPolicyDecider` from `EngineConfig` through to
`ToolContext.network_policy` and `McpPool::with_network_policy`. Each gate
point follows the same pattern: extract the host, call `decider.evaluate`,
then `Allow` proceeds, `Deny` returns a structured permission-denied error,
and `Prompt` falls through to the same denial with a hint pointing to
`/network allow <host>` (full modal flow lands in a follow-up).
* `fetch_url` — gates on the parsed URL host.
* `web_search` — gates DuckDuckGo (`html.duckduckgo.com`) and the Bing
fallback (`www.bing.com`) independently so a deny on one engine doesn't
silently let the other through.
* MCP — only the HTTP/SSE transport is gated; STDIO MCP servers are
unaffected. `McpConnection::connect_with_policy` replaces the old
`connect` (no external callers existed).
The session cache short-circuits `evaluate` once a host is approved, so
the existing `approve_session` hook is enough to wire the prompt-once
flow when the approval modal lands.
`NetworkPolicyDecider::with_default_audit` materializes the auditor at
`~/.deepseek/audit.log` when the config has `audit = true`.
Includes one tool-level test asserting `fetch_url` denies a blocked host
through the policy gate.
Adds the `[network]` table to both the workspace config crate (`ConfigToml`)
and the live tui config (`Config`), plus a documented example block in
`config.example.toml`. Schema:
```toml
[network]
default = "prompt" # allow | deny | prompt
allow = ["api.deepseek.com", "github.com"]
deny = []
audit = true
```
`NetworkPolicyToml::into_runtime()` builds a runtime `NetworkPolicy` so the
engine can construct a `NetworkPolicyDecider` without reaching across crate
boundaries. Defaults preserve pre-v0.7.0 behavior: when the section is
absent, no policy is enforced.
Introduces `network_policy::{Decision, NetworkPolicy, NetworkPolicyDecider,
NetworkAuditor, NetworkSessionCache, NetworkDenied}` for gating outbound
network calls.
Deny-wins precedence: a host listed in both `allow` and `deny` is denied.
Subdomain wildcard via leading-dot entries (`.example.com` matches
`api.example.com` but not the apex). Audit log writes one plaintext line
per terminal decision to `~/.deepseek/audit.log` in the format
`<RFC3339> network <host> <tool> <Allow|Deny|Prompt-Approved|Prompt-Denied>`.
Approve-once-for-session caching is implemented in `NetworkSessionCache`;
`approve_persistent` mutates the policy's allow list so callers can write
back to config later.
19 unit tests cover deny-wins precedence, subdomain matching, audit
logging, session-cache short-circuit, and `NetworkDenied` shape.
Adds first-class keyring management on the dispatcher CLI and wires
the TUI to read its DeepSeek key through the same Secrets façade.
Subcommands:
* `auth set --provider <name>` writes to the OS keyring; prompts
on stdin without echo, never prints the key, never touches
`config.toml`. Supports `--api-key` and `--api-key-stdin`.
* `auth get --provider <name>` reports `set` / `not set` plus the
resolving layer (keyring / env / config-file). Never prints the
value.
* `auth clear --provider <name>` deletes from keyring and from any
legacy plaintext slot in `config.toml` for parity.
* `auth list` table of all known providers and
whether each layer holds a key. Non-revealing.
* `auth migrate [--dry-run]` reads `api_key` (root + per-provider
blocks) from `config.toml`, writes them to the keyring, then
strips the entries from disk. Idempotent.
* `auth status` expanded to also report the active
keyring backend and per-provider keyring state.
`doctor` now prints `keyring backend: ...` plus per-provider
`keyring=yes/no, env=yes/no` lines and points users at
`deepseek auth set` when no key resolves.
`Config::deepseek_api_key()` in the TUI is rewritten to consult
`Secrets::auto_detect()` first (keyring -> env), then fall back to
the existing TOML slots with a deprecation warning. Error messages
now lead with `deepseek auth set --provider <name>`.
5 new unit tests cover argument parsing for the new subcommands and
end-to-end auth set/clear/migrate behaviour against an
`InMemoryKeyringStore`, verifying that no plaintext key ever lands
in `config.toml`.
Verified manually on macOS:
$ deepseek auth set --provider deepseek --api-key-stdin
$ security find-generic-password -s deepseek -a deepseek
# entry present
$ deepseek auth migrate
# api_key lines stripped from ~/.deepseek/config.toml
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes `ConfigToml::resolve_runtime_options` through the new
`deepseek_secrets::Secrets` façade so API keys are read from the OS
keyring before any environment variable, with the existing
plaintext-config layer kept as a deprecated last resort. The
precedence is now:
CLI flag -> keyring -> env -> config-file
Reads of an `api_key` value from `~/.deepseek/config.toml` now emit
a one-time `tracing::warn!` directing users to
`deepseek auth set` / `deepseek auth migrate`.
`resolve_runtime_options_with_secrets` is exposed for tests and
process-level injection (the `cfg(test)` default uses an in-memory
store so unit tests never touch the real OS keychain). The
nvidia-nim provider keeps its `DEEPSEEK_API_KEY` env fallback for
back-compat. New tests cover keyring > env > config-file precedence
end-to-end.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>