Commit Graph

68 Commits

Author SHA1 Message Date
Hunter Bown ddaabbfed2 chore(rebrand): finish codewhale release surfaces 2026-05-23 13:41:46 -05:00
Hunter Bown ad122fd6f8 feat(subagents): make step API timeout configurable 2026-05-21 00:45:04 +08:00
Hunter Bown 8597afc076 feat(provider): add Wanjie Ark support 2026-05-21 00:02:02 +08:00
Hunter Bown a3f88bf6cf fix(search): default web search to bing (#1619)
Summary:
- add Bing as explicit default web_search provider
- keep explicit DuckDuckGo configuration supported
- update docs/help/config examples

Validation: CI green before merge.
2026-05-14 03:31:15 -05:00
Hunter Bown a4637fe7d1 fix(settings): reduce motion in VTE flicker terminals
Harvested from PR #1527 by @axobase001.

Co-authored-by: axobase001 <dengzhuoran9@gmail.com>
2026-05-12 23:39:44 -05:00
Hunter Bown f964e2fd37 fix(snapshot): cap workspace size at 2 GB before first side-repo init
Users reported running `deepseek-tui` inside project directories
with hundreds of GB of content — ML datasets, model weights
(`.safetensors`, `.gguf`, `.pt`, `.onnx`), Docker image dumps,
parquet / arrow caches, anything that falls outside the snapshot
built-in excludes. The pre/post-turn snapshot path called
`SnapshotRepo::open_or_init` which initialized the side git repo
and then ran `git add -A` — which walked the entire workspace
indexing every file. On a 100-300 GB directory this hung the TUI
for minutes-to-hours while git churned through the index.

The pre-existing v0.8.27 fixes (#1112: retention cap, mid-session
prune, expanded built-in excludes) addressed the orthogonal
"snapshots grow unbounded over many turns" angle but did nothing
to prevent the first snapshot from being impossible to take.

This change adds `estimate_workspace_size_bounded()` — a bounded
`ignore::WalkBuilder` walk that respects `.gitignore` and the
snapshot module's existing skip list (`node_modules/`, `target/`,
`.next/`, `.venv/`, `__pycache__/`, etc.). The walk early-exits
at either the byte cap or 200,000 file entries, returning `None`
to signal "too big to snapshot."

`SnapshotRepo::open_or_init_with_cap(workspace, cap_bytes)` calls
the estimator *before* the side `git init`, and returns
`Err(InvalidInput)` with a "workspace too large" reason — which
`turn::snapshot_with_label` already logs at WARN and continues
past, so a too-large workspace silently disables snapshots
without blocking any turn. The check is paid only on first init;
subsequent snapshots through the existing side repo skip it.

Plumbing:
- `SnapshotsConfig.max_workspace_gb` (default 2, `0` disables)
- `EngineConfig.snapshots_max_workspace_bytes` resolved at engine
  construction from `config.snapshots_config().max_workspace_gb`
- `pre_turn_snapshot` / `post_turn_snapshot` / `pre_tool_snapshot`
  take a `cap_bytes: u64` argument threaded from the engine
- `SnapshotRepo::open_or_init` retains its v0.8.31 signature as a
  thin wrapper over `open_or_init_with_cap` using the default cap
- `config.example.toml` documents the new `max_workspace_gb` knob
  with the "set to 0 to disable" escape hatch for users with
  legitimate large monorepos

Six new tests pin both the estimator (under-cap returns Some,
over-cap returns None, builtin-excluded dirs skipped, cap=0
disables the bound) and the `open_or_init_with_cap` integration
(oversized workspace fails with the right error and references
the config knob; cap=0 succeeds even on oversized content).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 02:11:10 -05:00
Hunter Bown 3aaf0ad95e feat(vision): add image_analyze tool gated behind vision_model feature flag
`image_analyze` sends an image file to an OpenAI-compatible vision
endpoint and returns the model's natural-language description.
Complements `image_ocr` (which uses local tesseract for "what text
is on this image"); `image_analyze` is for "what is this image
about" — visual reasoning the local OCR engine can't do.

Trust-boundary scope: **two-step opt-in only**.

1. The feature is gated by `[features] vision_model = true` —
   default `false`.
2. The tool needs a `[vision_model]` config block specifying
   `model` (with optional `api_key` / `base_url` — falls back to
   the main config api_key + the OpenAI base URL).

Without both, the tool isn't registered, so no install fires a
vision API call without explicit user setup. Workspace boundary:
the tool rejects absolute paths and any `..` parent-dir
traversal before any base64 encoding or HTTP call. Stateless —
each call sends only the requested image + optional prompt; no
session, no conversation history attached. Supports PNG, JPEG,
GIF, WebP, and BMP inputs.

**Billing**: each call hits the configured vision endpoint
(OpenAI by default — `gpt-4o-mini` / `gpt-4o` family commonly
configured). Users with their own deployments (Gemini, Claude
Vision via OpenAI shim, local llama.cpp) can point `base_url` /
`api_key` at the alternative.

Tests cover the tool metadata (read-only capability, correct
name), MIME-type detection across the supported formats and the
unsupported-format rejection path, and the workspace-boundary
checks (absolute paths and `..` traversal both reject before
any API call). Skipped from the upstream PR: the
`.github/workflows/sync-cnb.yml` rewrite, which v0.8.31 already
addressed with the concurrency/scoped-push refactor; landing the
older form would regress that commit.

Resolved a clippy::collapsible_if in tool_setup.rs (the
`if feature && let Some(cfg) = ...` form) to satisfy the
workspace -D warnings gate.

Harvested from PR #1467 by @MMMarcinho

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 01:03:19 -05:00
Hunter Bown 8f33e4bd48 feat(providers): add AtlasCloud as a first-class provider
AtlasCloud (https://atlascloud.ai) hosts the V4 family on its own
DeepSeek-compatible endpoint at `https://api.atlascloud.ai/v1`, and
several contributors had been running it through the
OpenAI-compatible passthrough with manual `base_url` / model
overrides. Selecting `provider = "atlascloud"` in
`~/.deepseek/config.toml` (or via `DEEPSEEK_PROVIDER=atlascloud`)
now wires up:

- documented `DEFAULT_ATLASCLOUD_BASE_URL` /
  `DEFAULT_ATLASCLOUD_MODEL` defaults so a fresh install needs
  only the api_key
- a `[providers.atlascloud]` config block with the same fields
  every other named provider exposes (api_key / base_url / model
  / http_headers)
- `ATLASCLOUD_API_KEY` env var path, including the secrets test
  cleanup loop so per-test env hygiene continues to work
- the provider-picker / `/provider` slash command entries so the
  provider is reachable from the runtime UI, not just config
- the env-driven `*_BASE_URL` override branch so users who pin a
  proxy can still flip it without editing config.toml

Trust-boundary pins held: AtlasCloud is opt-in (default remains
DeepSeek), no API keys are hardcoded, the api_key resolution flows
through the same `secrets` crate path every other provider uses,
and the provider-config base_url stays settable per environment.

Resolved 3-way merge conflicts in `crates/secrets/src/lib.rs` (env
cleanup loop) and `crates/tui/src/config.rs` (per-provider
base_url match arm + `provider_passes_model_through` predicate)
so the contributor's AtlasCloud branch coexists with the v0.8.x
provider expansion already on `main`. Added the missing match arm
in `validate_provider_base_url` so the non-exhaustive-pattern
check passes after the new variant lands.

Harvested from PR #1436 by @lucaszhu-hue

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:40:43 -05:00
Hunter Bown 40df46c73d feat(web_search): add configurable Tavily and Bocha provider backends
DuckDuckGo HTML scraping with Bing fallback remains the default
`web_search` backend — no API key required, no behaviour change for
installs that don't opt in. Users in regions where those scrapers
are rate-limited or unreliable can now set `[search]
provider = "tavily" | "bocha"` plus `api_key = "..."` in
`~/.deepseek/config.toml` (or via the `DEEPSEEK_SEARCH_PROVIDER` /
`DEEPSEEK_SEARCH_API_KEY` env vars) to route every `web_search`
call through the chosen API.

Tavily targets general AI-agent search; Bocha (博查) is the
mainland-China-friendly equivalent. Both providers are gated by
the existing `[network]` policy on their respective hosts
(`api.tavily.com`, `api.bochaai.com`) and surface a clear
`ToolError` (rather than a silent fallback to DuckDuckGo) when
the user has opted in but forgotten to set `api_key`. Test pins
the missing-key behaviour for both providers.

Resolved 3-way merge conflicts in `web_search.rs` (description
text and test module) so the contributor's helpers
(`truncate_error_body`, `sanitize_error_body`) coexist with the
v0.8.30 spam-detection and query-parsing tests already on `main`.
Folded the `SearchProvider::default()` impl into a `#[default]`
derive to satisfy the workspace `-D warnings` clippy gate, and
threaded `search: Option<SearchConfig>` through `merge_config` so
the multi-config layering doesn't break the field initialiser.

Harvested from PR #1294 by @sandofree

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 00:37:34 -05:00
Jefsky Wong 5c112b40bf fix(config): default deepseek-cn to official api.deepseek.com (#1084)
Sets the `deepseek-cn` provider preset's default `base_url` to the official host (`https://api.deepseek.com`) per [api-docs.deepseek.com](https://api-docs.deepseek.com/). Keeps recognizing `api.deepseeki.com` in URL heuristics and chat-client normalization so existing configs continue to work, and updates the `doctor` strict-tool-mode endpoint hint, docs, and examples accordingly.

Closes #1079. Thanks to @Jefsky for the fix.
2026-05-07 12:01:07 -05:00
Hunter Bown f97604c3f0 fix(provider): enable OpenAI-compatible TUI runtime (#1017) 2026-05-07 05:32:15 -05:00
Hunter Bown c270ef81ef fix(tui): harden terminal resume and runtime context
Summary:
- Keep default auto alternate-screen mode inside the TUI so transcript scrolling stays app-owned unless users explicitly opt out.
- Queue terminal resume events when the engine channel is full, avoiding stranded paused terminal state after interactive tool cancellation or bursts.
- Scope crash-checkpoint recovery to the resolved launch workspace instead of the shell cwd.
- Add runtime deepseek_version to the prompt environment block so agents can distinguish installed runtime identity from a stale checkout.

Test plan:
- cargo test -p deepseek-tui --locked on a simulated merge with current main
- cargo fmt --all -- --check
- git diff --check
- Existing PR CI was green for lint, version drift, Linux/macOS/Windows tests, npm wrapper smoke, and GitGuardian.
2026-05-07 03:48:09 -05:00
Hunter Bown c7ed05a07c feat(api): default DeepSeek to beta endpoint
Closes #941.\n\nRefs #938, #939, #940.
2026-05-06 21:24:59 -05:00
Reid 78c415f40c feat(provider): add Ollama provider support (#921)
Source PR: #921 by @reidliu41.
Closes #908.

Local verification:
- cargo test --workspace --all-features ollama
- cargo fmt --all -- --check
- cargo build

Co-authored-by: reidliu41 <reid201711@gmail.com>
2026-05-06 20:16:46 -05:00
Hunter Bown da047c44ff feat(tui): notification_condition override + assistant text in OSC 9 body (#920)
* feat(tui): add `notification_condition` override + assistant text in body (#820)

`[notifications]` already controls method (auto/osc9/bel/off), the
`threshold_secs` gate, and the `include_summary` body. Some users
want a simpler high-level switch — "always notify on every turn" or
"never notify" — without having to know the lower-level fields.

This adds a single optional `[tui].notification_condition` field:

  - `"always"` — notify on every successful turn (no duration
    threshold). The configured `[notifications].method` and
    `include_summary` flag are still respected.
  - `"never"`  — suppress all turn-completion notifications.
  - omitted    — fall back to the existing `[notifications]` defaults
    (the v0.8.15 behavior is unchanged).

The OSC 9 / BEL body now also carries the assistant's reply text,
sanitized and truncated to 360 characters, with a fallback to the
latest assistant message in `api_messages` when the streaming buffer
was empty (e.g. a tool-only turn). When `include_summary = true`,
the elapsed/cost line is appended on a new line.

Drive-by: drop the unused `Method::from_str` helper (the new code
match-arms over the typed `NotificationMethod` enum, so the parser
helper had no remaining callers).

Differences from upstream #820:
- Keeps the `[notifications]` section in `config.example.toml`
  (with `notification_condition` documented as an opt-in override)
  rather than deleting the existing block. This avoids breaking
  configs that already set `[notifications].method` etc.
- Drops the unrelated `#[allow(dead_code)]` on
  `schema_migration::registry`.
- Threads `Option<CostEstimate>` through the helper (the cost
  surface changed from `Option<f64>` since #820 was authored).

Tests:
- `notification_settings_*` (3) — `always` keeps the configured
  method, `never` returns `None`, missing override falls back.
- `completed_turn_notification_*` (4) — streaming text wins, falls
  back to latest assistant message, default placeholder, and 360-char
  truncation with `...`.

Integrates #820.

Co-authored-by: zero <1603852@qq.com>
Co-authored-by: zerx-lab <161401688+zerx-lab@users.noreply.github.com>

* style: fmt — collapse short test message helper calls

---------

Co-authored-by: zero <1603852@qq.com>
Co-authored-by: zerx-lab <161401688+zerx-lab@users.noreply.github.com>
2026-05-06 19:29:02 -05:00
Hunter Bown 633092167c feat(config): support custom HTTP headers (#914)
Integrates the useful custom HTTP header support from #881 onto current main.

- support root, provider-specific, and DEEPSEEK_HTTP_HEADERS overrides
- apply validated extra headers to model API requests while preserving protected Authorization and Content-Type defaults
- document the config shape in README, config.example.toml, and docs/CONFIGURATION.md

Co-authored-by: Desheng <8596814+dst1213@users.noreply.github.com>
2026-05-06 18:13:18 -05:00
Hunter Bown 1043374e6a chore(release): prepare v0.8.15 2026-05-06 11:06:00 -05:00
Agent007 a335ff5e4c feat(provider): add vLLM provider support (#737)
Add vLLM as a first-class OpenAI-compatible self-hosted provider with VLLM_BASE_URL, VLLM_API_KEY, and VLLM_MODEL wiring.
2026-05-05 21:22: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 ff5c99965d feat(sandbox): pluggable SandboxBackend + Alibaba OpenSandbox adapter (#645) 2026-05-05 00:16:34 -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 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 848725e65d docs: document zh-Hans locale activation (#652) 2026-05-05 00:08:39 -05:00
wangfeng b6a6c88327 fix(tui): replace hardcoded colors with Color::Reset for terminal compatibility (closes #666) 2026-05-04 18:21:15 -07:00
wangfeng 5d1dee794d feat(runtime): route large tool outputs through workshop to protect parent context (closes #548)
Tool outputs (read_file, grep_files, exec_shell, fetch_url, web_search) that
exceed a configurable token threshold are now intercepted before they reach
the parent context. A structured synthesis header replaces the raw blob; the
full content is stored in the workshop variable `last_tool_result` for later
`promote_to_context` retrieval.

Key changes:
- New `crates/tui/src/tools/large_output_router.rs`: `LargeOutputRouter`,
  `WorkshopConfig`, `WorkshopVariables`, `RouteDecision`, token estimator,
  synthesis-prompt builder, and wrap_synthesis helper. Full unit-test suite.
- `ToolContext` gains `large_output_router` and `workshop_vars` fields plus
  the `with_large_output_router` builder; constructor defaults are `None` so
  sub-agents and test contexts are unaffected.
- `ToolRegistry::execute_full_with_context` applies routing after every tool
  call; `raw=true` in the tool input bypasses routing for that invocation.
- `EngineConfig` gains a `workshop` field; `Engine::new` creates the shared
  `WorkshopVariables` Arc when the field is present and wires it into every
  `build_tool_context` call.
- `Config` gains `[workshop]` table deserialization; `merge_config` propagates
  it like other optional tables.
- `config.example.toml` documents `[workshop]`, `large_output_threshold_tokens`
  (default 4096), and per-tool threshold overrides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 16:31:31 -07:00
macworkers 437615c7c4 docs: document zh-Hans locale activation for issue #566
- Add locale option to config.example.toml [tui] section with all
  supported values (auto/en/ja/zh-Hans/pt-BR) and clear notes that
  this controls TUI chrome only, not model output language.
- Fix README.zh-CN.md: settings.toml → config.toml (wrong filename).
- Expand README.zh-CN.md locale section with concrete config snippet
  and LANG= env-var example; add link to docs/LOCALIZATION.md.

The zh-Hans locale has been fully implemented in localization.rs since
v0.7.6 — this commit makes it discoverable without reading source code.

Closes #566

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 16:09:46 -07:00
macworkers 63e2201da1 feat(sandbox): pluggable SandboxBackend trait + Alibaba OpenSandbox adapter (#516)
Adds a pluggable sandbox layer to exec_shell, with Alibaba OpenSandbox
as the first opt-in backend. No sandbox by default — existing behavior
unchanged.

New files:
  crates/tui/src/sandbox/mod.rs       — SandboxBackend trait + factory
  crates/tui/src/sandbox/backend.rs   — SandboxOutput, SandboxKind
  crates/tui/src/sandbox/opensandbox.rs — HTTP adapter for OpenSandbox REST API

Config additions (~/.deepseek/config.toml):
  sandbox_backend = "opensandbox"   # or "none" (default)
  sandbox_url = "http://localhost:8080"
  sandbox_api_key = "..."           # optional

Implemented using `deepseek exec --model deepseek-v4-pro`. 🐋
2026-05-04 15:42:58 -07:00
Hunter Bown a68c8dc974 docs(notifications): only completed turns notify; add Key Reference + WezTerm-on-Windows test
Post-merge review feedback on #583 surfaced four small accuracy gaps:

1. The narrative docs in `docs/CONFIGURATION.md` and the inline comment
   in `config.example.toml` said the notification fires "when a turn
   takes longer than a threshold" — but the call site in
   `tui/ui.rs:928` is gated on `TurnOutcomeStatus::Completed`. Failed
   and cancelled turns are silent on purpose. Spell that out so users
   don't expect alerts on long failures.

2. The `notify_done` rustdoc still summarised `Auto` as "Osc9 for known
   terminals, Bel otherwise" — internally inconsistent with the new
   Windows-aware fallback documented one screen earlier on the
   `Method::Auto` enum and on `resolve_method`. Update the public
   rustdoc to point at the canonical resolution table on
   `resolve_method` and call out the `Off`-on-Windows branch.

3. The `## Key Reference` list in `docs/CONFIGURATION.md` had no entries
   for `[notifications].method`, `[notifications].threshold_secs`, or
   `[notifications].include_summary`. Other features with a dedicated
   subsection (e.g. `[memory].enabled`) are listed there too, so readers
   scanning the canonical key list could not discover the notification
   knobs. Added the three keys with cross-references to the
   Notifications subsection.

4. The Windows-only test only covered the unknown-`TERM_PROGRAM` →
   `Off` fallback. The positive path (known OSC-9 terminal still
   resolves to `Osc9`) was only tested via `iTerm.app`, which is a
   macOS-only program — Windows CI would still pass if the `WezTerm`
   arm of the match disappeared. Added
   `auto_detect_picks_osc9_for_wezterm_on_windows` so the
   WezTerm-on-Windows compatibility guarantee is exercised on the
   Windows runner.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 13:38:21 -05:00
Hunter Bown 3636908bb9 fix(notifications): default Windows Auto fallback to Off, not BEL
On Windows, the audio stack maps BEL (`\x07`) to the
`SystemAsterisk` / `MB_OK` chime — the same sound applications use
for error popups. So with the previous `Method::Auto` fallback to
`Bel`, every successful turn-completion notification ended up
sounding identical to a software error.

Reported by a community user who described it as "the popup-error
sound from a CAD program I used to use" (#583).

resolve_method() now returns `Off` instead of `Bel` on Windows for
unknown TERM_PROGRAM values. Known OSC-9-capable terminals
(`iTerm.app`, `Ghostty`, `WezTerm`) still resolve to `Osc9` on
every platform, so users running WezTerm on Windows keep getting
real notifications. macOS and Linux behaviour is unchanged.

Windows users who actively want an audible cue can opt back in by
setting `[notifications].method = "bel"` in `~/.deepseek/config.toml`.

Also:
- Documents `[notifications]` in `docs/CONFIGURATION.md` with an
  explicit Windows note (the schema was previously undocumented).
- Updates the inline comment in `config.example.toml` so users
  reading the seed config see the platform-specific behaviour.
- Splits the existing `auto_detect_picks_bel_for_unknown` test
  into a Unix variant (`#[cfg(not(target_os = "windows"))]`) and
  adds a new Windows-gated test that asserts the `Off` fallback,
  so CI's Windows runner exercises the platform-specific path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 12:49:03 -05:00
Hunter Bown af9e651017 feat(hooks): shell_env hook for per-shell-tool env injection (#456)
New `HookEvent::ShellEnv` fires immediately before each `exec_shell`
invocation. The hook's stdout is parsed as `KEY=VALUE\n` lines and the
resolved env vars are merged on top of the spawned process environment.
Useful for ephemeral credentials (`aws-vault export …`), per-skill
PATH adjustments, short-lived tokens.

* `HookExecutor::collect_shell_env(&context)` runs every matching
  `shell_env` hook synchronously, captures stdout, parses it, returns
  the merged map. Later hooks override earlier ones.
* `parse_env_lines` tolerates `export KEY=VAL`, quoted values
  (`"…"` / `'…'`), comments (`#`), blank lines. Lines without `=` are
  silently dropped — easier than failing the whole hook for one stray
  human-friendly line. Values are taken verbatim; we don't run the
  string through a shell to avoid expansion surprises.
* Resolved KEY names (NEVER values) are written to
  `~/.deepseek/audit.log` so a session can be reconciled later
  without leaking the secret material.
* Hook failure / timeout contributes no vars — `exec_shell` is never
  aborted because of a misbehaving env hook.

Plumbing:
* `RuntimeToolServices` gains an optional
  `Arc<HookExecutor>`. Wired in `tui/ui.rs` from the App's existing
  `app.hooks` clone. Test contexts default to `None`.
* `ShellManager::execute_with_options_env` and
  `execute_interactive_with_policy_env` are new variants that accept
  an `extra_env: HashMap<String, String>` and forward it via
  `CommandSpec::with_env` so `prepare()` carries it into `ExecEnv.env`.
* The original `execute_with_options` / `execute_interactive_with_policy`
  call the new variants with an empty map so existing callers
  (including all 5 internal call sites) keep working unchanged.
* `commands/hooks.rs` `event_label` covers the new variant.

Tests cover `parse_env_lines` against realistic hook output (bare
assignments, `export` prefix, quoted values, comments, blanks, malformed
lines). `cargo clippy --workspace --all-targets --all-features --locked --
-D warnings` clean.

`config.example.toml` documents the new event with an `aws-vault`
example and the audit-logging contract.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 02:52:20 -05:00
Hunter Bown 0047b3225b feat(runtime-api): daemon API quartet for whalescale (#561 #562 #563 #564) (#567)
Bridge work to unblock whalescale-desktop's Settings/Composer/Archived-chats
flows without requiring a daemon recompile per dev-port or client-side
aggregation.

#561 / whalescale#255 — CORS allow-list configurable
* Add `[runtime_api] cors_origins` config field, `--cors-origin URL`
  (repeatable) flag on `deepseek serve --http`, and `DEEPSEEK_CORS_ORIGINS`
  env var. User entries stack on top of the built-in defaults
  (localhost:3000, localhost:1420, tauri://localhost). Resolution preserves
  first-seen order and drops empty/duplicate values; invalid HeaderValues
  log a warning and are skipped.
* Refactor `cors_layer()` to read merged origins from `RuntimeApiState`.

#562 / whalescale#256 — `PATCH /v1/threads/{id}` accepts the full editable
field set
* Extend `UpdateThreadRequest` with `allow_shell`, `trust_mode`,
  `auto_approve`, `model`, `mode`, `title`, `system_prompt`. Each is
  optional; missing means no change. Empty-string clears `title`/
  `system_prompt`. Empty `model`/`mode` rejected with 400.
* Add `title: Option<String>` to `ThreadRecord` (additive, no schema bump
  per documented criteria — old readers ignore the field without
  misinterpretation). `list_threads_summary` now returns the user-set title
  when present, falling back to the derived input-summary title.
* `thread.updated` event payload now carries a `changes` map with only the
  fields that actually changed.

#563 / whalescale#260 — list-archived-only filter
* New `archived_only=true` query param on `GET /v1/threads` and
  `GET /v1/threads/summary`. Backed by a new `ThreadListFilter` enum
  (`ActiveOnly` | `IncludeArchived` | `ArchivedOnly`). `archived_only`
  takes precedence over `include_archived`. Default behavior unchanged.

#564 / whalescale#261 — `GET /v1/usage` aggregation
* New `RuntimeThreadManager::aggregate_usage` walks all threads/turns,
  filters by inclusive `since`/`until` RFC 3339 bounds, accumulates token
  totals + cost (via `pricing::calculate_turn_cost_from_usage`), and
  groups by `day` (default), `model`, `provider`, or `thread`.
* New `GET /v1/usage` route. `since`/`until`/`group_by` query params,
  `since > until` and unknown `group_by` rejected with 400. Empty time
  ranges yield empty `buckets` (never 404).

5 new tests cover preflight Allow-Origin echoing for both default and
extra origins, the extended PATCH field set + clear-by-empty + 400 paths,
the archived_only filter on list + summary endpoints, and the
/v1/usage envelope + validation errors. Existing 13 runtime_api tests
continue to pass; the parity gates and full workspace test suite are clean.

`docs/RUNTIME_API.md` and `config.example.toml` updated to document the
new params, body shape, endpoint, and CORS knob.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 02:18:19 -05:00
Hunter Bown bda30b0fd6 Merge main into feat/v0.8.8-tui-polish + gemini-code-assist feedback
Resolves the post-#514/#517/#518 conflicts:

- CHANGELOG.md: kept both polish-stack and Linux ARM64 entries under
  [Unreleased]; reordered so the ARM64/install-message Changed/Docs
  sections precede the Releases footer.
- config.example.toml: kept both the `instructions = [...]` example
  and the `[memory]` opt-in stanza in sequence.
- crates/tui/src/config.rs: kept both `instructions_paths()` (#454)
  and `memory_enabled()` (#489) on the Config impl.
- crates/tui/src/prompts.rs: extended
  `system_prompt_for_mode_with_context_and_skills` to take BOTH
  `instructions: Option<&[PathBuf]>` and `user_memory_block:
  Option<&str>`. Section 2.5a renders instructions; 2.5b renders the
  memory block — both above the skills block so KV prefix caching
  still wins.
- crates/tui/src/core/engine.rs: thread both args through the two
  call sites.
- crates/tui/src/prompts.rs: update the `system_prompt_for_mode_with_context`
  forwarder and the test caller to pass `None` for the new arg.
- .gitignore: ignore `.claude/*.local.md` and `*.local.json` so
  local ralph / Claude-Code notes can't leak into commits.

Folds in two valid suggestions from the gemini-code-assist review on #519:

- `client.rs`: collapse the duplicated `LlmError → label` match and the
  `human_retry_reason` body into a single
  `retry_reason_label_and_human(err) -> (&'static str, String)` helper.
- `widgets/footer.rs::retry_banner_spans`: merge the two separate
  `match &props.retry` blocks into one that returns both `(label, color)`.

Behavior is unchanged; refactor is a pure DRY win.
2026-05-03 08:29:59 -05:00
Hunter Bown bef1895bed Merge pull request #518 from Hmbown/feat/489-memory-mvp
feat(memory): user-memory MVP — persistent notes, `# ` quick-add, /memory, remember tool (#489–#493)
2026-05-03 08:18:47 -05:00
Hunter Bown ac0c16996e feat(config): instructions array merged into system prompt (#454)
Adds a new optional `instructions = ["./AGENTS.md", "~/.deepseek/global.md"]`
config field that's loaded at startup and concatenated into the
system prompt, in declared order, above the skills block.

* `Config::instructions: Option<Vec<String>>` — raw paths from
  `~/.deepseek/config.toml` or the per-project overlay.
* `Config::instructions_paths()` — `expand_path` each entry,
  drop empties, return the resolved `Vec<PathBuf>`.
* `merge_project_config` — project's array replaces the
  user-level array wholesale (including `instructions = []` to
  clear the user list for the current repo). The typical "merge"
  pattern is for users who want both — they list `~/global.md`
  inside the project array.
* `EngineConfig::instructions: Vec<PathBuf>` — threaded from
  config through both engine entry points (`Engine::new` for
  Default and `refresh_system_prompt` for runtime swaps).
* `prompts::render_instructions_block(paths)` — loads each file
  in order, caps each at 100 KiB with a `[…elided]` marker on
  overflow, skips missing files with a tracing warning. Returns
  `None` when nothing renders so the caller appends nothing.
* `system_prompt_for_mode_with_context_and_skills` gains an
  `instructions: Option<&[PathBuf]>` parameter. Block lives
  between the project-context block and the skills block so it
  benefits from KV prefix caching and per-project overrides
  apply consistently turn-over-turn.

Documentation:
* `config.example.toml` documents the field, the wholesale-
  override semantics, and the size cap.

Tests:
* 5 new tests in `prompts.rs`: no-op for empty input, skip
  missing files, declared-order concatenation, skip empty files,
  truncate oversize files, plus an end-to-end test that the
  block appears in the assembled system prompt when configured.
2026-05-03 05:25:31 -05:00
Hunter Bown 7547d168a4 feat(memory): user-memory MVP — persistent notes, # quick-add, /memory, remember tool (#489–#493)
Adds a small, opt-in user-memory layer so the model has access to durable
preferences and conventions across sessions, and the user can dump quick
notes without leaving the TUI.

### What ships

- **Hierarchy loader** (#490): on every prompt assembly the engine reads
  `Config::memory_path()` (defaults to `~/.deepseek/memory.md`, override via
  `memory_path` in config or `DEEPSEEK_MEMORY_PATH`) and injects the file
  as a `<user_memory>` block alongside the existing `<project_instructions>`
  block. Goes above the volatile-content boundary so prefix-cache stays warm.
  Oversize files (>100 KiB) are truncated with a marker.
- **`# foo` composer quick-add** (#492): typing a single line that starts
  with `#` (but not `##` / `#!`) appends a timestamped bullet to the memory
  file and consumes the input — no turn fires. The composer status line
  surfaces the path that was written. Multi-`#` prefixes deliberately fall
  through so users can paste Markdown headings.
- **`/memory` slash command** (#491): `/memory` (or `/memory show`) prints
  the resolved path and contents inline; `/memory path`, `/memory clear`,
  and `/memory edit` (prints `${VISUAL:-${EDITOR:-vi}} <path>`) cover the
  rest of the manual-curation surface.
- **`remember` tool** (auto-update): model-callable tool that takes a
  `note` string and appends it as a bullet — the same persistence path as
  `# foo`. Auto-approved (writes only the user's own memory file). Only
  registered when memory is enabled, so it doesn't pollute the catalog when
  the feature is off.
- **Opt-in toggle** (#493): default behaviour is off. Enable with
  `[memory] enabled = true` in `config.toml` or `DEEPSEEK_MEMORY=on` in
  the environment.

### What's wired

- New `crates/tui/src/memory.rs` module (`load`, `as_system_block`,
  `compose_block`, `append_entry`).
- New `crates/tui/src/tools/remember.rs` (`RememberTool` + 3 tests).
- New `crates/tui/src/commands/memory.rs` (`memory(app, arg)` handler).
- `EngineConfig` gains `memory_enabled: bool` + `memory_path: PathBuf`.
- `ToolContext` gains `memory_path: Option<PathBuf>`.
- `App` exposes `memory_path` + `use_memory` from `AppOptions` (previously
  destructured-and-dropped); `main.rs` populates `use_memory` from
  `config.memory_enabled()`.
- `system_prompt_for_mode_with_context_and_skills` accepts an optional
  `user_memory_block` parameter; the engine computes it via
  `memory::compose_block(...)` and threads it through.
- Composer Enter handler intercepts `# foo` only when
  `config.memory_enabled()` is true; otherwise falls through to existing
  turn-submission path.
- `MemoryConfig` table (`[memory] enabled`) added to `Config`, surfaced
  in `config.example.toml`, plumbed through `merge_config`.

### Tests

- 8 unit tests in `memory::tests` covering `load` (missing / whitespace /
  real), `as_system_block` (xml shape, empty input, oversize truncation),
  and `append_entry` (creation, repeated append, empty-after-strip rejection).
- 3 unit tests in `tools::remember::tests` covering disabled-state error,
  successful append, and missing-`note`-field validation.

### Verification

cargo fmt --all -- --check                                          ✓
cargo clippy --workspace --all-targets --all-features --locked --   -D warnings   ✓
cargo test --workspace --all-features --locked                      ✓ (1821 + supporting; was 1809 on main)

Closes #490 #491 #492 #493
Refines #489 (EPIC parent — phase-1 MVP delivered; phase-2 items
494–497 stay on the v0.9.0 board)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:51:17 -05:00
Hunter Bown 3013a54c78 feat(tui): emit OSC 8 hyperlinks so URLs are Cmd+click-openable (#498)
Modern terminals (iTerm2, Terminal.app 13+, Ghostty, Kitty, WezTerm,
Alacritty, recent gnome-terminal/konsole) make a URL clickable when it's
wrapped in:

    \x1b]8;;TARGET\x1b\\LABEL\x1b]8;;\x1b\\

Terminals that don't understand the sequence simply render the visible
LABEL and ignore the escape, so emitting OSC 8 is a strict UX upgrade
for supporting terminals and a no-op for the rest.

### What's wired

- New `crates/tui/src/tui/osc8.rs` module with `wrap_link(target, label)`,
  `strip_into(s, &mut out)`, and a process-wide `ENABLED` AtomicBool that
  defaults to `true`.
- `markdown_render::render_line_with_links` now wraps recognized URLs
  (`http(s)://…`) in OSC 8 when the runtime flag is on. Display width is
  computed from the bare URL — the escapes are zero-width on supporting
  terminals.
- `ui_text::line_to_string` and `line_to_plain` strip OSC 8 wrappers when
  the span content contains an escape, so selection / clipboard output
  carries clean URLs and not the raw escape codes.
- `[tui] osc8_links: bool` config (default `true`) added to `TuiConfig`,
  documented in `docs/CONFIGURATION.md`, and surfaced in
  `config.example.toml`. `run_tui` applies it at startup.

### Tests

- 7 unit tests in `osc8::tests` covering wrap, strip-with-ESC-terminator,
  strip-with-BEL-terminator, plain passthrough, mixed escapes, default
  state, and round-trip set/unset.
- 2 markdown_render tests proving URLs in paragraph blocks emit the OSC 8
  wrapper when enabled and emit plain text when disabled.
- 2 ui_text tests proving `line_to_plain` strips OSC 8 wrappers from spans
  and passes plain spans through unchanged.

Tests that touch the global ENABLED flag serialize through a static
Mutex inside the test module so cargo's parallel runner can't observe a
torn read.

### Verification

cargo fmt --all -- --check                                          ✓
cargo clippy --workspace --all-targets --all-features --locked --   -D warnings   ✓
cargo test --workspace --all-features --locked                      ✓ (1820 + supporting; was 1809)

Closes #498

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 02:13:14 -05:00
Hunter Bown ad8064b143 chore(v0.8.8): stabilization batch — sub-agent caps, mutex contention, RLM polish, CI cleanup
Bundles the v0.8.8 stabilization fixes that were already implemented in the
working tree, plus the workflow/doc reconciliation called out in #507.

### Sub-agent runtime fixes
- **#509** Default sub-agent cap raised to 10 (configurable via
  `[subagents].max_concurrent` in `config.toml`, hard ceiling 20). The
  running-count calculation now ignores non-running, no-handle, and finished
  handles so completed agents stop counting against the cap.
- **#510** `SharedSubAgentManager` is now `Arc<RwLock<...>>`; the read paths
  that previously held a `Mutex` for inspection now take a read lock,
  eliminating the multi-agent fan-out UI freeze.
- **#511** `compact_tool_result_for_context` summarizes `agent_result` /
  `agent_wait` payloads before they are folded into the parent context.
- **#512** RLM tool cards map to `ToolFamily::Rlm` and render `rlm`, not
  `swarm`. Stale "swarm" wording cleaned in docs/comments/tests.
- **#513** (foreground stopgap only) Foreground RLM work is visible in the
  Agents sidebar projection. Full async RLM lifecycle remains v0.8.9 — the
  issue stays open with a refined scope.

### TUI / UX fixes
- **#487** Offline composer queue is now session-scoped; legacy unscoped
  queues fail closed.
- **#488** Composer Option+Backspace deletes by word; cross-platform key
  routing helpers added.
- **#443/#444** Keyboard enhancement flags pop on normal AND panic exit; the
  raw-mode startup probe is now bounded by a configurable timeout.
- **#449** Production footer reads statusline colors from `app.ui_theme`
  rather than the bespoke palette.
- **#506** `display_path_with_home` no longer mutates `HOME` in tests; the
  flake on shared-env CI is gone.

### Self-update / packaging
- **#503** `update.rs` arch mapping uses release-asset naming (`arm64`/`x64`)
  instead of the raw Rust constants. The platform-asset selector also rejects
  `.sha256` siblings as primary binaries. Tests now live alongside the source
  in `mod tests` (the `#[path]`-based integration test was removed because it
  duplicated test runs and forced a `pub(crate)` helper that no real caller
  used).
- **`Max 5 in flight` wording updated** in `agent_spawn` description,
  `prompts/base.md`, and `docs/TOOL_SURFACE.md` so the model sees the real
  default cap (10) and the configuration knob name.

### CI / release docs (#507)
- Pruned three duplicated/dead workflows: `crates-publish.yml`, `parity.yml`,
  `publish-npm.yml`. Their gates already run in `ci.yml` for every push/PR.
- `release.yml` build job now allows `parity` to be skipped (it only runs on
  tag push), unblocking `workflow_dispatch` reruns. The job still fails
  closed on a real parity failure.
- `RELEASE_RUNBOOK.md` reconciled: crate publishing is documented as the
  manual `scripts/release/publish-crates.sh` flow (no automated workflow);
  references to the deleted workflows removed.
- `CLAUDE.md` notes the `RELEASE_TAG_PAT` requirement for the auto-tag →
  release.yml chain (without it, the tag is created but `release.yml` does
  not fire) and documents the `workflow_dispatch` parity-skip behavior.

### Docs
- `docs/COMPETITIVE_ANALYSIS.md` added — capability matrix vs OpenCode and
  Codex CLI, gap analysis, and recommended implementation order.

### Verification (this branch)
- `cargo fmt --all -- --check` ✓
- `cargo check --workspace --all-targets --locked` ✓
- `cargo clippy --workspace --all-targets --all-features --locked -- -D warnings` ✓
- `cargo test --workspace --all-features --locked` ✓ (1809 + supporting)
- Parity gates ✓ (snapshot, parity_protocol, parity_state)
- `cargo build --release --locked -p deepseek-tui-cli -p deepseek-tui` ✓
- Lockfile drift guard ✓
- `deepseek doctor --json` clean
- `deepseek eval` (offline harness) success=true, 0 tool errors

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 01:57:37 -05:00
Hunter Bown 7125172f67 fix(tui): tighten selection and live task panels 2026-05-02 21:05:15 -05:00
Hunter Bown c2b2c284f6 release: v0.7.5 — token-basis fixes, shell timeout recovery, context/cache policy
Issues #202, #203, #204, #205:

- Cycle/seam triggers use active request input size + response
  headroom reserve, not lifetime cumulative API usage.
- V4 hard-cycle headroom calibrated around fixed TURN_MAX_OUTPUT_TOKENS
  plus CONTEXT_HEADROOM_TOKENS safety buffer.
- /tokens, /cost, footer/header labels, and docs now separate
  active context, turn telemetry, cumulative usage, cache hit/miss,
  context percent, and cost.
- Foreground exec_shell timeout output tells the model the process
  was killed and suggests task_shell_start or background exec_shell
  plus poll/wait.
- Added regression tests for active-token basis, V4 headroom,
  seam trigger basis, footer label behavior, and shell timeout
  recovery metadata.
- Preserved #200/#201 policy: V4 default is append-only,
  prefix-cache preserving; replacement compaction, Flash seams,
  and capacity intervention remain opt-in.
2026-04-29 10:13:27 -05:00
Hunter Bown 41e8f2b5b2 Disable default compaction and opt in context seams 2026-04-29 09:12:20 -05:00
Hunter Bown 6d8ab4c2b8 fix: close v0.7.2 issue cleanup 2026-04-28 23:09:19 -05:00
Hunter Bown 27527699db Merge branch 'feat/v070-lsp' (#136 LSP diagnostics)
# Conflicts:
#	config.example.toml
#	crates/config/src/lib.rs
#	crates/tui/src/config.rs
#	crates/tui/src/core/engine.rs
#	crates/tui/src/main.rs
#	crates/tui/src/runtime_threads.rs
#	crates/tui/src/tui/ui.rs
2026-04-28 01:03:36 -05:00
Hunter Bown 41daab3ca0 Merge branch 'feat/v070-snapshots' (#137 side-git snapshots)
# Conflicts:
#	crates/config/src/lib.rs
#	crates/tui/src/config.rs
2026-04-28 00:58:16 -05:00
Hunter Bown 0781b7c203 feat(session): #137 prune stale workspace snapshots at session boot
`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>
2026-04-28 00:31:57 -05:00
Hunter Bown 03dcc74833 docs(skills): #140 document /skill install + [skills] config section
- 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>
2026-04-28 00:30:10 -05:00
Hunter Bown 05a1032e00 feat(lsp): #136 post-edit diagnostics injection
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>
2026-04-28 00:29:28 -05:00
Hunter Bown 87ac2e9bdc Merge branch 'feat/v070-network' (#135 egress network policy + audit)
- crates/tui/src/network_policy.rs (NEW, ~300 LOC) — NetworkPolicy/NetworkPolicyDecider/NetworkAuditor
- crates/tui/src/tools/{fetch_url,web_search,spec}.rs + mcp.rs — gate before egress
- crates/tui/src/core/engine.rs + runtime_threads.rs + tui/ui.rs — surface NetworkDenied
- crates/config/src/lib.rs + tui/src/config.rs + config.example.toml — [network] schema
- Subdomain-prefix matching with deny-wins precedence
- Audit format: <RFC3339> network <host> <tool> <decision>

# Conflicts:
#	crates/tui/src/config.rs
2026-04-28 00:07:42 -05:00
Hunter Bown a054789f79 Merge branch 'feat/v070-osc9' (#132 OSC 9 desktop notification on long-turn completion)
- crates/tui/src/tui/notifications.rs (NEW) — Method enum {Auto/Osc9/Bel/Off}, notify_done()
- crates/tui/src/tui/mod.rs + ui.rs — register module + hook EngineEvent::TurnComplete
- crates/tui/src/config.rs — NotificationsConfig (method/threshold_secs/include_summary)
- config.example.toml — [notifications] section
- 9 unit tests including tmux DCS passthrough wrapping
2026-04-28 00:06:26 -05:00
Hunter Bown abbb86cdd2 feat(network): #135 add [network] config schema for policy
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.
2026-04-28 00:02:34 -05:00
Hunter Bown 6432d47c53 feat: #132 emit OSC 9 / BEL desktop notification on long turn completion
Adds crates/tui/src/tui/notifications.rs with Method enum (Auto/Osc9/Bel/Off),
notify_done / notify_done_to helpers, tmux DCS passthrough, and 9 unit tests.
Wires the hook at the TurnComplete event in tui/ui.rs so turns >= threshold_secs
(default 30 s) emit an escape to stdout; method auto-detects iTerm.app/Ghostty/
WezTerm for OSC 9 and falls back to BEL. Config exposed under [notifications] in
config.toml and documented in config.example.toml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-27 23:52:58 -05:00