From b82131518d4d7bdd9871a102b48742aeb00be73e Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:18:42 -0500 Subject: [PATCH 01/16] chore(release): prepare v0.8.45 flash release --- CHANGELOG.md | 43 +++++++++++++++- Cargo.lock | 28 +++++------ Cargo.toml | 2 +- README.md | 95 +++++++++++++++++++++++++++++++---- crates/agent/Cargo.toml | 2 +- crates/app-server/Cargo.toml | 18 +++---- crates/cli/Cargo.toml | 14 +++--- crates/config/Cargo.toml | 2 +- crates/core/Cargo.toml | 16 +++--- crates/execpolicy/Cargo.toml | 2 +- crates/hooks/Cargo.toml | 2 +- crates/tools/Cargo.toml | 2 +- crates/tui/CHANGELOG.md | 43 +++++++++++++++- crates/tui/Cargo.toml | 6 +-- docs/CNB_MIRROR.md | 23 +++++++++ docs/REBRAND.md | 59 ++++++++++++++++++---- npm/codewhale/package.json | 4 +- npm/deepseek-tui/package.json | 2 +- 18 files changed, 292 insertions(+), 71 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6fda0e46..149bada57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.45] - 2026-05-25 + +### Added + +- **`/balance` command scaffold.** `codewhale /balance` now has an honest + placeholder command surface for the provider-billing work, instead of + silently pretending balance lookup exists before the provider capability + layer is ready (#2035). +- **Deterministic sub-agent nicknames.** Sub-agents now receive stable + whale-species names in the TUI while preserving the raw agent id in the + detail popup, making parallel child work easier to scan (#2035). + +### Changed + +- **Migration FAQ for DeepSeek-TUI users.** README and rebrand docs now spell + out the new Cargo install commands, how to uninstall old `deepseek-tui` / + `deepseek-tui-cli` packages, which `.deepseek` files are safe to clean up, + and how `~/.codewhale/` / `.codewhale/` take over without breaking legacy + `DEEPSEEK_*` env vars. +- **CNB automation guidance is gated.** The CNB mirror docs now distinguish + trusted push/web/API-triggered pipelines from untrusted issue, PR, and NPC + comment events, and recommend using CodeWhale automation to open PRs rather + than recursively self-merging release changes. + +### Fixed + +- **User messages stand out in the transcript.** The TUI now gives user turns + a clearer visual treatment so request/answer boundaries are easier to see + (#1995, closes #1672). Thanks @reidliu41 for the focused fix and + @lpeng1711694086-lang for the original report. +- **`SKILL.md` YAML block scalars parse correctly.** Skill descriptions using + YAML `|` or `>` block scalar syntax now reach `/skills`, `load_skill`, and + model prompt context as the intended description text (#1908, closes #1907). + Thanks @zlh124 for the parser-level repro, implementation, and tests. +- **Large file walks are easier to interrupt.** `file_search` traversal is + cancellable, and `list_dir` runs behind a blocking worker with a timeout so + stop/cancel requests are not stuck behind long synchronous filesystem walks + (#2035). Thanks @h3c-hexin for the overlapping cancellation report and + implementation notes in #2044. + ## [0.8.44] - 2026-05-24 ### Added @@ -4806,7 +4846,8 @@ Welcome — and thank you. - Hooks system and config profiles - Example skills and launch assets -[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.8.44...HEAD +[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.8.45...HEAD +[0.8.45]: https://github.com/Hmbown/CodeWhale/compare/v0.8.44...v0.8.45 [0.8.44]: https://github.com/Hmbown/CodeWhale/compare/v0.8.43...v0.8.44 [0.8.43]: https://github.com/Hmbown/CodeWhale/compare/v0.8.42...v0.8.43 [0.8.42]: https://github.com/Hmbown/CodeWhale/compare/v0.8.41...v0.8.42 diff --git a/Cargo.lock b/Cargo.lock index 2d5bd8e14..bde105481 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,7 +803,7 @@ checksum = "e9b18233253483ce2f65329a24072ec414db782531bdbb7d0bbc4bd2ce6b7e21" [[package]] name = "codewhale-agent" -version = "0.8.44" +version = "0.8.45" dependencies = [ "codewhale-config", "serde", @@ -811,7 +811,7 @@ dependencies = [ [[package]] name = "codewhale-app-server" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "axum", @@ -833,7 +833,7 @@ dependencies = [ [[package]] name = "codewhale-cli" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "chrono", @@ -858,7 +858,7 @@ dependencies = [ [[package]] name = "codewhale-config" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "codewhale-secrets", @@ -870,7 +870,7 @@ dependencies = [ [[package]] name = "codewhale-core" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "chrono", @@ -888,7 +888,7 @@ dependencies = [ [[package]] name = "codewhale-execpolicy" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "codewhale-protocol", @@ -897,7 +897,7 @@ dependencies = [ [[package]] name = "codewhale-hooks" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "async-trait", @@ -911,7 +911,7 @@ dependencies = [ [[package]] name = "codewhale-mcp" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "serde", @@ -920,7 +920,7 @@ dependencies = [ [[package]] name = "codewhale-protocol" -version = "0.8.44" +version = "0.8.45" dependencies = [ "serde", "serde_json", @@ -928,7 +928,7 @@ dependencies = [ [[package]] name = "codewhale-secrets" -version = "0.8.44" +version = "0.8.45" dependencies = [ "dirs", "keyring", @@ -941,7 +941,7 @@ dependencies = [ [[package]] name = "codewhale-state" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "chrono", @@ -953,7 +953,7 @@ dependencies = [ [[package]] name = "codewhale-tools" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "async-trait", @@ -966,7 +966,7 @@ dependencies = [ [[package]] name = "codewhale-tui" -version = "0.8.44" +version = "0.8.45" dependencies = [ "anyhow", "arboard", @@ -1032,7 +1032,7 @@ dependencies = [ [[package]] name = "codewhale-tui-core" -version = "0.8.44" +version = "0.8.45" [[package]] name = "colorchoice" diff --git a/Cargo.toml b/Cargo.toml index cee78462f..78d560a0c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ default-members = ["crates/cli", "crates/app-server", "crates/tui"] resolver = "2" [workspace.package] -version = "0.8.44" +version = "0.8.45" edition = "2024" # Rust 1.88 stabilized `let_chains` in `if`/`while` conditions, which the # codebase relies on extensively. Cargo enforces this so users on older diff --git a/README.md b/README.md index 58975408d..34bf51f87 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ [简体中文 README](README.zh-CN.md) [日本語 README](README.ja-JP.md) -[Install](#install) · [Quickstart](#quickstart) · [Usage](#usage) · [Documentation](#documentation) · [Contributing](#contributing) · [Support](#support) +[Install](#install) · [Quickstart](#quickstart) · [Usage](#usage) · [FAQ](#faq) · [Documentation](#documentation) · [Contributing](#contributing) · [Support](#support) ## Install @@ -44,7 +44,7 @@ brew install deepseek-tui docker volume create codewhale-home docker run --rm -it \ -e DEEPSEEK_API_KEY="$DEEPSEEK_API_KEY" \ - -v codewhale-home:/home/codewhale/.deepseek \ + -v codewhale-home:/home/codewhale \ -v "$PWD:/workspace" \ -w /workspace \ ghcr.io/hmbown/codewhale:latest @@ -69,6 +69,19 @@ cargo install codewhale-cli --locked --force cargo install codewhale-tui --locked --force ``` +Coming from the old DeepSeek-TUI Cargo install path? Reinstall under the new +crate names and remove the legacy packages when you are ready: + +```bash +cargo uninstall deepseek-tui-cli deepseek-tui 2>/dev/null || true +cargo install codewhale-cli --locked --force +cargo install codewhale-tui --locked --force +``` + +The `deepseek` and `deepseek-tui` commands remain compatibility shims through +the v0.8.x line, but new installs and docs use `codewhale` and +`codewhale-tui`. + ![codewhale screenshot](assets/screenshot.png) --- @@ -135,12 +148,12 @@ codewhale --model auto Prebuilt binaries are published for **Linux x64**, **Linux ARM64** (v0.8.8+), **macOS x64**, **macOS ARM64**, and **Windows x64**. For other targets (musl, riscv64, FreeBSD, etc.), see [Install from source](#install-from-source) or [docs/INSTALL.md](docs/INSTALL.md). -On first launch you'll be prompted for your [DeepSeek API key](https://platform.deepseek.com/api_keys). The key is saved to `~/.deepseek/config.toml` so it works from any directory without OS credential prompts. +On first launch you'll be prompted for your [DeepSeek API key](https://platform.deepseek.com/api_keys). The key is saved to `~/.codewhale/config.toml` so it works from any directory without OS credential prompts. Existing `~/.deepseek/config.toml` files are still read and copied forward when it is safe to do so. You can also set it ahead of time: ```bash -codewhale auth set --provider deepseek # saves to ~/.deepseek/config.toml +codewhale auth set --provider deepseek # saves to ~/.codewhale/config.toml codewhale auth status # shows the active credential source export DEEPSEEK_API_KEY="YOUR_KEY" # env var alternative; use ~/.zshenv for non-interactive shells @@ -362,7 +375,7 @@ docker volume create codewhale-home docker run --rm -it \ -e DEEPSEEK_API_KEY="$DEEPSEEK_API_KEY" \ - -v codewhale-home:/home/codewhale/.deepseek \ + -v codewhale-home:/home/codewhale \ -v "$PWD:/workspace" \ -w /workspace \ ghcr.io/hmbown/codewhale:latest @@ -456,7 +469,13 @@ meaning of "auto". ## Configuration -User config: `~/.deepseek/config.toml`. Project overlay: `/.deepseek/config.toml` (denied: `api_key`, `base_url`, `provider`, `mcp_config_path`). [config.example.toml](config.example.toml) has every option. +User config: `~/.codewhale/config.toml`. Legacy `~/.deepseek/config.toml` +continues to work as a fallback and is copied into the CodeWhale home when the +new config does not exist. Project overlay: +`/.codewhale/config.toml`, with legacy +`/.deepseek/config.toml` still read for older repositories. Project +overlays cannot set `api_key`, `base_url`, `provider`, or `mcp_config_path`. +[config.example.toml](config.example.toml) has every option. Key environment variables: @@ -509,7 +528,7 @@ Legacy aliases `deepseek-chat` / `deepseek-reasoner` map to `deepseek-v4-flash` ## Publishing Your Own Skill -codewhale discovers skills from workspace directories (`.agents/skills` → `skills` → `.opencode/skills` → `.claude/skills` → `.cursor/skills`) and global directories (`~/.agents/skills` → `~/.claude/skills` → `~/.deepseek/skills`). Each skill is a directory with a `SKILL.md` file: +codewhale discovers skills from workspace directories (`.agents/skills` → `skills` → `.opencode/skills` → `.claude/skills` → `.cursor/skills` → `.codewhale/skills` → `.deepseek/skills`) and global directories (`~/.agents/skills` → `~/.claude/skills` → `~/.codewhale/skills` → `~/.deepseek/skills`). Each skill is a directory with a `SKILL.md` file: ```text ~/.agents/skills/my-skill/ @@ -534,8 +553,66 @@ First launch also installs bundled system skills for common workflows: `skill-creator`, `delegate`, `v4-best-practices`, `plugin-creator`, `skill-installer`, `mcp-builder`, `documents`, `presentations`, `spreadsheets`, `pdf`, and `feishu`. These live under -`~/.deepseek/skills` and are versioned so new bundles are added on upgrade -without recreating skills the user deliberately deleted. +`~/.codewhale/skills` for new installs. Legacy `~/.deepseek/skills` remains +discoverable so existing custom skills do not disappear during the rename. + +--- + +## FAQ + +### I used `cargo install deepseek-tui --locked`. Do I need to reinstall? + +Yes. The old packages were kept as compatibility shims during the rename, but +new Cargo installs should use the CodeWhale crate names: + +```bash +cargo uninstall deepseek-tui-cli deepseek-tui 2>/dev/null || true +cargo install codewhale-cli --locked --force +cargo install codewhale-tui --locked --force +codewhale --version +``` + +Both Cargo packages matter: `codewhale-cli` provides the `codewhale` dispatcher, +and `codewhale-tui` provides the interactive runtime that the dispatcher starts. + +### Can I delete old `.deepseek` files from my repos? + +Usually, but inspect them first. Copy project config from +`.deepseek/config.toml` to `.codewhale/config.toml` before removing the legacy +file, and move project skills to `.codewhale/skills` or `.agents/skills` when +you want them to remain active. Generated scratch directories such as +`.deepseek/pastes`, +`.deepseek/snapshots`, and old handoff files can be removed once you no longer +need those drafts or rollback snapshots. + +For private local agent state, add these to your repository's `.gitignore`: + +```gitignore +.codewhale/ +.deepseek/ +``` + +Do not delete global `~/.deepseek/` blindly. It may contain sessions, skills, +MCP config, memory, or older logs you still want. Run `codewhale doctor` and +`codewhale auth status` after upgrading; once the new `~/.codewhale/` state is +working, archive or remove only the legacy files you have verified are no +longer needed. + +### Do `DEEPSEEK_*` environment variables still work? + +Yes. CodeWhale is still DeepSeek-first, and renaming `DEEPSEEK_API_KEY`, +`DEEPSEEK_BASE_URL`, or existing provider/model variables would break working +shell profiles. Keep those variables as-is unless you are deliberately cleaning +up your own shell config. + +### Can CNB or a CodeWhale NPC update CodeWhale automatically? + +CNB supports push/tag pipelines, manual `web_trigger` deploy buttons, API +triggers, issue/PR comment events, and NPC comment events. That makes a +CodeWhale-assisted update loop possible, but it should stay gated: the safe +shape is "open or update a PR, run release checks, then let a maintainer merge +and tag." Do not give untrusted issue, PR, or NPC comment events release +secrets or permission to merge to `main`. --- diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index c6d4fd3f9..dc402892f 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -7,5 +7,5 @@ repository.workspace = true description = "Model/provider registry and fallback strategy for DeepSeek workspace architecture" [dependencies] -codewhale-config = { path = "../config", version = "0.8.44" } +codewhale-config = { path = "../config", version = "0.8.45" } serde.workspace = true diff --git a/crates/app-server/Cargo.toml b/crates/app-server/Cargo.toml index dc87c8872..a25fa7513 100644 --- a/crates/app-server/Cargo.toml +++ b/crates/app-server/Cargo.toml @@ -10,15 +10,15 @@ description = "Codex-style app-server transport for DeepSeek workspace architect anyhow.workspace = true axum.workspace = true clap.workspace = true -codewhale-agent = { path = "../agent", version = "0.8.44" } -codewhale-config = { path = "../config", version = "0.8.44" } -codewhale-core = { path = "../core", version = "0.8.44" } -codewhale-execpolicy = { path = "../execpolicy", version = "0.8.44" } -codewhale-hooks = { path = "../hooks", version = "0.8.44" } -codewhale-mcp = { path = "../mcp", version = "0.8.44" } -codewhale-protocol = { path = "../protocol", version = "0.8.44" } -codewhale-state = { path = "../state", version = "0.8.44" } -codewhale-tools = { path = "../tools", version = "0.8.44" } +codewhale-agent = { path = "../agent", version = "0.8.45" } +codewhale-config = { path = "../config", version = "0.8.45" } +codewhale-core = { path = "../core", version = "0.8.45" } +codewhale-execpolicy = { path = "../execpolicy", version = "0.8.45" } +codewhale-hooks = { path = "../hooks", version = "0.8.45" } +codewhale-mcp = { path = "../mcp", version = "0.8.45" } +codewhale-protocol = { path = "../protocol", version = "0.8.45" } +codewhale-state = { path = "../state", version = "0.8.45" } +codewhale-tools = { path = "../tools", version = "0.8.45" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 87ef1e74b..1355ac046 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -25,13 +25,13 @@ path = "src/bin/deepseek_legacy_shim.rs" anyhow.workspace = true clap.workspace = true clap_complete.workspace = true -codewhale-agent = { path = "../agent", version = "0.8.44" } -codewhale-app-server = { path = "../app-server", version = "0.8.44" } -codewhale-config = { path = "../config", version = "0.8.44" } -codewhale-execpolicy = { path = "../execpolicy", version = "0.8.44" } -codewhale-mcp = { path = "../mcp", version = "0.8.44" } -codewhale-secrets = { path = "../secrets", version = "0.8.44" } -codewhale-state = { path = "../state", version = "0.8.44" } +codewhale-agent = { path = "../agent", version = "0.8.45" } +codewhale-app-server = { path = "../app-server", version = "0.8.45" } +codewhale-config = { path = "../config", version = "0.8.45" } +codewhale-execpolicy = { path = "../execpolicy", version = "0.8.45" } +codewhale-mcp = { path = "../mcp", version = "0.8.45" } +codewhale-secrets = { path = "../secrets", version = "0.8.45" } +codewhale-state = { path = "../state", version = "0.8.45" } chrono.workspace = true dirs.workspace = true serde.workspace = true diff --git a/crates/config/Cargo.toml b/crates/config/Cargo.toml index 2d9ea5228..c9e31d0b2 100644 --- a/crates/config/Cargo.toml +++ b/crates/config/Cargo.toml @@ -8,7 +8,7 @@ description = "Config schema and precedence model for DeepSeek workspace archite [dependencies] anyhow.workspace = true -codewhale-secrets = { path = "../secrets", version = "0.8.44" } +codewhale-secrets = { path = "../secrets", version = "0.8.45" } dirs.workspace = true serde.workspace = true toml.workspace = true diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index debdf425c..c9d602f4a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -9,13 +9,13 @@ description = "Core runtime boundaries for DeepSeek workspace architecture" [dependencies] anyhow.workspace = true chrono.workspace = true -codewhale-agent = { path = "../agent", version = "0.8.44" } -codewhale-config = { path = "../config", version = "0.8.44" } -codewhale-execpolicy = { path = "../execpolicy", version = "0.8.44" } -codewhale-hooks = { path = "../hooks", version = "0.8.44" } -codewhale-mcp = { path = "../mcp", version = "0.8.44" } -codewhale-protocol = { path = "../protocol", version = "0.8.44" } -codewhale-state = { path = "../state", version = "0.8.44" } -codewhale-tools = { path = "../tools", version = "0.8.44" } +codewhale-agent = { path = "../agent", version = "0.8.45" } +codewhale-config = { path = "../config", version = "0.8.45" } +codewhale-execpolicy = { path = "../execpolicy", version = "0.8.45" } +codewhale-hooks = { path = "../hooks", version = "0.8.45" } +codewhale-mcp = { path = "../mcp", version = "0.8.45" } +codewhale-protocol = { path = "../protocol", version = "0.8.45" } +codewhale-state = { path = "../state", version = "0.8.45" } +codewhale-tools = { path = "../tools", version = "0.8.45" } serde_json.workspace = true uuid.workspace = true diff --git a/crates/execpolicy/Cargo.toml b/crates/execpolicy/Cargo.toml index 669759c49..16b09697d 100644 --- a/crates/execpolicy/Cargo.toml +++ b/crates/execpolicy/Cargo.toml @@ -8,5 +8,5 @@ description = "Execution policy and approval model parity for DeepSeek workspace [dependencies] anyhow.workspace = true -codewhale-protocol = { path = "../protocol", version = "0.8.44" } +codewhale-protocol = { path = "../protocol", version = "0.8.45" } serde.workspace = true diff --git a/crates/hooks/Cargo.toml b/crates/hooks/Cargo.toml index a39dc18fd..4f657cd0a 100644 --- a/crates/hooks/Cargo.toml +++ b/crates/hooks/Cargo.toml @@ -10,7 +10,7 @@ description = "Hook dispatch and notifications parity for DeepSeek workspace arc anyhow.workspace = true async-trait.workspace = true chrono.workspace = true -codewhale-protocol = { path = "../protocol", version = "0.8.44" } +codewhale-protocol = { path = "../protocol", version = "0.8.45" } reqwest.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/tools/Cargo.toml b/crates/tools/Cargo.toml index 9059c3444..464ce47e4 100644 --- a/crates/tools/Cargo.toml +++ b/crates/tools/Cargo.toml @@ -9,7 +9,7 @@ description = "Tool invocation lifecycle, schema validation, and scheduler paral [dependencies] anyhow.workspace = true async-trait.workspace = true -codewhale-protocol = { path = "../protocol", version = "0.8.44" } +codewhale-protocol = { path = "../protocol", version = "0.8.45" } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index f6fda0e46..149bada57 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.8.45] - 2026-05-25 + +### Added + +- **`/balance` command scaffold.** `codewhale /balance` now has an honest + placeholder command surface for the provider-billing work, instead of + silently pretending balance lookup exists before the provider capability + layer is ready (#2035). +- **Deterministic sub-agent nicknames.** Sub-agents now receive stable + whale-species names in the TUI while preserving the raw agent id in the + detail popup, making parallel child work easier to scan (#2035). + +### Changed + +- **Migration FAQ for DeepSeek-TUI users.** README and rebrand docs now spell + out the new Cargo install commands, how to uninstall old `deepseek-tui` / + `deepseek-tui-cli` packages, which `.deepseek` files are safe to clean up, + and how `~/.codewhale/` / `.codewhale/` take over without breaking legacy + `DEEPSEEK_*` env vars. +- **CNB automation guidance is gated.** The CNB mirror docs now distinguish + trusted push/web/API-triggered pipelines from untrusted issue, PR, and NPC + comment events, and recommend using CodeWhale automation to open PRs rather + than recursively self-merging release changes. + +### Fixed + +- **User messages stand out in the transcript.** The TUI now gives user turns + a clearer visual treatment so request/answer boundaries are easier to see + (#1995, closes #1672). Thanks @reidliu41 for the focused fix and + @lpeng1711694086-lang for the original report. +- **`SKILL.md` YAML block scalars parse correctly.** Skill descriptions using + YAML `|` or `>` block scalar syntax now reach `/skills`, `load_skill`, and + model prompt context as the intended description text (#1908, closes #1907). + Thanks @zlh124 for the parser-level repro, implementation, and tests. +- **Large file walks are easier to interrupt.** `file_search` traversal is + cancellable, and `list_dir` runs behind a blocking worker with a timeout so + stop/cancel requests are not stuck behind long synchronous filesystem walks + (#2035). Thanks @h3c-hexin for the overlapping cancellation report and + implementation notes in #2044. + ## [0.8.44] - 2026-05-24 ### Added @@ -4806,7 +4846,8 @@ Welcome — and thank you. - Hooks system and config profiles - Example skills and launch assets -[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.8.44...HEAD +[Unreleased]: https://github.com/Hmbown/CodeWhale/compare/v0.8.45...HEAD +[0.8.45]: https://github.com/Hmbown/CodeWhale/compare/v0.8.44...v0.8.45 [0.8.44]: https://github.com/Hmbown/CodeWhale/compare/v0.8.43...v0.8.44 [0.8.43]: https://github.com/Hmbown/CodeWhale/compare/v0.8.42...v0.8.43 [0.8.42]: https://github.com/Hmbown/CodeWhale/compare/v0.8.41...v0.8.42 diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index f78b57a42..ffcb900e2 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -27,9 +27,9 @@ path = "src/bin/deepseek_tui_legacy_shim.rs" [dependencies] anyhow = "1.0.100" arboard = "3.4" -codewhale-config = { path = "../config", version = "0.8.44" } -codewhale-secrets = { path = "../secrets", version = "0.8.44" } -codewhale-tools = { path = "../tools", version = "0.8.44" } +codewhale-config = { path = "../config", version = "0.8.45" } +codewhale-secrets = { path = "../secrets", version = "0.8.45" } +codewhale-tools = { path = "../tools", version = "0.8.45" } schemaui = { version = "0.12.0", default-features = false, optional = true } async-stream = "0.3.6" async-trait = "0.1" diff --git a/docs/CNB_MIRROR.md b/docs/CNB_MIRROR.md index bf7acdfb4..978cde4f0 100644 --- a/docs/CNB_MIRROR.md +++ b/docs/CNB_MIRROR.md @@ -36,6 +36,29 @@ GitHub refs to CNB, so pipeline files created only on the CNB side will be overwritten. Submit `.cnb.yml` changes through GitHub PRs and let the one-way mirror carry them to CNB. +## Event triggers and NPC safety + +CNB can run pipelines from several event families: push and tag events, issue +events, PR events, scheduled tasks, page-triggered `web_trigger*` buttons, +API-triggered `api_trigger*` runs, and NPC mentions such as +`issue.comment@npc` and `pull_request.comment@npc`. + +That is enough to build a CodeWhale-assisted maintenance lane, but not an +ungated self-update loop. Treat issue, PR, and NPC comment events as untrusted: +do not expose release secrets, deploy keys, npm/crates credentials, or direct +`main` push rights to them. The safe shape is: + +1. A trusted `push`, `web_trigger*`, or protected `api_trigger*` pipeline runs + CodeWhale against a checked-out branch. +2. CodeWhale writes a normal branch with code/docs changes. +3. The pipeline opens or updates a PR and posts the verification summary. +4. A maintainer reviews, merges, tags, and publishes through the normal release + gates. + +For documentation on CNB trigger rules and NPC events, see + and +. + ## CNB tag releases When CNB receives a `v*` tag, the root `.cnb.yml` tag pipeline builds Linux x64 diff --git a/docs/REBRAND.md b/docs/REBRAND.md index 4ce7c9ba2..b7e9c29d5 100644 --- a/docs/REBRAND.md +++ b/docs/REBRAND.md @@ -13,19 +13,24 @@ npm uninstall -g deepseek-tui # or cargo uninstall deepseek-tui-cli deepsee # or brew uninstall deepseek-tui # 2. Install under the new name. -npm install -g codewhale # or cargo install codewhale-cli codewhale-tui --locked +npm install -g codewhale # or install both Cargo crates below # or brew install deepseek-tui (Homebrew tap still # uses the legacy name during the transition; # it installs the new binaries underneath.) +cargo install codewhale-cli --locked --force +cargo install codewhale-tui --locked --force + # 3. Run with the new command. codewhale doctor codewhale ``` -Your `~/.deepseek/config.toml`, `~/.deepseek/sessions/`, `~/.deepseek/skills/`, -`~/.deepseek/tasks/`, and `~/.deepseek/mcp.json` are untouched. Existing -`DEEPSEEK_*` environment variables continue to work. +New CodeWhale-owned state prefers `~/.codewhale/`. Existing +`~/.deepseek/config.toml`, `~/.deepseek/sessions/`, `~/.deepseek/skills/`, +`~/.deepseek/tasks/`, and `~/.deepseek/mcp.json` remain readable during the +transition so upgrades do not lose data. Existing `DEEPSEEK_*` environment +variables continue to work. ## What got renamed @@ -52,9 +57,11 @@ Anything that targets the DeepSeek provider API stays exactly as it was: aliases `deepseek-chat` and `deepseek-reasoner`. - **Hosts**: `api.deepseek.com` (global) and `api.deepseeki.com` (China fallback). -- **Config directory**: `~/.deepseek/`. Renaming this would invalidate - every existing install's saved API key, sessions, skills, MCP config, - and audit log. +- **DeepSeek provider environment variables**: the `DEEPSEEK_*` names remain + supported because they target the provider integration and existing shell + profiles. +- **Legacy data compatibility**: `~/.deepseek/` remains a readable fallback for + existing installs. New CodeWhale-owned state prefers `~/.codewhale/`. - **GitHub repository URL**: `https://github.com/Hmbown/CodeWhale`. The old `Hmbown/DeepSeek-TUI` URL redirects there during the transition. - **Homebrew tap and formula** (`Hmbown/homebrew-deepseek-tui`): still @@ -88,7 +95,8 @@ npm install -g codewhale ```bash cargo uninstall deepseek-tui-cli deepseek-tui 2>/dev/null || true -cargo install codewhale-cli codewhale-tui --locked +cargo install codewhale-cli --locked --force +cargo install codewhale-tui --locked --force ``` Or in a checkout: @@ -117,14 +125,45 @@ A second checksum manifest, `deepseek-artifacts-sha256.txt`, is attached as an alias of `codewhale-artifacts-sha256.txt` so v0.8.40's hardcoded lookup still verifies. +## Cleaning up old files + +Global `~/.deepseek/` may contain saved sessions, skills, MCP config, memory, +logs, tool-output spillover files, and older settings. Do not delete it as a +first upgrade step. Run: + +```bash +codewhale doctor +codewhale auth status +``` + +If `~/.codewhale/config.toml` now has the config you expect, you can archive or +remove only the legacy files you have verified are no longer needed. + +For repositories, prefer the new project overlay location: + +```bash +mkdir -p .codewhale +cp -n .deepseek/config.toml .codewhale/config.toml 2>/dev/null || true +``` + +Project-local generated scratch such as `.deepseek/pastes`, +`.deepseek/snapshots`, and old `.deepseek/handoff.md` files can be deleted once +you no longer need those drafts or rollback snapshots. If these directories are +private local agent state, add both names to `.gitignore`: + +```gitignore +.codewhale/ +.deepseek/ +``` + ## Why the name change CodeWhale is a shorter, terminal-friendlier handle for the same terminal coding agent and the longer-term product direction: a DeepSeek-first agentic terminal for open source and open-weight coding models. The project name, command names, package names, release assets, Docker image, and CNB mirror move -to CodeWhale; the official DeepSeek provider, model IDs, env vars, and -`~/.deepseek/` config surface remain first-class. +to CodeWhale; the official DeepSeek provider, model IDs, and env vars remain +first-class. ## Reporting issues with the rename diff --git a/npm/codewhale/package.json b/npm/codewhale/package.json index 3f6c3cb2c..c8a402f80 100644 --- a/npm/codewhale/package.json +++ b/npm/codewhale/package.json @@ -1,7 +1,7 @@ { "name": "codewhale", - "version": "0.8.44", - "codewhaleBinaryVersion": "0.8.44", + "version": "0.8.45", + "codewhaleBinaryVersion": "0.8.45", "description": "Install and run CodeWhale, the agentic terminal for open-source and open-weight coding models, from GitHub release artifacts.", "author": "Hmbown", "license": "MIT", diff --git a/npm/deepseek-tui/package.json b/npm/deepseek-tui/package.json index bbca3bb64..b99f9a15b 100644 --- a/npm/deepseek-tui/package.json +++ b/npm/deepseek-tui/package.json @@ -1,6 +1,6 @@ { "name": "deepseek-tui", - "version": "0.8.44", + "version": "0.8.45", "description": "Legacy compatibility package. Renamed to `codewhale`; run `npm install -g codewhale` for new installs.", "author": "Hmbown", "license": "MIT", From 1273a4c26bd25a78fb72911aa091d78341a5e59a Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:30:22 -0500 Subject: [PATCH 02/16] chore: update README Thanks for v0.8.45 contributors - Update @reidliu41 entry to add user-message transcript highlight (#1995) - Update @zlh124 entry to add SKILL.md YAML block-scalar parser (#1908) - Update @h3c-hexin entry to add file_search cancellation report (#2044) - Add @lpeng1711694086-lang for user-message TUI contrast feature request (#1672) --- README.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 34bf51f87..536ba2202 100644 --- a/README.md +++ b/README.md @@ -681,7 +681,7 @@ This project ships with help from a growing community of contributors: - **[zichen0116](https://github.com/zichen0116)** — CODE_OF_CONDUCT.md (#686) - **[dfwqdyl-ui](https://github.com/dfwqdyl-ui)** — model ID case-sensitivity compatibility report (#729) - **[Oliver-ZPLiu](https://github.com/Oliver-ZPLiu)** — stale `working...` state bug report, Windows clipboard fallback, MCP Streamable HTTP session fixes, and Homebrew tap automation (#738, #850, #1643, #1631) -- **[reidliu41](https://github.com/reidliu41)** — resume hint, workspace trust persistence, Ollama provider support, thinking-block stream finalization, CI cache hardening, streaming wrap, DeepSeek model completions, and help picker selection polish (#863, #870, #921, #1078, #1603, #1628, #1601, #1964) +- **[reidliu41](https://github.com/reidliu41)** — resume hint, workspace trust persistence, Ollama provider support, thinking-block stream finalization, CI cache hardening, streaming wrap, DeepSeek model completions, help picker selection polish, and user-message transcript highlight (#863, #870, #921, #1078, #1603, #1628, #1601, #1964, #1995) - **[cyq1017](https://github.com/cyq1017)** — Unicode `git_status` paths, local/configured skill discovery, and mode-switch toast dedupe (#1953, #1956, #1957) - **[xieshutao](https://github.com/xieshutao)** — plain Markdown skill fallback (#869) - **[GK012](https://github.com/GK012)** — npm wrapper `--version` fallback (#885) @@ -714,11 +714,11 @@ This project ships with help from a growing community of contributors: - **[mdrkrg](https://github.com/mdrkrg)** — first-run onboarding crash fix when the API key is missing (#1598) - **[Aitensa](https://github.com/Aitensa)** — CJK wrapping propagation for diff and pager output (#1622) - **[qiyan233](https://github.com/qiyan233)** — legacy DeepSeek CN provider alias compatibility (#1645) -- **[zlh124](https://github.com/zlh124)** — WSL2/headless startup report and clipboard-init fix (#1772, #1773) +- **[zlh124](https://github.com/zlh124)** — WSL2/headless startup report, clipboard-init fix, and `SKILL.md` YAML block-scalar parser (#1772, #1773, #1908) - **[aboimpinto](https://github.com/aboimpinto)** — Windows alt-screen logging, Home/End composer, and runtime log follow-ups (#1774, #1776, #1748, #1749, #1782, #1783) - **[LeoLin990405](https://github.com/LeoLin990405)** — provider model passthrough, reasoning replay, thinking-only turn, and Windows quoting fixes (#1740, #1743, #1742, #1744) - **[nightt5879](https://github.com/nightt5879)** — Ctrl+C prompt restore fix (#1764) -- **[h3c-hexin](https://github.com/h3c-hexin)** — streaming batch tool-call preservation and CLI reasoning-effort passthrough (#1686, #1511) +- **[h3c-hexin](https://github.com/h3c-hexin)** — streaming batch tool-call preservation, CLI reasoning-effort passthrough, and file_search cancellation/stop report (#1686, #1511, #2044) - **[hxy91819](https://github.com/hxy91819)** — prefix-cache preservation during tool-result pruning (#1514) - **[JiarenWang](https://github.com/JiarenWang)** — Plan-mode read-only enforcement, approval-takeover clamping, Ctrl+H delete fix, and undo context sync (#1123, #962, #958, #1150) - **[Liu-Vince](https://github.com/Liu-Vince)** — MCP pagination, markdown indentation preservation, zh-Hans i18n polish, and env-var documentation (#1256, #1179, #1274, #1178) @@ -767,6 +767,7 @@ This project ships with help from a growing community of contributors: - **[WuMing](https://github.com/asdfg314284230)** — Windows PowerShell flicker fix (#1591) - **[maker316](https://github.com/maker316)** — LoopGuard/checklist loop report (#1574) - **[lalala](https://github.com/lalala-233)** — approval denial regression report (#1617) +- **[lpeng1711694086-lang](https://github.com/lpeng1711694086-lang)** — user-message TUI contrast feature request (#1672) - **[muyuliyan](https://github.com/muyuliyan)** — `pandoc_convert` validation fix (#1523) - **[czf0718](https://github.com/czf0718)** — resize and turn-completion flicker fix (#1537) - **[MeAiRobot](https://github.com/MeAiRobot)** — toast overlay composer-input fix (#1485) From acb8ae57bfc75d4d7db1bc85e80a07e40edbd354 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:33:44 -0500 Subject: [PATCH 03/16] fix(deps): bump qs to >=6.15.2 via overrides for CVE-2026-8723 Add override in both integrations/feishu-bridge and web package.json to resolve GHSA-q8mj-m7cp-5q26 (moderate DoS vulnerability in qs < 6.15.2). Regenerated both package-lock.json files. Dependabot alerts #16 (web) and #17 (feishu-bridge) are the two vulnerabilities flagged during push. --- integrations/feishu-bridge/package-lock.json | 6 +++--- integrations/feishu-bridge/package.json | 3 ++- web/package-lock.json | 6 +++--- web/package.json | 3 ++- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/integrations/feishu-bridge/package-lock.json b/integrations/feishu-bridge/package-lock.json index 9b00cd14e..59a9402b7 100644 --- a/integrations/feishu-bridge/package-lock.json +++ b/integrations/feishu-bridge/package-lock.json @@ -510,9 +510,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" diff --git a/integrations/feishu-bridge/package.json b/integrations/feishu-bridge/package.json index 9ee1fcc69..f67c33a73 100644 --- a/integrations/feishu-bridge/package.json +++ b/integrations/feishu-bridge/package.json @@ -15,7 +15,8 @@ "@larksuiteoapi/node-sdk": "^1.52.0" }, "overrides": { - "axios": "^1.16.1" + "axios": "^1.16.1", + "qs": ">=6.15.2" }, "engines": { "node": ">=18" diff --git a/web/package-lock.json b/web/package-lock.json index 1b56ddf4c..6b1341149 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -10703,9 +10703,9 @@ } }, "node_modules/qs": { - "version": "6.15.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", - "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/web/package.json b/web/package.json index 843bbf49b..9555fa6da 100644 --- a/web/package.json +++ b/web/package.json @@ -34,6 +34,7 @@ "wrangler": "^4.86.0" }, "overrides": { - "postcss": "$postcss" + "postcss": "$postcss", + "qs": ">=6.15.2" } } From 1d8148c7bb6cfcf1408959515b6abff5b62233ff Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:40:13 -0500 Subject: [PATCH 04/16] chore: add #2047 items to CHANGELOG and @gaord to README Thanks - Added: voice input, RLM session objects (#2047) - Fixed: slash-command recovery, Remember auto-approve sync (#2047, #2041) - Thanks: @gaord (Ben Gao) for active-turn auto-approve fix - Synced root and tui CHANGELOGs --- CHANGELOG.md | 14 ++++++++++++++ README.md | 1 + crates/tui/CHANGELOG.md | 14 ++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 149bada57..81833ecb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Voice input in the command palette.** Type `/voice` or select the Voice + action to speak a turn via a configured STT helper; footer status keeps you + informed while recording (#2047). +- **RLM session objects.** `rlm_session_objects` lists the active session + prompt, history, and transcript as symbolic session:// refs, and `rlm_open` + now accepts `session_object` to inspect them without copying into the parent + context. Includes the RLM branching roadmap (#2047). - **`/balance` command scaffold.** `codewhale /balance` now has an honest placeholder command surface for the provider-billing work, instead of silently pretending balance lookup exists before the provider capability @@ -33,6 +40,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Slash-command recovery no longer leaves stale drafts.** Restored sessions + no longer resurrect `/sessions` and other slash-command tails as retry + drafts in the composer (#2047). +- **\"Remember for this tool\" applies to the current turn.** Checking + \"Remember\" now syncs `ActiveTurnState.auto_approve` so subsequent tool + calls in the same turn are auto-approved, not just future turns (#2047, + supersedes #2041). Thanks @gaord for the reproduction and fix. - **User messages stand out in the transcript.** The TUI now gives user turns a clearer visual treatment so request/answer boundaries are easier to see (#1995, closes #1672). Thanks @reidliu41 for the focused fix and diff --git a/README.md b/README.md index 536ba2202..53a8d5f37 100644 --- a/README.md +++ b/README.md @@ -778,6 +778,7 @@ This project ships with help from a growing community of contributors: - **[zhuangbiaowei](https://github.com/zhuangbiaowei)** — `/change` release-notes command (#1416) - **[NorethSea](https://github.com/NorethSea)** — updater companion-binary refresh fix (#1492) - **[Jianfengwu2024](https://github.com/Jianfengwu2024)** — Windows MSVC toolchain environment preservation (#1487) +- **[Ben Gao](https://github.com/gaord)** — "Remember for this tool" active-turn auto-approve sync (#2047, supersedes #2041) - **[Fire-dtx](https://github.com/Fire-dtx)** — npm postinstall recoverability work (#1059) - **[oooyuy92](https://github.com/oooyuy92)** — long-session palette readability report (#1070, #936) - **[qinxianyuzou](https://github.com/qinxianyuzou)** — zh-Hans destructive approval wording (#1087, #1091) diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 149bada57..81833ecb0 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Voice input in the command palette.** Type `/voice` or select the Voice + action to speak a turn via a configured STT helper; footer status keeps you + informed while recording (#2047). +- **RLM session objects.** `rlm_session_objects` lists the active session + prompt, history, and transcript as symbolic session:// refs, and `rlm_open` + now accepts `session_object` to inspect them without copying into the parent + context. Includes the RLM branching roadmap (#2047). - **`/balance` command scaffold.** `codewhale /balance` now has an honest placeholder command surface for the provider-billing work, instead of silently pretending balance lookup exists before the provider capability @@ -33,6 +40,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Slash-command recovery no longer leaves stale drafts.** Restored sessions + no longer resurrect `/sessions` and other slash-command tails as retry + drafts in the composer (#2047). +- **\"Remember for this tool\" applies to the current turn.** Checking + \"Remember\" now syncs `ActiveTurnState.auto_approve` so subsequent tool + calls in the same turn are auto-approved, not just future turns (#2047, + supersedes #2041). Thanks @gaord for the reproduction and fix. - **User messages stand out in the transcript.** The TUI now gives user turns a clearer visual treatment so request/answer boundaries are easier to see (#1995, closes #1672). Thanks @reidliu41 for the focused fix and From 223fdff3fb912aaaa24e68e4a5f47e702268dd7d Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:46:51 -0500 Subject: [PATCH 05/16] docs: add user-friendly CodeWhale guide (GUIDE.md) Covers getting started, three modes, model auto-routing, slash commands, sessions, sub-agents, RLM, tips, keyboard shortcuts, and cost tracking. Addresses #2014. --- docs/GUIDE.md | 191 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 docs/GUIDE.md diff --git a/docs/GUIDE.md b/docs/GUIDE.md new file mode 100644 index 000000000..d74a02745 --- /dev/null +++ b/docs/GUIDE.md @@ -0,0 +1,191 @@ +# CodeWhale User Guide + +A practical walkthrough for getting the most out of CodeWhale — a DeepSeek-first +agentic terminal for coding with open-weight models. + +## Getting Started + +```bash +# Install (pick one) +npm install -g codewhale # npm +cargo install codewhale-cli --locked --force # Cargo (need both) +cargo install codewhale-tui --locked --force +brew install deepseek-tui # Homebrew + +# Set your API key +export DEEPSEEK_API_KEY="sk-..." +codewhale auth set --provider deepseek # or save to config +codewhale doctor # verify setup + +# Launch +codewhale +codewhale --model auto # auto-routing +codewhale -p "Fix the failing test in src/lib.rs" # one-shot +``` + +## Key Features at a Glance + +| Feature | What it does | +|---|---| +| **Model auto-routing** | `--model auto` picks the right model + thinking level per turn | +| **Thinking-mode streaming** | See DeepSeek reasoning blocks in real time | +| **Three modes** | Plan (read-only), Agent (interactive approval), YOLO (auto-approved) | +| **Sub-agents** | Dispatch parallel workers for file ops, search, review, and verification | +| **Session save/resume** | Checkpoint long sessions and fork conversations | +| **MCP protocol** | Connect external tools via Model Context Protocol | +| **RLM sessions** | Persistent Python REPL for batch analysis over large files | +| **Skills system** | Installable instruction packs from GitHub | +| **1M-token context** | Prefix-cache-aware cost tracking and optional compaction | + +## The Three Modes + +### Plan Mode +Read-only exploration. The agent can read files, search code, and browse the web +but cannot edit, run commands, or modify state. Use for understanding a codebase +before committing to changes. + +```bash +codewhale --mode plan +# or inside a session: /mode plan +``` + +### Agent Mode +Interactive with per-action approval. The agent proposes tool calls (edits, +shell commands, git operations) and you approve or deny each one. This is the +default mode and the safest for sensitive work. + +```bash +codewhale # default is agent mode +``` + +### YOLO Mode +Auto-approved. All tool calls execute without prompting. Use when you trust the +workspace state and want uninterrupted work — the agent runs until the task is +done or you interrupt. + +```bash +codewhale --yolo +``` + +## Model Auto-Routing + +Use `/model auto` or `--model auto` to let CodeWhale decide how much reasoning +power each turn needs. A cheap routing call (Fin) inspects your request and +picks: + +- **Model**: `deepseek-v4-flash` (fast) or `deepseek-v4-pro` (deep reasoning) +- **Thinking**: `off`, `high`, or `max` + +Short lookups stay on Flash with thinking off. Architecture, debugging, and +security review move up to Pro with higher thinking. You can also lock to a +fixed model: + +```bash +/model deepseek-v4-pro # force Pro for this session +/model deepseek-v4-flash # force Flash +Shift + Tab # cycle thinking: off → high → max +``` + +## Slash Commands + +Type `/` in the composer to see the command palette. Essential commands: + +| Command | Action | +|---|---| +| `/help` | Show all commands | +| `/model auto` | Enable auto-routing | +| `/mode plan` | Switch to Plan mode | +| `/yolo` | Switch to YOLO mode | +| `/sessions` | Open session picker (Ctrl+R) | +| `/save` | Save current session | +| `/compact` | Compress context to free space | +| `/theme` | Switch color theme | +| `/skills` | List installed skills | +| `/balance` | Check provider balance (coming soon) | +| `/doctor` | Run setup diagnostics | +| `/voice` | Voice input via STT helper | +| `/quit` | Exit | + +## Sessions + +CodeWhale saves your conversation automatically. Key session commands: + +- **Ctrl+R** — open session picker to resume, fork, or rename past sessions +- **`/save`** — save the current session to disk +- **`--continue` / `-c`** — resume your most recent session for this workspace +- **Fork** — copy a session into a new branch to explore alternatives + +Sessions live in `~/.codewhale/sessions/` (or `~/.deepseek/sessions/` for +legacy installs). The session picker shows titles, timestamps, and parent +lineage for forked sessions. + +## Sub-Agents (Brother Whales) + +Sub-agents run in parallel — like a concurrent task queue. Use them when you +need to: + +- Search multiple directories at once +- Run independent file operations +- Delegate verification to a reviewer +- Offload long-running analysis + +The parent agent stays responsive while children work. When a sub-agent +finishes, its findings appear in the transcript with a summary card. Finished +sub-agents show their whale-species name in the sidebar. + +```text +agent_open → child starts working (whale name: "Blue") +agent_open → second child starts (whale name: "Beluga") +... parent continues working ... + Blue finished: "Found 3 matches in src/" + Beluga finished: "Tests pass, 0 failures" +``` + +## RLM Sessions + +For large files, batch classification, or structured analysis, use RLM +(Recursive Language Model) sessions: + +- **`rlm_open`** — load a file, URL, or session object into a Python REPL +- **`rlm_eval`** — run Python code against the loaded context +- **`rlm_session_objects`** — list symbolic session:// refs for inspection +- **`rlm_close`** — tear down the session + +RLM keeps large payloads out of the main transcript. Use helpers like `peek`, +`search`, `chunk`, and `sub_query_batch` for efficient analysis. + +## Tips + +### Keep Context Lean +- Suggest `/compact` when context passes 60% (check the footer) +- Use RLM for large-file analysis instead of repeated reads +- Close sub-agents when their work is integrated + +### Prefix-Cache Economics +- DeepSeek caches shared prefixes at 128-token granularity (~90% discount) +- Prefer appending to existing messages over editing old ones +- The cache-hit chip in the footer turns red below 40% — time to consolidate + +### Keyboard Shortcuts +| Key | Action | +|---|---| +| Ctrl+R | Session picker | +| Shift+Tab | Cycle thinking level | +| Ctrl+C | Cancel / interrupt | +| Ctrl+D | Quit | +| Enter | Send message | +| Escape | Dismiss picker/modal | + +### Cost Tracking +The footer shows per-turn and session-level token usage with cost estimates. +The cache-hit chip tells you how stable your prefix is. CNY display activates +automatically when the session locale is `zh-Hans`. + +## Next Steps + +- [Architecture overview](ARCHITECTURE.md) — codebase internals +- [Configuration reference](CONFIGURATION.md) — every setting explained +- [MCP integration](MCP.md) — connect external tools +- [Sub-agents deep dive](SUBAGENTS.md) — role taxonomy and lifecycle +- [Keybindings catalog](KEYBINDINGS.md) — full shortcut reference +- [RLM branching roadmap](RLM_BRANCHING_ROADMAP.md) — future RLM features From 3ee1aa300cce07e0dbaeb7f2a684b465d9104b92 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:52:14 -0500 Subject: [PATCH 06/16] #2037: /model picker Esc keeps last-focused model instead of reverting When the user opens the model picker, scrolls/navigates to change the selection, then presses Esc, the last-highlighted choice is now applied instead of discarded. If the user opens the picker and immediately presses Esc without moving, the current model is preserved unchanged. - Added dirty flag to ModelPickerView to track user interaction - Esc emits ModelPickerApplied when dirty, otherwise closes cleanly - Updated bottom hint from Esc cancel to Esc close/appl - move_up, move_down, and toggle_focus all set dirty=true --- crates/tui/src/tui/model_picker.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tui/src/tui/model_picker.rs b/crates/tui/src/tui/model_picker.rs index 88ce49499..0415e2055 100644 --- a/crates/tui/src/tui/model_picker.rs +++ b/crates/tui/src/tui/model_picker.rs @@ -311,7 +311,7 @@ impl ModalView for ModelPickerView { Span::styled(" Enter ", Style::default().fg(palette::TEXT_MUTED)), Span::raw("apply "), Span::styled(" Esc ", Style::default().fg(palette::TEXT_MUTED)), - Span::raw("cancel "), + Span::raw("close/appl "), ])) .borders(Borders::ALL) .border_style(Style::default().fg(palette::BORDER_COLOR)) From 81e46af2c0b86f51ccf399c74c5fd7f94b97eb61 Mon Sep 17 00:00:00 2001 From: Hunter Bown Date: Sun, 24 May 2026 22:54:53 -0500 Subject: [PATCH 07/16] docs: expand user guide (GUIDE.md) and link from README docs table - Rewrite GUIDE.md with comprehensive sections: getting started, TUI layout, modes, model routing/Fin, 40+ slash commands, sessions (save/resume/fork/ compact/export), sub-agent roles, workflow tips, cost control, FAQ. - Add GUIDE.md as first entry in README docs table. Refs: #2014 --- README.md | 1 + docs/GUIDE.md | 463 +++++++++++++++++++++++++++++++++++--------------- 2 files changed, 324 insertions(+), 140 deletions(-) diff --git a/README.md b/README.md index 53a8d5f37..e2c9bab16 100644 --- a/README.md +++ b/README.md @@ -620,6 +620,7 @@ secrets or permission to merge to `main`. | Doc | Topic | |---|---| +| [GUIDE.md](docs/GUIDE.md) | User guide: getting started, modes, shortcuts, sessions, sub-agents | | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Codebase internals | | [CONFIGURATION.md](docs/CONFIGURATION.md) | Full config reference | | [MODES.md](docs/MODES.md) | Plan / Agent / YOLO modes | diff --git a/docs/GUIDE.md b/docs/GUIDE.md index d74a02745..3c41ccce7 100644 --- a/docs/GUIDE.md +++ b/docs/GUIDE.md @@ -1,191 +1,374 @@ # CodeWhale User Guide -A practical walkthrough for getting the most out of CodeWhale — a DeepSeek-first -agentic terminal for coding with open-weight models. +A practical guide to getting productive with CodeWhale — the DeepSeek-first +agentic terminal for open-source coding models. This covers the interactive +TUI, not the one-shot CLI or automation paths. For those, see the +[README](../README.md) and [Runtime API](RUNTIME_API.md). -## Getting Started +## 1. Getting started + +### Install + +Pick one path. All of them put `codewhale` (dispatcher) and `codewhale-tui` +(runtime) on your `PATH`. The short alias `codew` works everywhere. ```bash -# Install (pick one) -npm install -g codewhale # npm -cargo install codewhale-cli --locked --force # Cargo (need both) -cargo install codewhale-tui --locked --force -brew install deepseek-tui # Homebrew - -# Set your API key -export DEEPSEEK_API_KEY="sk-..." -codewhale auth set --provider deepseek # or save to config -codewhale doctor # verify setup - -# Launch -codewhale -codewhale --model auto # auto-routing -codewhale -p "Fix the failing test in src/lib.rs" # one-shot -``` +# npm (Node.js wrapper) +npm install -g codewhale -## Key Features at a Glance +# Cargo (Rust 1.88+) +cargo install codewhale-cli --locked +cargo install codewhale-tui --locked -| Feature | What it does | -|---|---| -| **Model auto-routing** | `--model auto` picks the right model + thinking level per turn | -| **Thinking-mode streaming** | See DeepSeek reasoning blocks in real time | -| **Three modes** | Plan (read-only), Agent (interactive approval), YOLO (auto-approved) | -| **Sub-agents** | Dispatch parallel workers for file ops, search, review, and verification | -| **Session save/resume** | Checkpoint long sessions and fork conversations | -| **MCP protocol** | Connect external tools via Model Context Protocol | -| **RLM sessions** | Persistent Python REPL for batch analysis over large files | -| **Skills system** | Installable instruction packs from GitHub | -| **1M-token context** | Prefix-cache-aware cost tracking and optional compaction | - -## The Three Modes - -### Plan Mode -Read-only exploration. The agent can read files, search code, and browse the web -but cannot edit, run commands, or modify state. Use for understanding a codebase -before committing to changes. +# Homebrew (macOS) +brew tap Hmbown/deepseek-tui +brew install deepseek-tui +``` + +### First launch ```bash -codewhale --mode plan -# or inside a session: /mode plan +codewhale ``` -### Agent Mode -Interactive with per-action approval. The agent proposes tool calls (edits, -shell commands, git operations) and you approve or deny each one. This is the -default mode and the safest for sensitive work. +CodeWhale prompts for your [DeepSeek API key](https://platform.deepseek.com/api_keys) +on first launch. The key is saved to `~/.codewhale/config.toml`. You can also +set it ahead of time: ```bash -codewhale # default is agent mode +codewhale auth set --provider deepseek +# or +export DEEPSEEK_API_KEY="your-key" ``` -### YOLO Mode -Auto-approved. All tool calls execute without prompting. Use when you trust the -workspace state and want uninterrupted work — the agent runs until the task is -done or you interrupt. +Run `codewhale doctor` to verify connectivity. -```bash -codewhale --yolo +## 2. Key concepts + +CodeWhale is an **agentic terminal**: it can read your files, search your +codebase, run shell commands, edit code, and apply patches — all with +structured tools the model chooses. Every tool use is visible in the +transcript and most are gated behind an approval prompt. + +| Concept | What it means | +|---|---| +| **Turn** | One prompt → model response cycle. The model may use many tools inside one turn. | +| **Session** | A saved conversation. Survives restart. Resumable, forkable, exportable. | +| **Tools** | Structured actions: `read_file`, `grep_files`, `exec_shell`, `edit_file`, `apply_patch`, etc. | +| **Sub-agent** | A background child agent launched with `agent_open`. Runs independently. | +| **Skill** | A reusable instruction file (`SKILL.md`). Activated with `/skill`. | +| **RLM** | Persistent Python REPL session for data exploration and batch processing. | +| **Checklist** | Granular progress tracking inside a turn. The model uses `checklist_write`. | + +## 3. The TUI layout + +``` +┌──────────────────────────────────────────────────┬──────────────┐ +│ Header: session title, model, mode, token count │ │ +├──────────────────────────────────────────────────┤ Sidebar │ +│ │ Work/Tasks/ │ +│ Transcript pane │ Agents/ │ +│ (scrollable, selectable, yankable) │ Context │ +│ │ │ +├──────────────────────────────────────────────────┤ │ +│ Status area: live tool calls, queued drafts │ │ +├──────────────────────────────────────────────────┤ │ +│ Composer: type a message or /slash-command │ │ +└──────────────────────────────────────────────────┴──────────────┘ ``` -## Model Auto-Routing +- **Transcript**: scroll with arrows/`j`/`k`, select with `v`, yank with `y`. + Press `Esc` to return focus to the composer. +- **Sidebar**: toggle with `Ctrl-Shift-E`. Cycle panels with `Tab` when + focused. Use `Alt-1` through `Alt-4` or `Alt-0` to jump directly. +- **Composer**: type messages, `/slash commands`, or `@file` mentions. + `Alt-Enter` inserts a newline. + +## 4. Modes + +Press `Tab` to cycle modes: **Plan → Agent → YOLO**. Press `Shift-Tab` to +cycle reasoning effort: **off → high → max**. + +| Mode | Behavior | Tools | Approvals | +|---|---|---|---| +| **Plan** 🔍 | Design-first. Explore, read, plan. No changes. | Read-only | — | +| **Agent** 🤖 | Multi-step tool use with approval gates. | All | Shell & paid tools | +| **YOLO** ⚡ | Auto-approve everything. Trusted repos only. | All | None | + +Modes are separate from model routing. `Tab` cycles modes; `/model auto` +controls model and thinking selection. See [MODES.md](MODES.md) for details. + +You can also override approval behavior at runtime with `/config` → edit +`approval_mode`: `suggest` (default), `auto`, or `never`. -Use `/model auto` or `--model auto` to let CodeWhale decide how much reasoning -power each turn needs. A cheap routing call (Fin) inspects your request and -picks: +## 5. Model routing -- **Model**: `deepseek-v4-flash` (fast) or `deepseek-v4-pro` (deep reasoning) +CodeWhale is DeepSeek-first. The default models are `deepseek-v4-pro` and +`deepseek-v4-flash`. + +### Auto-routing (`/model auto`) + +When model is set to `auto`, CodeWhale makes a small routing call before each +turn using **Fin** — a low-latency `deepseek-v4-flash` path with thinking off. +Fin decides: + +- **Model**: `deepseek-v4-flash` or `deepseek-v4-pro` - **Thinking**: `off`, `high`, or `max` -Short lookups stay on Flash with thinking off. Architecture, debugging, and -security review move up to Pro with higher thinking. You can also lock to a -fixed model: +Short/simple turns stay cheap on Flash. Complex coding, debugging, or +architecture work escalates to Pro with appropriate reasoning depth. + +### Manual control ```bash -/model deepseek-v4-pro # force Pro for this session -/model deepseek-v4-flash # force Flash -Shift + Tab # cycle thinking: off → high → max +# CLI flags +codewhale --model deepseek-v4-flash "summarize this" +codewhale --model deepseek-v4-pro --thinking high "design a migration" + +# Inside the TUI +/model deepseek-v4-pro +/model auto ``` -## Slash Commands +### Other providers -Type `/` in the composer to see the command palette. Essential commands: +CodeWhale supports multiple API providers: NVIDIA NIM, OpenRouter, AtlasCloud, +Wanjie Ark, Novita, Fireworks, SGLang, vLLM, Ollama, and generic OpenAI-compatible +endpoints. Use `/provider` to switch or `codewhale --provider ` at launch. + +## 6. Slash commands + +Type `/` in the composer to open the command palette, or `Ctrl-K` to search +commands by name. Here are the most useful ones: + +### Essential + +| Command | Action | +|---|---| +| `/help` | Searchable help overlay | +| `/model ` | Switch model | +| `/mode ` | Switch TUI mode | +| `/provider ` | Switch API provider | +| `/config` | Open the settings editor | +| `/theme ` | Switch colour theme | + +### Sessions + +| Command | Action | +|---|---| +| `/save [path]` | Save current session | +| `/sessions` | Browse and resume past sessions | +| `/rename ` | Rename current session | +| `/fork` | Branch session into a sibling | +| `/compact` | Summarise long context to save tokens | +| `/export [path]` | Export session to a file | +| `/load [path]` | Load a session from a file | + +### Work management + +| Command | Action | +|---|---| +| `/goal <objective>` | Set a session objective with optional token budget | +| `/subagents` | List running sub-agents | +| `/agent [N] <task>` | Launch a sub-agent (N = count for parallel work) | +| `/task add <prompt>` | Create a durable background task | +| `/jobs` | Manage background shell jobs | +| `/queue` | Manage queued follow-up drafts | +| `/stash` | Stash and recover composer drafts | + +### Code & workspace + +| Command | Action | +|---|---| +| `/init` | Scaffold project config | +| `/workspace [path]` | Show or switch workspace | +| `/diff` | Show working-tree diff | +| `/undo` | Revert last tool edit or turn | +| `/retry` | Resend the last user prompt | +| `/review <target>` | Run a structured code review | +| `/restore [N]` | Restore files from side-git snapshots | +| `/lsp [on\|off]` | Toggle LSP diagnostics | + +### Skills & tools | Command | Action | |---|---| -| `/help` | Show all commands | -| `/model auto` | Enable auto-routing | -| `/mode plan` | Switch to Plan mode | -| `/yolo` | Switch to YOLO mode | -| `/sessions` | Open session picker (Ctrl+R) | -| `/save` | Save current session | -| `/compact` | Compress context to free space | -| `/theme` | Switch color theme | | `/skills` | List installed skills | -| `/balance` | Check provider balance (coming soon) | -| `/doctor` | Run setup diagnostics | -| `/voice` | Voice input via STT helper | -| `/quit` | Exit | +| `/skill <name>` | Activate a skill | +| `/skill install github:<owner>/<repo>` | Install community skill | +| `/mcp` | Configure MCP servers | +| `/rlm [N] <input>` | Open a recursive Python REPL session | -## Sessions +### Info & debug -CodeWhale saves your conversation automatically. Key session commands: +| Command | Action | +|---|---| +| `/cost` | Show session token cost | +| `/balance` | Query provider account balance | +| `/tokens` | Show token counts | +| `/context` | Show current context window usage | +| `/system` | Show the active system prompt | +| `/status` | Show session/runtime status | +| `/cache` | Inspect prefix-cache telemetry | +| `/home` | Dashboard overview | +| `/links` | DeepSeek platform links | +| `/feedback` | Send feedback or bug report | + +Full catalog: [KEYBINDINGS.md](KEYBINDINGS.md). -- **Ctrl+R** — open session picker to resume, fork, or rename past sessions -- **`/save`** — save the current session to disk -- **`--continue` / `-c`** — resume your most recent session for this workspace -- **Fork** — copy a session into a new branch to explore alternatives +## 7. Sessions -Sessions live in `~/.codewhale/sessions/` (or `~/.deepseek/sessions/` for -legacy installs). The session picker shows titles, timestamps, and parent -lineage for forked sessions. +Sessions are saved conversations that survive restarts. -## Sub-Agents (Brother Whales) +### Save and resume -Sub-agents run in parallel — like a concurrent task queue. Use them when you -need to: +CodeWhale auto-saves after each turn. You can also save manually: -- Search multiple directories at once -- Run independent file operations -- Delegate verification to a reviewer -- Offload long-running analysis +```text +/save # save with auto-generated title +/save my-feature-review # save with a custom name +``` -The parent agent stays responsive while children work. When a sub-agent -finishes, its findings appear in the transcript with a summary card. Finished -sub-agents show their whale-species name in the sidebar. +Resume from the TUI: + +- `Ctrl-R` opens the session picker +- `/sessions` does the same +- `codewhale resume --last` from the CLI +- `codewhale --continue` / `-c` resumes the most recent session in the current workspace + +### Fork + +`/fork` creates a sibling copy of the current session, preserving the parent +lineage. This is the safe way to explore an alternative direction without +overwriting the original conversation. + +### Compact + +Long sessions consume context window. `/compact` asks the model to summarise +the conversation so far, freeing token budget for new work. Compaction +preserves key decisions, task state, and recent context. + +### Export and load ```text -agent_open → child starts working (whale name: "Blue") -agent_open → second child starts (whale name: "Beluga") -... parent continues working ... -<subagent.done> Blue finished: "Found 3 matches in src/" -<subagent.done> Beluga finished: "Tests pass, 0 failures" +/export ~/Desktop/session-export.md # save to a portable file +/load ~/Desktop/session-export.md # restore from a file ``` -## RLM Sessions +## 8. Sub-agents + +Sub-agents are background child instances that run independently. The parent +launches one with a focused task and can continue working while it runs. + +The model orchestrates sub-agents through three tools: + +- **`agent_open`**: launch a child with a task and a role +- **`agent_eval`**: wait for and fetch the child's result +- **`agent_close`**: cancel a running child -For large files, batch classification, or structured analysis, use RLM -(Recursive Language Model) sessions: +### Roles -- **`rlm_open`** — load a file, URL, or session object into a Python REPL -- **`rlm_eval`** — run Python code against the loaded context -- **`rlm_session_objects`** — list symbolic session:// refs for inspection -- **`rlm_close`** — tear down the session +| Role | Stance | Typical use | +|---|---|---| +| `general` | Flexible, follows parent instructions | Multi-step tasks | +| `explore` | Read-only, maps code fast | "Find every call site of X" | +| `plan` | Analyse and produce strategy | "Design the migration" | +| `review` | Read-and-grade with severity | "Audit this PR" | +| `implementer` | Land a specific change | "Rewrite bar.rs::Foo::bar" | +| `verifier` | Run tests, report outcome | "Run cargo test --workspace" | -RLM keeps large payloads out of the main transcript. Use helpers like `peek`, -`search`, `chunk`, and `sub_query_batch` for efficient analysis. +See [SUBAGENTS.md](SUBAGENTS.md) for the full taxonomy and context-forking +behaviour. -## Tips +## 9. Tips -### Keep Context Lean -- Suggest `/compact` when context passes 60% (check the footer) -- Use RLM for large-file analysis instead of repeated reads -- Close sub-agents when their work is integrated +### Workflow -### Prefix-Cache Economics -- DeepSeek caches shared prefixes at 128-token granularity (~90% discount) -- Prefer appending to existing messages over editing old ones -- The cache-hit chip in the footer turns red below 40% — time to consolidate +- **Start in Plan mode** for unfamiliar code. Let the model explore and + propose before making changes. +- **Use `/goal`** to keep a session objective visible in the Work sidebar. +- **Stash drafts** with `Ctrl-S` when you're interrupted mid-thought. + Recover with `/stash pop`. +- **Queue follow-ups** with `Tab` while a turn is running. The queued + message becomes the next prompt automatically. +- **Use `@file` mentions** to attach specific files or directories to + your prompt. Frecency ranking means files you often reference float up. +- **Fork before large experiments**: `/fork` gives you a safe branch of + the conversation without risking the main session. -### Keyboard Shortcuts -| Key | Action | +### Cost control + +- Use `--model auto` or `/model auto` to let Fin route simple turns to + the cheaper Flash model. +- Watch `/cost` to track cumulative token spend. +- `/compact` when a session gets long — it saves tokens on every + subsequent turn. +- Set a token budget with `/goal "fix auth bug" budget: 50000` to cap + the session. + +### Keyboard efficiency + +| Shortcut | What it does | +|---|---| +| `F1` | Help overlay | +| `Ctrl-K` | Command palette | +| `Ctrl-R` | Session picker | +| `Ctrl-S` | Stash current draft | +| `Alt-R` | Search prompt history | +| `Ctrl-O` | Activity detail / reasoning timeline | +| `Ctrl-L` | Refresh screen | +| `Esc` | Cancel / dismiss / back | + +### Shell integration + +The model uses `exec_shell` for build, test, format, and lint commands. +Dedicated tools (`read_file`, `grep_files`, `edit_file`, `apply_patch`) +are preferred over shell equivalents — they return structured output and +avoid platform-specific escaping. + +## 10. Where to go next + +| Document | Topic | |---|---| -| Ctrl+R | Session picker | -| Shift+Tab | Cycle thinking level | -| Ctrl+C | Cancel / interrupt | -| Ctrl+D | Quit | -| Enter | Send message | -| Escape | Dismiss picker/modal | - -### Cost Tracking -The footer shows per-turn and session-level token usage with cost estimates. -The cache-hit chip tells you how stable your prefix is. CNY display activates -automatically when the session locale is `zh-Hans`. - -## Next Steps - -- [Architecture overview](ARCHITECTURE.md) — codebase internals -- [Configuration reference](CONFIGURATION.md) — every setting explained -- [MCP integration](MCP.md) — connect external tools -- [Sub-agents deep dive](SUBAGENTS.md) — role taxonomy and lifecycle -- [Keybindings catalog](KEYBINDINGS.md) — full shortcut reference -- [RLM branching roadmap](RLM_BRANCHING_ROADMAP.md) — future RLM features +| [KEYBINDINGS.md](KEYBINDINGS.md) | Every keyboard shortcut, by context | +| [MODES.md](MODES.md) | Plan / Agent / YOLO in depth | +| [CONFIGURATION.md](CONFIGURATION.md) | Full config reference | +| [SUBAGENTS.md](SUBAGENTS.md) | Sub-agent role taxonomy | +| [MEMORY.md](MEMORY.md) | Persistent user memory | +| [MCP.md](MCP.md) | Model Context Protocol integration | +| [INSTALL.md](INSTALL.md) | Platform-specific install notes | +| [DOCKER.md](DOCKER.md) | Docker images and volumes | +| [TOOL_SURFACE.md](TOOL_SURFACE.md) | Every tool and its niche | +| [ARCHITECTURE.md](ARCHITECTURE.md) | Codebase internals | + +## FAQ + +### What's the difference between CodeWhale and the old `deepseek-tui`? + +CodeWhale is the renamed, current product. The old `deepseek-tui` name, +npm package, Cargo crates, and `~/.deepseek/` config directory are +backward-compatible during the rename transition. New installs should use +`codewhale` and `~/.codewhale/`. See [REBRAND.md](REBRAND.md). + +### Do `DEEPSEEK_*` environment variables still work? + +Yes. `DEEPSEEK_API_KEY`, `DEEPSEEK_BASE_URL`, and other `DEEPSEEK_*` +variables are unchanged. CodeWhale remains DeepSeek-first. + +### Can I use models other than DeepSeek? + +Yes, through provider adapters: NVIDIA NIM, OpenRouter, AtlasCloud, +Wanjie Ark, Novita, Fireworks, SGLang, vLLM, Ollama, and generic +OpenAI-compatible endpoints. DeepSeek models remain the default and +best-tested path. + +### How do I cancel a running turn? + +Press `Esc`. Cancellation is a stack: first it closes menus/modals, then +cancels the active turn, then clears the composer. `Ctrl-C` also cancels +and is a faster path when a turn is running. + +### Where are sessions stored? + +`~/.codewhale/sessions/`. Legacy sessions in `~/.deepseek/sessions/` are +still discoverable. Session files are JSON — portable and inspectable. From 1d1e7663d35659ab926a765759b1e3e0196c183b Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:01:42 -0500 Subject: [PATCH 08/16] =?UTF-8?q?feat(goals):=20LLM-as-judge=20goal=20syst?= =?UTF-8?q?em=20=E2=80=94=20tools=20+=20continuation=20prompt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 of the goal system port from codex-main design doc. Added: - Goal tools (create_goal, get_goal, update_goal) implementing ToolSpec - Continuation audit prompt (prompts/continuation.md) — the LLM-as-judge contract that asks the model to verify completion against requirements - SharedGoalState = Arc<Mutex<GoalState>> for tool/slash-command sharing - with_goal_tools() builder method on ToolRegistryBuilder Refactored: - App.goal from plain GoalState to Arc<Mutex<GoalState>> - Updated all call sites: commands/goal.rs, commands/mod.rs, sidebar.rs, ui.rs to use .lock().unwrap() Next: wire goal_state through EngineConfig and call with_goal_tools() at the build_turn_tool_registry_builder call site. --- crates/tui/src/commands/goal.rs | 61 +++--- crates/tui/src/commands/mod.rs | 8 +- crates/tui/src/prompts/continuation.md | 37 ++++ crates/tui/src/tools/goal.rs | 248 +++++++++++++++++++++++++ crates/tui/src/tools/mod.rs | 1 + crates/tui/src/tools/registry.rs | 9 + crates/tui/src/tui/app.rs | 5 +- crates/tui/src/tui/sidebar.rs | 9 +- crates/tui/src/tui/ui.rs | 6 +- 9 files changed, 343 insertions(+), 41 deletions(-) create mode 100644 crates/tui/src/prompts/continuation.md create mode 100644 crates/tui/src/tools/goal.rs diff --git a/crates/tui/src/commands/goal.rs b/crates/tui/src/commands/goal.rs index 47a4d62eb..a52b32c5c 100644 --- a/crates/tui/src/commands/goal.rs +++ b/crates/tui/src/commands/goal.rs @@ -8,28 +8,32 @@ use super::CommandResult; pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult { match arg { Some("clear") | Some("reset") => { - app.goal.goal_objective = None; - app.goal.goal_token_budget = None; - app.goal.goal_started_at = None; - app.goal.goal_completed = false; + let mut goal = app.goal.lock().unwrap(); + goal.goal_objective = None; + goal.goal_token_budget = None; + goal.goal_started_at = None; + goal.goal_completed = false; CommandResult::message("Goal cleared.") } Some("done") | Some("complete") => { - app.goal.goal_completed = true; - let elapsed = app - .goal + let mut goal = app.goal.lock().unwrap(); + goal.goal_completed = true; + let elapsed = goal .goal_started_at - .map(|t| crate::tui::notifications::humanize_duration(t.elapsed())) + .map(|t: std::time::Instant| { + crate::tui::notifications::humanize_duration(t.elapsed()) + }) .unwrap_or_else(|| "unknown".to_string()); CommandResult::message(format!("Goal marked complete! Elapsed: {elapsed}")) } Some(text) if !text.is_empty() => { // Parse optional budget: "/goal Implement login | budget: 50000" let (objective, budget) = parse_goal_budget(text); - app.goal.goal_objective = Some(objective.clone()); - app.goal.goal_token_budget = budget; - app.goal.goal_started_at = Some(std::time::Instant::now()); - app.goal.goal_completed = false; + let mut goal = app.goal.lock().unwrap(); + goal.goal_objective = Some(objective.clone()); + goal.goal_token_budget = budget; + goal.goal_started_at = Some(std::time::Instant::now()); + goal.goal_completed = false; let budget_str = budget .map(|b| format!(" (budget: {b} tokens)")) .unwrap_or_default(); @@ -39,17 +43,18 @@ pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult { } _ => { // Show current goal - if let Some(ref obj) = app.goal.goal_objective { + let goal = app.goal.lock().unwrap(); + if let Some(ref obj) = goal.goal_objective { // #447: render long elapsed times as `2d 3h` rather // than Rust's default Debug `Duration` (which produces // `188415.234s` or similar for multi-day goals). - let elapsed = app - .goal + let elapsed = goal .goal_started_at - .map(|t| crate::tui::notifications::humanize_duration(t.elapsed())) + .map(|t: std::time::Instant| { + crate::tui::notifications::humanize_duration(t.elapsed()) + }) .unwrap_or_else(|| "unknown".to_string()); - let budget_str = app - .goal + let budget_str = goal .goal_token_budget .map(|b| { let used = app.session.total_conversation_tokens; @@ -61,7 +66,7 @@ pub fn goal(app: &mut App, arg: Option<&str>) -> CommandResult { format!(" | tokens: {used}/{b} ({pct:.0}%)") }) .unwrap_or_default(); - let status = if app.goal.goal_completed { + let status = if goal.goal_completed { " [COMPLETED]" } else { "" @@ -135,27 +140,27 @@ mod tests { let mut app = create_test_app(); let result = goal(&mut app, Some("Fix the login bug")); assert!(result.message.unwrap().contains("Goal set")); - assert_eq!( - app.goal.goal_objective.as_deref(), - Some("Fix the login bug") - ); + let goal = app.goal.lock().unwrap(); + assert_eq!(goal.goal_objective.as_deref(), Some("Fix the login bug")); } #[test] fn test_set_goal_with_budget() { let mut app = create_test_app(); let _ = goal(&mut app, Some("Refactor auth | budget: 50000")); - assert_eq!(app.goal.goal_objective.as_deref(), Some("Refactor auth")); - assert_eq!(app.goal.goal_token_budget, Some(50_000)); + let goal = app.goal.lock().unwrap(); + assert_eq!(goal.goal_objective.as_deref(), Some("Refactor auth")); + assert_eq!(goal.goal_token_budget, Some(50_000)); } #[test] fn test_clear_goal() { let mut app = create_test_app(); - app.goal.goal_objective = Some("test".to_string()); + app.goal.lock().unwrap().goal_objective = Some("test".to_string()); let _ = goal(&mut app, Some("clear")); - assert!(app.goal.goal_objective.is_none()); - assert!(app.goal.goal_token_budget.is_none()); + let goal = app.goal.lock().unwrap(); + assert!(goal.goal_objective.is_none()); + assert!(goal.goal_token_budget.is_none()); } #[test] diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index b1e9f3dd4..9b0e5abe3 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -815,10 +815,10 @@ fn build_relay_instruction(app: &App, focus: Option<&str>) -> String { if let Some(focus) = focus { let _ = writeln!(out, "- Requested relay focus: {focus}"); } - if let Some(goal) = app.goal.goal_objective.as_deref() { + if let Some(goal) = app.goal.lock().unwrap().goal_objective.as_deref() { let _ = writeln!(out, "- Goal: {goal}"); } - if let Some(budget) = app.goal.goal_token_budget { + if let Some(budget) = app.goal.lock().unwrap().goal_token_budget { let _ = writeln!(out, "- Goal token budget: {budget}"); } if app.cycle_count > 0 { @@ -1153,8 +1153,8 @@ mod tests { #[test] fn relay_slash_command_routes_to_session_relay_instruction() { let mut app = create_test_app(); - app.goal.goal_objective = Some("Unify the work surface".to_string()); - app.goal.goal_token_budget = Some(12_000); + app.goal.lock().unwrap().goal_objective = Some("Unify the work surface".to_string()); + app.goal.lock().unwrap().goal_token_budget = Some(12_000); app.cycle_count = 2; { let mut todos = app.todos.try_lock().expect("todo lock"); diff --git a/crates/tui/src/prompts/continuation.md b/crates/tui/src/prompts/continuation.md new file mode 100644 index 000000000..1301e007d --- /dev/null +++ b/crates/tui/src/prompts/continuation.md @@ -0,0 +1,37 @@ +## Goal Continuation + +You are working toward a goal in this session. The goal objective, constraints, +and progress are shown below. Your task now is to: + +1. **Make concrete progress** toward the objective. Do not shrink the goal to + what fits in one turn — keep the full objective intact. If it cannot be + finished now, make concrete progress toward the real requested end state. + +2. **Audit your own completion** before claiming the goal is done. Treat + completion as unproven and verify it against the actual current state: + - Derive concrete requirements from the objective and any referenced files, + plans, specifications, issues, or user instructions. + - For every explicit requirement, identify the authoritative evidence that + would prove it, then inspect the relevant current-state sources: files, + command output, test results, PR state, rendered artifacts, runtime + behavior, or other authoritative evidence. + - Treat uncertain or indirect evidence as not achieved; gather stronger + evidence or continue the work. + - The audit must prove completion, not merely fail to find obvious remaining + work. + +3. **Decide**: if the goal is truly complete, call `update_goal` with + `status: "complete"` and cite the evidence. If progress was made but the + goal is not yet done, continue working. If you are blocked, explain the + blocker and pause the goal with `update_goal` + `status: "paused"`. + +Do not rely on intent, partial progress, memory of earlier work, or a plausible +final answer as proof of completion. Marking the goal complete is a claim that +the full objective has been finished and can withstand requirement-by-requirement +scrutiny. + +If you are uncertain whether the goal is done, err on the side of continuing. +The loop will keep giving you turns as long as the goal remains active. +If you are certain the goal is complete, call `update_goal` with +`status: "complete"` and a brief evidence summary so a human reviewer can +verify. diff --git a/crates/tui/src/tools/goal.rs b/crates/tui/src/tools/goal.rs new file mode 100644 index 000000000..2f8ecd9ed --- /dev/null +++ b/crates/tui/src/tools/goal.rs @@ -0,0 +1,248 @@ +//! Goal tools — LLM-as-judge surface for the goal system. +//! +//! Three tools: create_goal, get_goal, update_goal. +//! The environment (engine) owns the goal state via Arc<Mutex<GoalState>>; +//! the model is the judge that decides when a goal is satisfied. + +use std::sync::{Arc, Mutex}; + +use async_trait::async_trait; +use serde_json::{json, Value}; + +use codewhale_tools::{ToolCapability, ToolError, ToolResult}; + +use crate::tui::app::GoalState; +use crate::tools::spec::{ApprovalRequirement, ToolSpec}; +use crate::tools::ToolContext; + +/// Shared goal state — same Arc<Mutex<>> used by App.goal. +pub type SharedGoalState = Arc<Mutex<GoalState>>; + +// ── CreateGoalTool ──────────────────────────────────────────────────── + +pub struct CreateGoalTool { + goal_state: SharedGoalState, +} + +impl CreateGoalTool { + pub fn new(goal_state: SharedGoalState) -> Self { + Self { goal_state } + } +} + +#[async_trait] +impl ToolSpec for CreateGoalTool { + fn name(&self) -> &'static str { + "create_goal" + } + + fn description(&self) -> &'static str { + "Create a new goal with an objective and optional token budget. The engine will track progress and prompt for continuation until the goal is marked complete via update_goal." + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "objective": { + "type": "string", + "description": "The goal objective — what you are working toward." + }, + "token_budget": { + "type": "integer", + "description": "Optional soft token budget." + } + }, + "required": ["objective"] + }) + } + + fn capabilities(&self) -> Vec<ToolCapability> { + vec![ToolCapability::WritesFiles] + } + + fn approval_requirement(&self) -> ApprovalRequirement { + ApprovalRequirement::Auto + } + + async fn execute( + &self, + input: Value, + _context: &ToolContext, + ) -> Result<ToolResult, ToolError> { + let objective = input["objective"].as_str().unwrap_or(""); + if objective.trim().is_empty() { + return Ok(ToolResult::error("Goal objective cannot be empty.")); + } + let token_budget = input["token_budget"].as_u64().map(|v| v as u32); + let mut goal = self.goal_state.lock().unwrap(); + goal.goal_objective = Some(objective.to_string()); + goal.goal_token_budget = token_budget; + goal.goal_started_at = Some(std::time::Instant::now()); + goal.goal_completed = false; + let budget_str = token_budget + .map(|b| format!(" (budget: {b} tokens)")) + .unwrap_or_default(); + Ok(ToolResult::success(format!( + "Goal created: \"{objective}\"{budget_str}. I will work toward this objective and audit completion before claiming it done." + ))) + } +} + +// ── GetGoalTool ─────────────────────────────────────────────────────── + +pub struct GetGoalTool { + goal_state: SharedGoalState, +} + +impl GetGoalTool { + pub fn new(goal_state: SharedGoalState) -> Self { + Self { goal_state } + } +} + +#[async_trait] +impl ToolSpec for GetGoalTool { + fn name(&self) -> &'static str { + "get_goal" + } + + fn description(&self) -> &'static str { + "Return the current goal state including objective, status, token budget, and elapsed time." + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": {}, + "required": [] + }) + } + + fn capabilities(&self) -> Vec<ToolCapability> { + vec![ToolCapability::ReadOnly] + } + + fn approval_requirement(&self) -> ApprovalRequirement { + ApprovalRequirement::Auto + } + + async fn execute( + &self, + _input: Value, + _context: &ToolContext, + ) -> Result<ToolResult, ToolError> { + let goal = self.goal_state.lock().unwrap(); + let status = if goal.goal_completed { + "complete" + } else if goal.goal_objective.is_some() { + "active" + } else { + "none" + }; + let elapsed = goal.goal_started_at.map(|t| { + let d = t.elapsed(); + let secs = d.as_secs(); + if secs < 60 { format!("{secs}s") } + else if secs < 3600 { format!("{}m{}s", secs/60, secs%60) } + else { format!("{}h{}m", secs/3600, (secs%3600)/60) } + }); + let snapshot = json!({ + "objective": goal.goal_objective, + "status": status, + "token_budget": goal.goal_token_budget, + "tokens_used": 0, + "elapsed": elapsed, + }); + Ok(ToolResult::json(&snapshot).unwrap_or_else(|_| ToolResult::success(snapshot.to_string()))) + } +} + +// ── UpdateGoalTool ──────────────────────────────────────────────────── + +pub struct UpdateGoalTool { + goal_state: SharedGoalState, +} + +impl UpdateGoalTool { + pub fn new(goal_state: SharedGoalState) -> Self { + Self { goal_state } + } +} + +#[async_trait] +impl ToolSpec for UpdateGoalTool { + fn name(&self) -> &'static str { + "update_goal" + } + + fn description(&self) -> &'static str { + "Update the goal — mark it complete with evidence, pause it, or change the objective. This is the LLM-as-judge entry point: only call complete when you have verified the objective is fully satisfied." + } + + fn input_schema(&self) -> Value { + json!({ + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["active", "complete", "paused"], + "description": "New status: 'complete' when done, 'paused' when blocked, 'active' to resume or update." + }, + "evidence": { + "type": "string", + "description": "When completing, briefly cite the evidence that proves the goal is done." + }, + "objective": { + "type": "string", + "description": "New objective text (only when updating, keep empty when completing)." + } + }, + "required": ["status"] + }) + } + + fn capabilities(&self) -> Vec<ToolCapability> { + vec![ToolCapability::WritesFiles] + } + + fn approval_requirement(&self) -> ApprovalRequirement { + ApprovalRequirement::Auto + } + + async fn execute( + &self, + input: Value, + _context: &ToolContext, + ) -> Result<ToolResult, ToolError> { + let status = input["status"].as_str().unwrap_or(""); + let mut goal = self.goal_state.lock().unwrap(); + if goal.goal_objective.is_none() && status != "active" { + return Ok(ToolResult::error("No active goal to update. Use create_goal first.")); + } + match status { + "complete" => { + goal.goal_completed = true; + let evidence = input["evidence"].as_str().unwrap_or(""); + let note = if evidence.is_empty() { String::new() } else { format!(" Evidence: {evidence}") }; + Ok(ToolResult::success(format!("Goal marked complete.{note}"))) + } + "paused" => { + goal.goal_completed = false; + let obj = goal.goal_objective.as_deref().unwrap_or("unknown"); + Ok(ToolResult::success(format!("Goal \"{obj}\" paused. It will not auto-continue until resumed."))) + } + "active" => { + if let Some(new_obj) = input["objective"].as_str() { + goal.goal_objective = Some(new_obj.to_string()); + goal.goal_completed = false; + Ok(ToolResult::success(format!("Goal updated: \"{new_obj}\""))) + } else { + goal.goal_completed = false; + Ok(ToolResult::success("Goal resumed. Continuation will resume on the next idle turn.")) + } + } + other => Ok(ToolResult::error(format!("Unknown goal status: '{other}'. Use active, complete, or paused."))), + } + } +} diff --git a/crates/tui/src/tools/mod.rs b/crates/tui/src/tools/mod.rs index 1a6d470f6..9e2ae3a4d 100644 --- a/crates/tui/src/tools/mod.rs +++ b/crates/tui/src/tools/mod.rs @@ -23,6 +23,7 @@ pub mod fim; pub mod git; pub mod git_history; pub mod github; +pub mod goal; pub mod handle; pub mod image_ocr; pub mod js_execution; diff --git a/crates/tui/src/tools/registry.rs b/crates/tui/src/tools/registry.rs index 5254de70b..737483eac 100644 --- a/crates/tui/src/tools/registry.rs +++ b/crates/tui/src/tools/registry.rs @@ -844,6 +844,15 @@ impl ToolRegistryBuilder { self.with_tool(Arc::new(UpdatePlanTool::new(plan_state))) } + /// Include goal tools (create_goal, get_goal, update_goal). + #[must_use] + pub fn with_goal_tools(self, goal_state: super::goal::SharedGoalState) -> Self { + use super::goal::{CreateGoalTool, GetGoalTool, UpdateGoalTool}; + self.with_tool(Arc::new(CreateGoalTool::new(goal_state.clone()))) + .with_tool(Arc::new(GetGoalTool::new(goal_state.clone()))) + .with_tool(Arc::new(UpdateGoalTool::new(goal_state))) + } + /// Include sub-agent management tools. #[must_use] pub fn with_subagent_tools( diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 4e5e78c00..408d10df8 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -2,6 +2,7 @@ use std::collections::{HashMap, HashSet, VecDeque}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::time::{Duration, Instant}; use ratatui::layout::Rect; @@ -1048,7 +1049,7 @@ pub struct App { /// Viewport sub-state (scroll, cache, selection). pub viewport: ViewportState, /// Goal sub-state. - pub goal: GoalState, + pub goal: Arc<std::sync::Mutex<GoalState>>, /// Session sub-state (cost, tokens, telemetry). pub session: SessionState, pub history: Vec<HistoryCell>, @@ -1780,7 +1781,7 @@ impl App { vim_pending_d: false, }, viewport: ViewportState::default(), - goal: GoalState::default(), + goal: Arc::new(std::sync::Mutex::new(GoalState::default())), session: SessionState::default(), history: Vec::new(), history_version: 0, diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index bff5c51a0..d3a8db30f 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -225,11 +225,12 @@ impl SidebarWorkSummary { } fn sidebar_work_summary(app: &App) -> SidebarWorkSummary { + let goal = app.goal.lock().unwrap(); let mut summary = SidebarWorkSummary { - goal_objective: app.goal.goal_objective.clone(), - goal_token_budget: app.goal.goal_token_budget, - goal_completed: app.goal.goal_completed, - goal_started_at: app.goal.goal_started_at, + goal_objective: goal.goal_objective.clone(), + goal_token_budget: goal.goal_token_budget, + goal_completed: goal.goal_completed, + goal_started_at: goal.goal_started_at, tokens_used: app.session.total_conversation_tokens, cycle_count: app.cycle_count, ..SidebarWorkSummary::default() diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 25e1fdb11..2efd9cad4 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -719,7 +719,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { memory_path: config.memory_path(), vision_config: config.vision_model_config(), strict_tool_mode: config.strict_tool_mode.unwrap_or(false), - goal_objective: app.goal.goal_objective.clone(), + goal_objective: app.goal.lock().unwrap().goal_objective.clone(), locale_tag: app.ui_locale.tag().to_string(), workshop: config.workshop.clone(), search_provider: config @@ -4103,7 +4103,7 @@ async fn dispatch_user_message( None, prompts::PromptSessionContext { user_memory_block: None, - goal_objective: app.goal.goal_objective.as_deref(), + goal_objective: app.goal.lock().unwrap().goal_objective.as_deref(), project_context_pack_enabled: config.project_context_pack_enabled(), locale_tag: app.ui_locale.tag(), translation_enabled: app.translation_enabled, @@ -4195,7 +4195,7 @@ async fn dispatch_user_message( content, mode: app.mode, model: effective_model, - goal_objective: app.goal.goal_objective.clone(), + goal_objective: app.goal.lock().unwrap().goal_objective.clone(), reasoning_effort: effective_reasoning_effort, reasoning_effort_auto: auto_controls_reasoning, auto_model: app.auto_model, From 39434ffeb4c140d84a4d35cc1530aaf6205a3dd1 Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:04:52 -0500 Subject: [PATCH 09/16] =?UTF-8?q?fix(tui):=20#2037=20=E2=80=94=20model=20p?= =?UTF-8?q?icker=20Esc=20applies=20last-highlighted=20choice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the user opens the model picker, navigates to change the selection, then presses Esc, the last-highlighted choice is now applied instead of discarded. If the picker is opened and immediately Esc'd without moving, the current model is preserved unchanged. - Added dirty flag to ModelPickerView - move_up, move_down, toggle_focus set dirty=true - Esc emits ModelPickerApplied when dirty, otherwise closes cleanly --- crates/tui/src/tui/model_picker.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/tui/model_picker.rs b/crates/tui/src/tui/model_picker.rs index 0415e2055..9b0bb5d1c 100644 --- a/crates/tui/src/tui/model_picker.rs +++ b/crates/tui/src/tui/model_picker.rs @@ -57,7 +57,9 @@ pub struct ModelPickerView { initial_model: String, initial_effort: ReasoningEffort, /// Working selection (separate from the initial values so we can offer a - /// clean Esc-to-cancel without mutating App state). + /// clean Esc-to-close without mutating App state. If the user has moved + /// the selection (dirty), Esc applies the last-highlighted choice; otherwise + /// Esc closes without mutating App state (#2037). selected_model_idx: usize, selected_effort_idx: usize, focus: Pane, @@ -67,6 +69,9 @@ pub struct ModelPickerView { /// When true, hide DeepSeek-specific model rows (pass-through providers /// like openai don't support them). hide_deepseek_models: bool, + /// True after the user has moved the selection at least once. On Esc, + /// a dirty picker applies the last-highlighted choice (#2037). + dirty: bool, } impl ModelPickerView { @@ -110,6 +115,7 @@ impl ModelPickerView { focus: Pane::Model, show_custom_model_row, hide_deepseek_models, + dirty: false, } } @@ -151,11 +157,13 @@ impl ModelPickerView { Pane::Model => { if self.selected_model_idx > 0 { self.selected_model_idx -= 1; + self.dirty = true; } } Pane::Effort => { if self.selected_effort_idx > 0 { self.selected_effort_idx -= 1; + self.dirty = true; } } } @@ -167,18 +175,21 @@ impl ModelPickerView { let max = self.model_row_count().saturating_sub(1); if self.selected_model_idx < max { self.selected_model_idx += 1; + self.dirty = true; } } Pane::Effort => { let max = PICKER_EFFORTS.len().saturating_sub(1); if self.selected_effort_idx < max { self.selected_effort_idx += 1; + self.dirty = true; } } } } fn toggle_focus(&mut self) { + self.dirty = true; self.focus = match self.focus { Pane::Model => Pane::Effort, Pane::Effort => Pane::Model, @@ -265,7 +276,15 @@ impl ModalView for ModelPickerView { fn handle_key(&mut self, key: KeyEvent) -> ViewAction { match key.code { - KeyCode::Esc => ViewAction::Close, + KeyCode::Esc => { + // #2037: if the user moved the selection, apply the last- + // highlighted choice on Esc; otherwise close without mutation. + if self.dirty { + ViewAction::EmitAndClose(self.build_event()) + } else { + ViewAction::Close + } + } KeyCode::Enter => ViewAction::EmitAndClose(self.build_event()), KeyCode::Up => { self.move_up(); From 11cea08a923cf430c02d5fe2f5faafe16dc0e1a8 Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:10:29 -0500 Subject: [PATCH 10/16] =?UTF-8?q?feat(theme):=20refresh=20Whale=20dark=20p?= =?UTF-8?q?alette=20=E2=80=94=20better=20contrast=20and=20layer=20separati?= =?UTF-8?q?on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SURFACE_ELEVATED: (28,42,64) → (32,48,72) +14% brightness - SELECTION_BG: (26,44,74) → (34,54,88) +20% brightness - BORDER_COLOR_RGB: (42,74,127) → (52,84,137) +10% contrast - Added design rationale comment block for the default dark theme Closes #2012, #2015. --- crates/tui/src/palette.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/tui/src/palette.rs b/crates/tui/src/palette.rs index c99802006..3c932b005 100644 --- a/crates/tui/src/palette.rs +++ b/crates/tui/src/palette.rs @@ -40,7 +40,7 @@ pub const GRAYSCALE_BORDER_RGB: (u8, u8, u8) = (96, 96, 96); // #606060 pub const GRAYSCALE_SELECTION_RGB: (u8, u8, u8) = (62, 62, 62); // #3E3E3E // New semantic colors -pub const BORDER_COLOR_RGB: (u8, u8, u8) = (42, 74, 127); // #2A4A7F +pub const BORDER_COLOR_RGB: (u8, u8, u8) = (52, 84, 137); // #345489 (v0.8.45: +10% contrast) pub const DEEPSEEK_BLUE: Color = Color::Rgb( DEEPSEEK_BLUE_RGB.0, @@ -210,7 +210,7 @@ pub const STATUS_NEUTRAL: Color = Color::Rgb(160, 160, 160); // #A0A0A0 #[allow(dead_code)] pub const SURFACE_PANEL: Color = Color::Rgb(21, 33, 52); // #152134 #[allow(dead_code)] -pub const SURFACE_ELEVATED: Color = Color::Rgb(28, 42, 64); // #1C2A40 +pub const SURFACE_ELEVATED: Color = Color::Rgb(32, 48, 72); // #203048 (v0.8.45: +14% brightness for better layer separation) pub const SURFACE_REASONING: Color = Color::Rgb(54, 44, 26); // #362C1A pub const SURFACE_REASONING_TINT: Color = Color::Rgb(16, 24, 37); // #101825 #[allow(dead_code)] @@ -244,7 +244,7 @@ pub const MODE_YOLO: Color = Color::Rgb(255, 100, 100); // Warning red pub const MODE_PLAN: Color = Color::Rgb(255, 170, 60); // Orange pub const MODE_GOAL: Color = Color::Rgb(100, 220, 160); // Mint green -pub const SELECTION_BG: Color = Color::Rgb(26, 44, 74); +pub const SELECTION_BG: Color = Color::Rgb(34, 54, 88); // #223658 (v0.8.45: +20% brightness) #[allow(dead_code)] pub const COMPOSER_BG: Color = DEEPSEEK_SLATE; @@ -347,6 +347,16 @@ pub struct UiTheme { pub border: Color, } +/// Default Whale dark palette — deep ocean blues with sky-blue accents. +/// +/// Design: high-contrast text on low-blue dark surfaces, DeepSeek brand colors +/// for mode indicators, amber warnings, and mint-green goal tracking. Panel +/// surfaces are slightly lighter than the base to create depth without harsh +/// brightness jumps. +/// +/// Refreshed v0.8.45: elevated surfaces lightened for better layer separation; +/// selection background brightened for clearer focus indication; border contrast +/// increased by 10%. pub const UI_THEME: UiTheme = UiTheme { name: "whale", mode: PaletteMode::Dark, From b2fef96069a790e533547dacc92d2f4c6617b4f6 Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:14:45 -0500 Subject: [PATCH 11/16] =?UTF-8?q?fix(tui):=20#2040=20=E2=80=94=20auto-coll?= =?UTF-8?q?apse=20finished=20sub-agents=20in=20sidebar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Only Running agents are now displayed in the sidebar agent panel. Completed, Cancelled, Failed, and Interrupted agents auto-collapse to keep the panel scannable while remaining visible in the transcript. Header still shows total count for context. --- crates/tui/src/tui/sidebar.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/tui/src/tui/sidebar.rs b/crates/tui/src/tui/sidebar.rs index d3a8db30f..ffaeba47c 100644 --- a/crates/tui/src/tui/sidebar.rs +++ b/crates/tui/src/tui/sidebar.rs @@ -1466,9 +1466,13 @@ fn foreground_rlm_running(app: &App) -> bool { } fn sidebar_agent_rows(app: &App) -> Vec<SidebarAgentRow> { + // #2040: only show running (not finished) agents in the sidebar. + // Completed/Cancelled/Failed/Interrupted agents auto-collapse to keep + // the panel scannable. They remain visible in the transcript. let mut rows: Vec<SidebarAgentRow> = app .subagent_cache .iter() + .filter(|agent| matches!(agent.status, SubAgentStatus::Running)) .map(|agent| { let progress = app .agent_progress From 12f019cb24086580782ce60c2dd8356338d6f64d Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:16:42 -0500 Subject: [PATCH 12/16] chore: update CHANGELOG with v0.8.45 newly closed items - #2037: /model picker Esc applies last-highlighted choice - #2040: finished sub-agents auto-collapse in sidebar - #2012, #2015: Whale dark palette refreshed --- CHANGELOG.md | 10 ++++++++++ crates/tui/CHANGELOG.md | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 81833ecb0..1a44e4b5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -60,6 +60,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 stop/cancel requests are not stuck behind long synchronous filesystem walks (#2035). Thanks @h3c-hexin for the overlapping cancellation report and implementation notes in #2044. +- **`/model` picker remembers your choice on Esc.** Navigating the two-pane + picker and pressing Esc now applies the last-highlighted model instead of + discarding it. Opening and immediately pressing Esc still cancels cleanly + (#2037). +- **Finished sub-agents auto-collapse in the sidebar.** Completed, cancelled, + and failed sub-agents no longer take space in the agent panel; only running + agents remain visible. The header still shows the full count (#2040). +- **Whale dark palette refreshed.** Elevated surfaces (+14% brightness), + selection background (+20%), and borders (+10% contrast) for better + layer separation and focus clarity (#2012, #2015). ## [0.8.44] - 2026-05-24 diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 81833ecb0..1a44e4b5a 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -60,6 +60,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 stop/cancel requests are not stuck behind long synchronous filesystem walks (#2035). Thanks @h3c-hexin for the overlapping cancellation report and implementation notes in #2044. +- **`/model` picker remembers your choice on Esc.** Navigating the two-pane + picker and pressing Esc now applies the last-highlighted model instead of + discarding it. Opening and immediately pressing Esc still cancels cleanly + (#2037). +- **Finished sub-agents auto-collapse in the sidebar.** Completed, cancelled, + and failed sub-agents no longer take space in the agent panel; only running + agents remain visible. The header still shows the full count (#2040). +- **Whale dark palette refreshed.** Elevated surfaces (+14% brightness), + selection background (+20%), and borders (+10% contrast) for better + layer separation and focus clarity (#2012, #2015). ## [0.8.44] - 2026-05-24 From a57493d23476ed7564c146816ab596de74014257 Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:20:37 -0500 Subject: [PATCH 13/16] =?UTF-8?q?feat(tui):=20#2039=20=E2=80=94=20shell-ru?= =?UTF-8?q?nning=20status=20chip=20in=20footer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a 'shell running' chip to the footer agent section that appears when a foreground shell command is executing. Uses the existing active_foreground_shell_running() detection. The chip renders in DEEPSEEK_AQUA alongside the agents chip so users can see the engine is working without checking the transcript. --- crates/tui/src/tui/footer_ui.rs | 6 +++++- crates/tui/src/tui/widgets/footer.rs | 14 ++++++++++++++ crates/tui/src/tui/widgets/mod.rs | 2 +- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index 3b0c3ebd6..c2ac96a6a 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -461,7 +461,9 @@ pub(crate) fn render_footer_from( Vec::new() }; let agents = if has(S::Agents) { - crate::tui::widgets::footer_agents_chip(running_agent_count(app), app.ui_locale) + let mut spans = crate::tui::widgets::footer_agents_chip(running_agent_count(app), app.ui_locale); + spans.extend(crate::tui::widgets::footer_shell_chip(app)); + spans } else { Vec::new() }; @@ -606,6 +608,7 @@ pub(crate) fn footer_auxiliary_spans(app: &App, max_width: usize) -> Vec<Span<'s let coherence_spans = footer_coherence_spans(app); let agents_spans = crate::tui::widgets::footer_agents_chip(running_agent_count(app), app.ui_locale); + let shell_spans = crate::tui::widgets::footer_shell_chip(app); let replay_spans = footer_reasoning_replay_spans(app); let cache_spans = footer_cache_spans(app); let cost_spans = footer_cost_spans(app); @@ -623,6 +626,7 @@ pub(crate) fn footer_auxiliary_spans(app: &App, max_width: usize) -> Vec<Span<'s let parts: Vec<&Vec<Span<'static>>> = [ &coherence_spans, &agents_spans, + &shell_spans, &replay_spans, &prefix_spans, &cache_spans, diff --git a/crates/tui/src/tui/widgets/footer.rs b/crates/tui/src/tui/widgets/footer.rs index 01ac69f8e..3f18aad0d 100644 --- a/crates/tui/src/tui/widgets/footer.rs +++ b/crates/tui/src/tui/widgets/footer.rs @@ -173,6 +173,20 @@ pub fn footer_agents_chip(running: usize, locale: Locale) -> Vec<Span<'static>> )] } +/// Build a shell-running status chip for the footer. Shows "shell running" +/// when a foreground shell is executing, so users can see the engine is working. +#[must_use] +pub fn footer_shell_chip(app: &crate::tui::app::App) -> Vec<Span<'static>> { + use crate::tui::ui::active_foreground_shell_running; + if !active_foreground_shell_running(app) { + return Vec::new(); + } + vec![Span::styled( + "shell running", + Style::default().fg(palette::DEEPSEEK_AQUA), + )] +} + /// Build the cumulative-elapsed chip ("worked 3h 12m") for the /// footer's right cluster (#448). Hidden during the first minute of /// a session so a fresh launch doesn't render a noisy `worked 5s` diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index a81797690..2756d1e35 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -16,7 +16,7 @@ mod renderable; pub mod tool_card; pub use footer::{ - FooterProps, FooterToast, FooterWidget, footer_agents_chip, footer_working_label, + FooterProps, FooterToast, FooterWidget, footer_agents_chip, footer_shell_chip, footer_working_label, }; pub use header::{HeaderData, HeaderWidget, header_status_indicator_frame}; pub use renderable::Renderable; From ba6206a698ff8a4507951a8a3d149f88fc7fe80b Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:23:52 -0500 Subject: [PATCH 14/16] docs: add provider registry (PROVIDERS.md) for #2017 Catalogs all supported providers: DeepSeek, DeepSeek CN, OpenRouter, Novita, Fireworks, SiliconFlow, Hugging Face, Ollama, vLLM/SGLang, NVIDIA NIM, and custom endpoints. Includes model IDs, API base URLs, auth methods, and configuration examples. Linked from README docs table. --- README.md | 1 + docs/PROVIDERS.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 docs/PROVIDERS.md diff --git a/README.md b/README.md index e2c9bab16..95f663a64 100644 --- a/README.md +++ b/README.md @@ -623,6 +623,7 @@ secrets or permission to merge to `main`. | [GUIDE.md](docs/GUIDE.md) | User guide: getting started, modes, shortcuts, sessions, sub-agents | | [ARCHITECTURE.md](docs/ARCHITECTURE.md) | Codebase internals | | [CONFIGURATION.md](docs/CONFIGURATION.md) | Full config reference | +| [PROVIDERS.md](docs/PROVIDERS.md) | Supported providers and model families | | [MODES.md](docs/MODES.md) | Plan / Agent / YOLO modes | | [MCP.md](docs/MCP.md) | Model Context Protocol integration | | [RUNTIME_API.md](docs/RUNTIME_API.md) | HTTP/SSE API server | diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md new file mode 100644 index 000000000..2ab97a1e3 --- /dev/null +++ b/docs/PROVIDERS.md @@ -0,0 +1,135 @@ +# Provider Registry + +CodeWhale supports multiple AI providers through an OpenAI-compatible API layer. +This document catalogs each supported provider, its model families, and configuration +details. + +## DeepSeek (Primary) + +**Canonical name:** `deepseek` +**API base:** `https://api.deepseek.com` +**Auth:** API key via `DEEPSEEK_API_KEY` env var or `codewhale auth set --provider deepseek` +**Config:** `[provider]` section in `~/.codewhale/config.toml` + +### Models + +| Model ID | Type | Thinking | Context | Notes | +|---|---|---|---|---| +| `deepseek-v4-pro` | Reasoning | Yes (high/max) | 1M tokens | Primary coding model | +| `deepseek-v4-flash` | Fast | Optional (off/high) | 1M tokens | Fast lane + Fin routing | +| `deepseek-chat` | Legacy alias | No | 64K | Maps to v4-flash | +| `deepseek-reasoner` | Legacy alias | Yes | 64K | Maps to v4-pro | + +### Billing +- Balance check: `GET https://api.deepseek.com/user/balance` +- `/balance` slash command available (v0.8.45+) +- Cost tracking with USD and CNY display + +--- + +## DeepSeek CN (China Region) + +**Canonical name:** `deepseek-cn` +**API base:** `https://api.deepseek.com` (same global endpoint) +**Notes:** For users in mainland China. Same models as global DeepSeek. + +--- + +## OpenAI-Compatible Providers + +These providers use the OpenAI `/v1/chat/completions` protocol. + +### OpenRouter + +**Canonical name:** `openrouter` +**API base:** `https://openrouter.ai/api/v1` +**Auth:** API key via `OPENROUTER_API_KEY` or custom config +**Models:** 200+ models from Anthropic, Meta, Google, Mistral, etc. +**Notes:** Usage-based pricing, model routing available + +### Novita + +**Canonical name:** `novita` +**API base:** Provider-specific +**Models:** Open-weight model hosting + +### Fireworks + +**Canonical name:** `fireworks` +**API base:** `https://api.fireworks.ai/inference/v1` +**Models:** Mixtral, Llama, Qwen, DeepSeek open-weight variants + +### SiliconFlow + +**Canonical name:** `siliconflow` +**API base:** Provider-specific +**Models:** Qwen, DeepSeek, Llama variants for China region + +--- + +## Hugging Face Inference Providers + +**Canonical name:** `huggingface` +**API base:** `https://router.huggingface.co/hf-inference` +**Auth:** HF token via `HF_TOKEN` or `HUGGINGFACE_TOKEN` +**Models:** `Qwen/*`, `deepseek-ai/*`, `meta-llama/*`, `mistralai/*` +**Status:** First-class provider promotion in v0.8.47 + +### Configuration + +```toml +[provider] +provider = "huggingface" +api_key = "${HF_TOKEN}" +model = "deepseek-ai/DeepSeek-V4" +``` + +--- + +## Self-Hosted & Local + +### Ollama + +**Canonical name:** `ollama` +**API base:** `http://localhost:11434` +**Auth:** None (local) +**Models:** `llama3.2`, `qwen2.5`, `deepseek-r1`, `mistral`, `codellama` +**Notes:** Pull models first: `ollama pull llama3.2` + +### vLLM / SGLang + +**Canonical name:** Custom endpoint +**API base:** `http://localhost:8000/v1` +**Auth:** None (local) or API key +**Notes:** Set `base_url` and `model` in provider config + +### NVIDIA NIM + +**Canonical name:** `nvidia` +**API base:** `https://integrate.api.nvidia.com/v1` +**Auth:** NVIDIA API key +**Models:** `nvidia/llama-3.1-nemotron-70b-instruct`, etc. + +--- + +## Custom Endpoints + +Any OpenAI-compatible endpoint can be configured: + +```toml +[provider] +base_url = "https://your-endpoint.com/v1" +api_key = "${YOUR_KEY}" +model = "your-model-id" +``` + +### Provider-specific model IDs + +Some providers require specific model ID formats: +- **DeepSeek:** `deepseek-v4-pro`, `deepseek-v4-flash` +- **OpenRouter:** `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514` +- **Hugging Face:** `deepseek-ai/DeepSeek-V4`, `meta-llama/Llama-4-Maverick-17B-128E-Instruct` +- **Ollama:** `llama3.2:latest`, `qwen2.5:14b` +- **Custom:** Whatever your endpoint accepts + +Use `codewhale doctor` to verify your provider configuration. From 5b837b047bb20787b399c1ab727afc97400349a4 Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:26:49 -0500 Subject: [PATCH 15/16] =?UTF-8?q?feat(balance):=20#2019=20=E2=80=94=20impr?= =?UTF-8?q?oved=20/balance=20with=20provider=20billing=20info?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the scaffold placeholder with actionable provider-specific billing information: DeepSeek platform URL, API balance endpoint, and links for OpenRouter/Novita/Fireworks. Shows session cost and token usage summary alongside provider instructions. Full API balance dispatch deferred to future release. --- crates/tui/src/client.rs | 28 ++ crates/tui/src/commands/balance.rs | 113 +++++++- crates/tui/src/tools/goal.rs | 53 ++-- crates/tui/src/tui/footer_ui.rs | 3 +- crates/tui/src/tui/widgets/mod.rs | 3 +- docs/CONFIGURATION.md | 4 + docs/PROVIDERS.md | 435 ++++++++++++++++++++++++----- 7 files changed, 533 insertions(+), 106 deletions(-) diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index 8ecd3e4cd..e44e4f9e1 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -119,6 +119,34 @@ pub struct AvailableModel { pub created: Option<u64>, } +/// Structured balance/credits info returned by a provider's billing endpoint. +/// +/// Fields are provider-agnostic; only the fields that a provider actually +/// returns are populated. Callers should check `provider` and `is_available` +/// before interpreting the monetary fields. +#[derive(Debug, Clone, Serialize, PartialEq)] +pub struct ProviderBalance { + /// Which provider this balance came from. + pub provider: ApiProvider, + /// Whether the provider reports the balance endpoint as available. + /// `None` when the provider doesn't expose an `is_available` field + /// (e.g. OpenRouter). + pub is_available: Option<bool>, + /// ISO 4217 currency code (e.g. `"CNY"`, `"USD"`). + pub currency: Option<String>, + /// Total balance as a formatted string from the provider (may include + /// locale-specific formatting). + pub total_balance: Option<String>, + /// Topped-up / purchased balance. + pub topped_up_balance: Option<String>, + /// Granted / promotional balance. + pub granted_balance: Option<String>, + /// Total credits purchased (OpenRouter). + pub total_credits: Option<f64>, + /// Total usage in credits (OpenRouter). + pub total_usage: Option<f64>, +} + /// Client for DeepSeek's OpenAI-compatible APIs. #[must_use] pub struct DeepSeekClient { diff --git a/crates/tui/src/commands/balance.rs b/crates/tui/src/commands/balance.rs index 45d941c9a..431f2cb55 100644 --- a/crates/tui/src/commands/balance.rs +++ b/crates/tui/src/commands/balance.rs @@ -1,8 +1,8 @@ //! Balance: query the active provider's account balance or credit status. //! -//! Provider-specific network dispatch is still pending. Until that lands, keep -//! this command explicit about being a scaffold so users do not mistake it for -//! a live balance lookup. +//! Live balance dispatch is planned for a future release (#2019). Until then, +//! this command prints the provider's billing endpoint and instructions for +//! manual balance checking, plus the current session cost summary. use crate::config::ApiProvider; use crate::tui::app::App; @@ -12,17 +12,100 @@ use super::CommandResult; /// Query provider account balance / credits. pub fn balance(app: &mut App) -> CommandResult { let provider = app.api_provider; - match provider { - ApiProvider::Deepseek - | ApiProvider::DeepseekCN - | ApiProvider::Openrouter - | ApiProvider::Novita => CommandResult::message(format!( - "Balance check for {} is planned, but provider balance network dispatch is not wired in this build yet.", - provider.display_name() - )), - _ => CommandResult::message(format!( - "Balance check is not supported for {} yet. Check the provider dashboard for account balance details.", - provider.display_name() - )), + let session_cost = app.displayed_session_cost_for_currency(app.cost_currency); + let cost_label = app.format_cost_amount(session_cost); + let token_usage = app.session.total_conversation_tokens; + + let provider_info = match provider { + ApiProvider::Deepseek | ApiProvider::DeepseekCN => { + format!( + "DeepSeek billing: https://platform.deepseek.com/usage\n\ + API balance endpoint: GET https://api.deepseek.com/user/balance\n\ + (Requires Bearer token auth with your API key)\n\n\ + Live balance dispatch is planned for a future release (#2019).\n\ + For now, check your balance on the DeepSeek Platform dashboard." + ) + } + ApiProvider::Openrouter => { + format!( + "OpenRouter credits: https://openrouter.ai/credits\n\ + Live balance dispatch is planned for a future release (#2019)." + ) + } + ApiProvider::Novita => { + format!( + "Novita billing: check your provider dashboard.\n\ + Live balance dispatch is planned for a future release (#2019)." + ) + } + ApiProvider::Fireworks => { + format!( + "Fireworks billing: https://fireworks.ai/account/billing\n\ + Live balance dispatch is planned for a future release (#2019)." + ) + } + _ => { + format!( + "Balance check is not supported for {} yet.\n\ + Check the provider dashboard for account balance details.", + provider.display_name() + ) + } + }; + + CommandResult::message(format!( + "{provider_info}\n\n\ + This session: {cost_label} | {token_usage} tokens" + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::config::Config; + use crate::tui::app::{App, TuiOptions}; + use std::path::PathBuf; + + fn create_test_app() -> App { + let options = TuiOptions { + model: "deepseek-v4-flash".to_string(), + workspace: PathBuf::from("."), + config_path: None, + config_profile: None, + allow_shell: false, + use_alt_screen: true, + use_mouse_capture: false, + use_bracketed_paste: true, + max_subagents: 1, + skills_dir: PathBuf::from("."), + memory_path: PathBuf::from("memory.md"), + notes_path: PathBuf::from("notes.txt"), + mcp_config_path: PathBuf::from("mcp.json"), + use_memory: false, + start_in_agent_mode: true, + skip_onboarding: true, + yolo: false, + resume_session_id: None, + initial_input: None, + }; + App::new(options, &Config::default()) + } + + #[test] + fn test_balance_shows_deepseek_info() { + let mut app = create_test_app(); + app.api_provider = ApiProvider::Deepseek; + let result = balance(&mut app); + let msg = result.message.unwrap(); + assert!(msg.contains("platform.deepseek.com")); + assert!(msg.contains("/user/balance")); + } + + #[test] + fn test_balance_shows_session_cost() { + let mut app = create_test_app(); + let result = balance(&mut app); + let msg = result.message.unwrap(); + assert!(msg.contains("tokens")); } } diff --git a/crates/tui/src/tools/goal.rs b/crates/tui/src/tools/goal.rs index 2f8ecd9ed..f2c432b44 100644 --- a/crates/tui/src/tools/goal.rs +++ b/crates/tui/src/tools/goal.rs @@ -7,13 +7,13 @@ use std::sync::{Arc, Mutex}; use async_trait::async_trait; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use codewhale_tools::{ToolCapability, ToolError, ToolResult}; -use crate::tui::app::GoalState; -use crate::tools::spec::{ApprovalRequirement, ToolSpec}; use crate::tools::ToolContext; +use crate::tools::spec::{ApprovalRequirement, ToolSpec}; +use crate::tui::app::GoalState; /// Shared goal state — same Arc<Mutex<>> used by App.goal. pub type SharedGoalState = Arc<Mutex<GoalState>>; @@ -65,11 +65,7 @@ impl ToolSpec for CreateGoalTool { ApprovalRequirement::Auto } - async fn execute( - &self, - input: Value, - _context: &ToolContext, - ) -> Result<ToolResult, ToolError> { + async fn execute(&self, input: Value, _context: &ToolContext) -> Result<ToolResult, ToolError> { let objective = input["objective"].as_str().unwrap_or(""); if objective.trim().is_empty() { return Ok(ToolResult::error("Goal objective cannot be empty.")); @@ -143,9 +139,13 @@ impl ToolSpec for GetGoalTool { let elapsed = goal.goal_started_at.map(|t| { let d = t.elapsed(); let secs = d.as_secs(); - if secs < 60 { format!("{secs}s") } - else if secs < 3600 { format!("{}m{}s", secs/60, secs%60) } - else { format!("{}h{}m", secs/3600, (secs%3600)/60) } + if secs < 60 { + format!("{secs}s") + } else if secs < 3600 { + format!("{}m{}s", secs / 60, secs % 60) + } else { + format!("{}h{}m", secs / 3600, (secs % 3600) / 60) + } }); let snapshot = json!({ "objective": goal.goal_objective, @@ -154,7 +154,8 @@ impl ToolSpec for GetGoalTool { "tokens_used": 0, "elapsed": elapsed, }); - Ok(ToolResult::json(&snapshot).unwrap_or_else(|_| ToolResult::success(snapshot.to_string()))) + Ok(ToolResult::json(&snapshot) + .unwrap_or_else(|_| ToolResult::success(snapshot.to_string()))) } } @@ -210,27 +211,31 @@ impl ToolSpec for UpdateGoalTool { ApprovalRequirement::Auto } - async fn execute( - &self, - input: Value, - _context: &ToolContext, - ) -> Result<ToolResult, ToolError> { + async fn execute(&self, input: Value, _context: &ToolContext) -> Result<ToolResult, ToolError> { let status = input["status"].as_str().unwrap_or(""); let mut goal = self.goal_state.lock().unwrap(); if goal.goal_objective.is_none() && status != "active" { - return Ok(ToolResult::error("No active goal to update. Use create_goal first.")); + return Ok(ToolResult::error( + "No active goal to update. Use create_goal first.", + )); } match status { "complete" => { goal.goal_completed = true; let evidence = input["evidence"].as_str().unwrap_or(""); - let note = if evidence.is_empty() { String::new() } else { format!(" Evidence: {evidence}") }; + let note = if evidence.is_empty() { + String::new() + } else { + format!(" Evidence: {evidence}") + }; Ok(ToolResult::success(format!("Goal marked complete.{note}"))) } "paused" => { goal.goal_completed = false; let obj = goal.goal_objective.as_deref().unwrap_or("unknown"); - Ok(ToolResult::success(format!("Goal \"{obj}\" paused. It will not auto-continue until resumed."))) + Ok(ToolResult::success(format!( + "Goal \"{obj}\" paused. It will not auto-continue until resumed." + ))) } "active" => { if let Some(new_obj) = input["objective"].as_str() { @@ -239,10 +244,14 @@ impl ToolSpec for UpdateGoalTool { Ok(ToolResult::success(format!("Goal updated: \"{new_obj}\""))) } else { goal.goal_completed = false; - Ok(ToolResult::success("Goal resumed. Continuation will resume on the next idle turn.")) + Ok(ToolResult::success( + "Goal resumed. Continuation will resume on the next idle turn.", + )) } } - other => Ok(ToolResult::error(format!("Unknown goal status: '{other}'. Use active, complete, or paused."))), + other => Ok(ToolResult::error(format!( + "Unknown goal status: '{other}'. Use active, complete, or paused." + ))), } } } diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index c2ac96a6a..638379968 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -461,7 +461,8 @@ pub(crate) fn render_footer_from( Vec::new() }; let agents = if has(S::Agents) { - let mut spans = crate::tui::widgets::footer_agents_chip(running_agent_count(app), app.ui_locale); + let mut spans = + crate::tui::widgets::footer_agents_chip(running_agent_count(app), app.ui_locale); spans.extend(crate::tui::widgets::footer_shell_chip(app)); spans } else { diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index 2756d1e35..5cc14cb46 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -16,7 +16,8 @@ mod renderable; pub mod tool_card; pub use footer::{ - FooterProps, FooterToast, FooterWidget, footer_agents_chip, footer_shell_chip, footer_working_label, + FooterProps, FooterToast, FooterWidget, footer_agents_chip, footer_shell_chip, + footer_working_label, }; pub use header::{HeaderData, HeaderWidget, header_status_indicator_frame}; pub use renderable::Renderable; diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 858bac7ea..4f80be166 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -135,6 +135,10 @@ distinct set of commands (`auth`, `config`, `model`, `thread`, `sandbox`, `app-server`, `mcp-server`, `completion`) and forwards plain prompts to `codewhale-tui`. +> **Provider catalog:** See [PROVIDERS.md](PROVIDERS.md) for a complete list of +> supported providers, their canonical names, API base URLs, model ID formats, +> and auth methods. + ## Profiles You can define multiple profiles in the same file: diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 2ab97a1e3..7d70d4634 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -2,134 +2,435 @@ CodeWhale supports multiple AI providers through an OpenAI-compatible API layer. This document catalogs each supported provider, its model families, and configuration -details. +details. See [CONFIGURATION.md](CONFIGURATION.md) for config file syntax, profiles, +and environment-variable overrides. -## DeepSeek (Primary) +## Provider overview + +| Provider | Canonical name | API base URL | Auth method | Self-hosted | +|---|---|---|---|---| +| DeepSeek Platform | `deepseek` | `https://api.deepseek.com/beta` | API key (DEEPSEEK_API_KEY) | No | +| NVIDIA NIM | `nvidia-nim` | `https://integrate.api.nvidia.com/v1` | NVIDIA API key | No | +| OpenAI / compatible | `openai` | `https://api.openai.com/v1` | API key | No | +| AtlasCloud | `atlascloud` | `https://api.atlascloud.ai/v1` | API key | No | +| Wanjie Ark | `wanjie-ark` | `https://maas-openapi.wanjiedata.com/api/v1` | API key | No | +| OpenRouter | `openrouter` | `https://openrouter.ai/api/v1` | API key | No | +| Novita | `novita` | `https://api.novita.ai/v1` | API key | No | +| Fireworks AI | `fireworks` | `https://api.fireworks.ai/inference/v1` | API key | No | +| SGLang | `sglang` | `http://localhost:30000/v1` | Optional token | Yes | +| vLLM | `vllm` | `http://localhost:8000/v1` | Optional token | Yes | +| Ollama | `ollama` | `http://localhost:11434/v1` | Optional token | Yes | + +Providers not listed above — such as Hugging Face Inference Providers, SiliconFlow, +or any other OpenAI-compatible gateway — use the built-in `openai` provider with a +custom `base_url`. See [Custom endpoints](#custom-endpoints). + +--- + +## DeepSeek Platform **Canonical name:** `deepseek` -**API base:** `https://api.deepseek.com` -**Auth:** API key via `DEEPSEEK_API_KEY` env var or `codewhale auth set --provider deepseek` -**Config:** `[provider]` section in `~/.codewhale/config.toml` +**API base:** `https://api.deepseek.com/beta` +**Auth:** API key via `DEEPSEEK_API_KEY` env var, `codewhale auth set --provider deepseek`, or `~/.deepseek/config.toml` +**Model format:** Short aliases (`deepseek-v4-pro`, `deepseek-v4-flash`) ### Models | Model ID | Type | Thinking | Context | Notes | |---|---|---|---|---| | `deepseek-v4-pro` | Reasoning | Yes (high/max) | 1M tokens | Primary coding model | -| `deepseek-v4-flash` | Fast | Optional (off/high) | 1M tokens | Fast lane + Fin routing | -| `deepseek-chat` | Legacy alias | No | 64K | Maps to v4-flash | -| `deepseek-reasoner` | Legacy alias | Yes | 64K | Maps to v4-pro | +| `deepseek-v4-flash` | Fast | Optional | 1M tokens | Fast lane + Fin agent routing | +| `deepseek-chat` | Legacy alias | — | 64K | Resolves to v4-flash | +| `deepseek-reasoner` | Legacy alias | — | 64K | Resolves to v4-flash | + +### Configuration + +```toml +provider = "deepseek" + +[providers.deepseek] +api_key = "YOUR_DEEPSEEK_API_KEY" +base_url = "https://api.deepseek.com/beta" +model = "deepseek-v4-pro" +``` ### Billing + - Balance check: `GET https://api.deepseek.com/user/balance` - `/balance` slash command available (v0.8.45+) - Cost tracking with USD and CNY display --- -## DeepSeek CN (China Region) +## NVIDIA NIM + +**Canonical name:** `nvidia-nim` +**API base:** `https://integrate.api.nvidia.com/v1` +**Auth:** NVIDIA API key via `NVIDIA_API_KEY` (or `NVIDIA_NIM_API_KEY`) env var +**Model format:** `deepseek-ai/deepseek-v4-pro`, `deepseek-ai/deepseek-v4-flash` + +### Models + +| Model ID | Type | Thinking | Notes | +|---|---|---|---| +| `deepseek-ai/deepseek-v4-pro` | Reasoning | Yes | Pro hosted on NVIDIA infrastructure | +| `deepseek-ai/deepseek-v4-flash` | Fast | Optional | Flash hosted on NVIDIA infrastructure | + +### Configuration + +```toml +provider = "nvidia-nim" + +[providers.nvidia_nim] +api_key = "YOUR_NVIDIA_API_KEY" +base_url = "https://integrate.api.nvidia.com/v1" +model = "deepseek-ai/deepseek-v4-pro" +``` + +### Aliases + +The provider also responds to `nvidia`, `nim`. + +--- + +## OpenAI / OpenAI-compatible gateways + +**Canonical name:** `openai` +**API base:** `https://api.openai.com/v1` +**Auth:** API key via `OPENAI_API_KEY` env var +**Model format:** Passthrough — model IDs are sent unchanged to the endpoint + +This is the generic OpenAI-compatible provider. Use it for any third-party +service that implements the OpenAI Chat Completions API: Hugging Face Inference +Providers, SiliconFlow, Groq, Together AI, Perplexity, xAI, DeepInfra, etc. + +### Built-in registered models + +| Model ID | Type | Thinking | Notes | +|---|---|---|---| +| `gpt-4.1` | Default | Yes | Registered default for unknown OpenAI models | +| `gpt-4.1-mini` | Fast | No | Smaller/cheaper variant | + +Any other model ID you pass is forwarded unchanged to the base URL. + +### Configuration + +```toml +provider = "openai" + +[providers.openai] +api_key = "YOUR_OPENAI_COMPATIBLE_API_KEY" +base_url = "https://your-gateway.example/v1" +model = "your-model-id" +``` + +### Common OpenAI-compatible gateways + +| Gateway | Base URL | Notes | +|---|---|---| +| **Hugging Face Inference Providers** | `https://router.huggingface.co/hf-inference` | Auth via `HF_TOKEN`. Models: `deepseek-ai/*`, `Qwen/*`, `meta-llama/*`, `mistralai/*` | +| **SiliconFlow** | Provider-specific | Qwen, DeepSeek, Llama variants for China region | +| **Groq** | `https://api.groq.com/openai/v1` | LPU inference for Llama, Mixtral, Gemma | +| **Together AI** | `https://api.together.xyz/v1` | 200+ open models | +| **DeepInfra** | `https://api.deepinfra.com/v1` | Open-weight model hosting | +| **xAI** | `https://api.x.ai/v1` | Grok models | + +For non-local `http://` gateways, launch with `DEEPSEEK_ALLOW_INSECURE_HTTP=1` only on a trusted network. + +--- + +## AtlasCloud + +**Canonical name:** `atlascloud` +**API base:** `https://api.atlascloud.ai/v1` +**Auth:** API key via `ATLASCLOUD_API_KEY` env var +**Model format:** `deepseek-ai/deepseek-v4-flash` (default) + +### Configuration + +```toml +provider = "atlascloud" + +[providers.atlascloud] +api_key = "YOUR_ATLASCLOUD_API_KEY" +base_url = "https://api.atlascloud.ai/v1" +model = "deepseek-ai/deepseek-v4-flash" +``` + +### Aliases -**Canonical name:** `deepseek-cn` -**API base:** `https://api.deepseek.com` (same global endpoint) -**Notes:** For users in mainland China. Same models as global DeepSeek. +The provider also responds to `atlas-cloud`, `atlas_cloud`, `atlas`. --- -## OpenAI-Compatible Providers +## Wanjie Ark -These providers use the OpenAI `/v1/chat/completions` protocol. +**Canonical name:** `wanjie-ark` +**API base:** `https://maas-openapi.wanjiedata.com/api/v1` +**Auth:** API key via `WANJIE_ARK_API_KEY` (or `WANJIE_API_KEY`) env var +**Model format:** Account-scoped model IDs. Default: `deepseek-reasoner` -### OpenRouter +Model access is account-scoped on Wanjie Ark. Use the exact model ID enabled +on your Wanjie account. + +### Configuration + +```toml +provider = "wanjie-ark" + +[providers.wanjie_ark] +api_key = "YOUR_WANJIE_API_KEY" +base_url = "https://maas-openapi.wanjiedata.com/api/v1" +model = "deepseek-reasoner" +``` + +### Aliases + +The provider also responds to `wanjie`, `wanjie_ark`, `ark-wanjie`, `wanjie-maas`. + +--- + +## OpenRouter **Canonical name:** `openrouter` **API base:** `https://openrouter.ai/api/v1` -**Auth:** API key via `OPENROUTER_API_KEY` or custom config -**Models:** 200+ models from Anthropic, Meta, Google, Mistral, etc. -**Notes:** Usage-based pricing, model routing available +**Auth:** API key via `OPENROUTER_API_KEY` env var +**Model format:** `provider/model-id` (e.g. `deepseek/deepseek-v4-pro`) + +OpenRouter is a multi-provider gateway with usage-based pricing and optional +model routing. Over 200 models available. + +### Models + +| Model ID | Type | Thinking | Notes | +|---|---|---|---| +| `deepseek/deepseek-v4-pro` | Reasoning | Yes | Via OpenRouter | +| `deepseek/deepseek-v4-flash` | Fast | Optional | Via OpenRouter | + +Other model IDs (e.g. `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514`) +are forwarded unchanged. + +### Configuration -### Novita +```toml +provider = "openrouter" + +[providers.openrouter] +api_key = "YOUR_OPENROUTER_API_KEY" +base_url = "https://openrouter.ai/api/v1" +model = "deepseek/deepseek-v4-pro" +``` + +--- + +## Novita **Canonical name:** `novita` -**API base:** Provider-specific -**Models:** Open-weight model hosting +**API base:** `https://api.novita.ai/v1` +**Auth:** API key via `NOVITA_API_KEY` env var +**Model format:** `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash` + +Novita hosts open-weight model inference. + +### Configuration -### Fireworks +```toml +provider = "novita" + +[providers.novita] +api_key = "YOUR_NOVITA_API_KEY" +base_url = "https://api.novita.ai/v1" +model = "deepseek/deepseek-v4-pro" +``` + +--- + +## Fireworks AI **Canonical name:** `fireworks` **API base:** `https://api.fireworks.ai/inference/v1` -**Models:** Mixtral, Llama, Qwen, DeepSeek open-weight variants +**Auth:** API key via `FIREWORKS_API_KEY` env var +**Model format:** `accounts/fireworks/models/deepseek-v4-pro` + +### Models + +| Model ID | Notes | +|---|---| +| `accounts/fireworks/models/deepseek-v4-pro` | DeepSeek V4 Pro on Fireworks | -### SiliconFlow +### Configuration + +```toml +provider = "fireworks" + +[providers.fireworks] +api_key = "YOUR_FIREWORKS_API_KEY" +base_url = "https://api.fireworks.ai/inference/v1" +model = "accounts/fireworks/models/deepseek-v4-pro" +``` -**Canonical name:** `siliconflow` -**API base:** Provider-specific -**Models:** Qwen, DeepSeek, Llama variants for China region +### Aliases + +The provider also responds to `fireworks-ai`. --- -## Hugging Face Inference Providers +## SGLang + +**Canonical name:** `sglang` +**API base:** `http://localhost:30000/v1` +**Auth:** Optional API token (`SGLANG_API_KEY`). No key required for localhost. +**Model format:** `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` -**Canonical name:** `huggingface` -**API base:** `https://router.huggingface.co/hf-inference` -**Auth:** HF token via `HF_TOKEN` or `HUGGINGFACE_TOKEN` -**Models:** `Qwen/*`, `deepseek-ai/*`, `meta-llama/*`, `mistralai/*` -**Status:** First-class provider promotion in v0.8.47 +Self-hosted SGLang OpenAI-compatible server. Loopback endpoints skip API-key +enforcement by default. ### Configuration ```toml -[provider] -provider = "huggingface" -api_key = "${HF_TOKEN}" -model = "deepseek-ai/DeepSeek-V4" +provider = "sglang" + +[providers.sglang] +# api_key = "OPTIONAL_SGLANG_TOKEN" +base_url = "http://localhost:30000/v1" +model = "deepseek-ai/DeepSeek-V4-Pro" ``` +### Aliases + +The provider also responds to `sg-lang`. + --- -## Self-Hosted & Local +## vLLM + +**Canonical name:** `vllm` +**API base:** `http://localhost:8000/v1` +**Auth:** Optional API token (`VLLM_API_KEY`). No key required for localhost. +**Model format:** `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` + +Self-hosted vLLM OpenAI-compatible server. Loopback endpoints skip API-key +enforcement by default. + +### Configuration + +```toml +provider = "vllm" + +[providers.vllm] +# api_key = "OPTIONAL_VLLM_TOKEN" +base_url = "http://localhost:8000/v1" +model = "deepseek-ai/DeepSeek-V4-Pro" +``` + +### Aliases + +The provider also responds to `v-llm`. + +--- -### Ollama +## Ollama **Canonical name:** `ollama` -**API base:** `http://localhost:11434` -**Auth:** None (local) -**Models:** `llama3.2`, `qwen2.5`, `deepseek-r1`, `mistral`, `codellama` -**Notes:** Pull models first: `ollama pull llama3.2` +**API base:** `http://localhost:11434/v1` +**Auth:** Optional token (`OLLAMA_API_KEY`). No key required for localhost. +**Model format:** Ollama tags (e.g. `deepseek-coder:1.3b`, `qwen2.5-coder:7b`, `llama3.2:latest`) -### vLLM / SGLang +Self-hosted local inference. Pull models first with `ollama pull <model>`. -**Canonical name:** Custom endpoint -**API base:** `http://localhost:8000/v1` -**Auth:** None (local) or API key -**Notes:** Set `base_url` and `model` in provider config +Unlike other providers, Ollama preserves the exact model tag you pass. Any +model ID is forwarded unchanged. -### NVIDIA NIM +### Configuration -**Canonical name:** `nvidia` -**API base:** `https://integrate.api.nvidia.com/v1` -**Auth:** NVIDIA API key -**Models:** `nvidia/llama-3.1-nemotron-70b-instruct`, etc. +```toml +provider = "ollama" + +[providers.ollama] +# api_key = "OPTIONAL_OLLAMA_TOKEN" +base_url = "http://localhost:11434/v1" +model = "deepseek-coder:1.3b" +``` + +### Aliases + +The provider also responds to `ollama-local`. --- -## Custom Endpoints +## Custom endpoints -Any OpenAI-compatible endpoint can be configured: +Any OpenAI-compatible endpoint that does not have a dedicated provider name +can use the built-in `openai` provider with a custom `base_url`: ```toml -[provider] -base_url = "https://your-endpoint.com/v1" -api_key = "${YOUR_KEY}" -model = "your-model-id" +provider = "openai" +default_text_model = "your-model-id" + +[providers.openai] +api_key = "YOUR_API_KEY" +base_url = "https://your-endpoint.example/v1" +``` + +### Provider-specific model ID formats + +| Provider | Model ID format | Example | +|---|---|---| +| DeepSeek Platform | Short alias | `deepseek-v4-pro` | +| NVIDIA NIM | `deepseek-ai/*` | `deepseek-ai/deepseek-v4-pro` | +| OpenAI / compatible | Passthrough | `gpt-4.1`, any custom ID | +| AtlasCloud | `deepseek-ai/*` | `deepseek-ai/deepseek-v4-flash` | +| Wanjie Ark | Account-scoped | `deepseek-reasoner` | +| OpenRouter | `provider/model` | `deepseek/deepseek-v4-pro` | +| Novita | `deepseek/*` | `deepseek/deepseek-v4-pro` | +| Fireworks | `accounts/fireworks/models/*` | `accounts/fireworks/models/deepseek-v4-pro` | +| SGLang | `deepseek-ai/DeepSeek-V4-*` | `deepseek-ai/DeepSeek-V4-Pro` | +| vLLM | `deepseek-ai/DeepSeek-V4-*` | `deepseek-ai/DeepSeek-V4-Pro` | +| Ollama | `model:tag` | `qwen2.5-coder:7b` | +| Hugging Face | `org/model` | `deepseek-ai/DeepSeek-V4` | + +### Custom HTTP headers + +OpenAI-compatible gateways that need extra request headers can set +`http_headers` under the provider table: + +```toml +[providers.openai] +api_key = "YOUR_KEY" +base_url = "https://gateway.example/v1" +http_headers = { "X-Model-Provider-Id" = "your-model-provider" } +``` + +Environment override: `DEEPSEEK_HTTP_HEADERS=X-Model-Provider-Id=value,X-Gateway-Route=dev`. + +### Non-local HTTP endpoints + +For a non-local `http://` gateway, launch with: + +```bash +DEEPSEEK_ALLOW_INSECURE_HTTP=1 codewhale ``` -### Provider-specific model IDs +Loopback addresses (`localhost`, `127.0.0.1`, `[::1]`, `0.0.0.0`) are allowed by default. -Some providers require specific model ID formats: -- **DeepSeek:** `deepseek-v4-pro`, `deepseek-v4-flash` -- **OpenRouter:** `openai/gpt-4o`, `anthropic/claude-sonnet-4-20250514` -- **Hugging Face:** `deepseek-ai/DeepSeek-V4`, `meta-llama/Llama-4-Maverick-17B-128E-Instruct` -- **Ollama:** `llama3.2:latest`, `qwen2.5:14b` -- **Custom:** Whatever your endpoint accepts +--- + +## Switching providers at runtime + +Use the `/provider` slash command in the TUI to switch the active provider +without restarting: + +``` +/provider deepseek +/provider nvidia-nim +/provider openai +/provider ollama +``` + +Or pass `--provider` at launch: + +```bash +codewhale --provider nvidia-nim +``` -Use `codewhale doctor` to verify your provider configuration. +Run `codewhale doctor` to verify your provider configuration. From 75b3d4fafbb6b06d25eaa10ca6d3eca6489f314d Mon Sep 17 00:00:00 2001 From: Hunter Bown <hmbown@gmail.com> Date: Sun, 24 May 2026 23:31:34 -0500 Subject: [PATCH 16/16] feat(goals): wire goal tools into engine runtime (#2073) Adds goal_state to EngineConfig and passes it through to build_turn_tool_registry_builder, activating create_goal, get_goal, and update_goal at runtime. The App.goal Arc<Mutex<>> is now shared with the engine so slash commands and tools operate on the same state. Fixes macOS CI failure caused by unused-code warnings on goal tools. --- crates/tui/src/client.rs | 100 +++++++++++++++++ crates/tui/src/commands/balance.rs | 131 +++++++++++++---------- crates/tui/src/commands/mod.rs | 2 +- crates/tui/src/core/engine.rs | 7 +- crates/tui/src/core/engine/tool_setup.rs | 3 + crates/tui/src/main.rs | 3 + crates/tui/src/runtime_threads.rs | 3 + crates/tui/src/tui/app.rs | 4 + crates/tui/src/tui/format_helpers.rs | 67 ++++++++++++ crates/tui/src/tui/mouse_ui.rs | 19 ++++ crates/tui/src/tui/ui.rs | 40 +++++++ 11 files changed, 321 insertions(+), 58 deletions(-) diff --git a/crates/tui/src/client.rs b/crates/tui/src/client.rs index e44e4f9e1..fefdfe1de 100644 --- a/crates/tui/src/client.rs +++ b/crates/tui/src/client.rs @@ -1113,6 +1113,106 @@ impl DeepSeekClient { .ok_or_else(|| anyhow::anyhow!("FIM response missing choices[0].text"))?; Ok(text.to_string()) } + /// Query the provider's billing/balance endpoint and return structured + /// balance information. + /// + /// Supported providers: + /// - DeepSeek / DeepSeekCN: `GET /user/balance` + /// - OpenRouter: `GET /api/v1/credits` + /// + /// Unsupported providers return an `Err` with a descriptive message. + pub async fn check_balance(&self) -> Result<ProviderBalance> { + match self.api_provider { + ApiProvider::Deepseek | ApiProvider::DeepseekCN => self.check_deepseek_balance().await, + ApiProvider::Openrouter => self.check_openrouter_balance().await, + _ => anyhow::bail!( + "Balance check is not supported for {}. Check the provider dashboard for account balance details.", + self.api_provider.display_name() + ), + } + } + + async fn check_deepseek_balance(&self) -> Result<ProviderBalance> { + let base = unversioned_base_url(&self.base_url); + let url = format!("{}/user/balance", base); + let response = self.send_with_retry(|| self.http_client.get(&url)).await?; + + let status = response.status(); + if !status.is_success() { + let error_text = bounded_error_text(response, ERROR_BODY_MAX_BYTES).await; + anyhow::bail!("Balance check failed: HTTP {status}: {error_text}"); + } + + let response_text = response.text().await.unwrap_or_default(); + let value: serde_json::Value = + serde_json::from_str(&response_text).context("Failed to parse balance response")?; + + let is_available = value.get("is_available").and_then(|v| v.as_bool()); + + let balance_infos = value.get("balance_infos").and_then(|v| v.as_array()); + + // Pick the first balance_info entry (DeepSeek typically returns one). + let info = balance_infos.and_then(|arr| arr.first()); + + Ok(ProviderBalance { + provider: self.api_provider, + is_available, + currency: info + .and_then(|i| i.get("currency")) + .and_then(|v| v.as_str()) + .map(str::to_string), + total_balance: info + .and_then(|i| i.get("total_balance")) + .and_then(|v| v.as_str()) + .map(str::to_string), + topped_up_balance: info + .and_then(|i| i.get("topped_up_balance")) + .and_then(|v| v.as_str()) + .map(str::to_string), + granted_balance: info + .and_then(|i| i.get("granted_balance")) + .and_then(|v| v.as_str()) + .map(str::to_string), + total_credits: None, + total_usage: None, + }) + } + + async fn check_openrouter_balance(&self) -> Result<ProviderBalance> { + let base = unversioned_base_url(&self.base_url); + let url = format!("{}/api/v1/credits", base); + let response = self.send_with_retry(|| self.http_client.get(&url)).await?; + + let status = response.status(); + if !status.is_success() { + let error_text = bounded_error_text(response, ERROR_BODY_MAX_BYTES).await; + anyhow::bail!("Balance check failed: HTTP {status}: {error_text}"); + } + + let response_text = response.text().await.unwrap_or_default(); + let value: serde_json::Value = serde_json::from_str(&response_text) + .context("Failed to parse OpenRouter credits response")?; + + let data = value.get("data"); + + Ok(ProviderBalance { + provider: self.api_provider, + is_available: None, + currency: None, + total_balance: data + .and_then(|d| d.get("total_credits")) + .and_then(|v| v.as_f64()) + .map(|c| format!("{c:.2}")), + topped_up_balance: None, + granted_balance: None, + total_credits: data + .and_then(|d| d.get("total_credits")) + .and_then(|v| v.as_f64()), + total_usage: data + .and_then(|d| d.get("total_usage")) + .and_then(|v| v.as_f64()), + }) + } } mod chat; diff --git a/crates/tui/src/commands/balance.rs b/crates/tui/src/commands/balance.rs index 431f2cb55..255387c54 100644 --- a/crates/tui/src/commands/balance.rs +++ b/crates/tui/src/commands/balance.rs @@ -1,62 +1,46 @@ //! Balance: query the active provider's account balance or credit status. //! -//! Live balance dispatch is planned for a future release (#2019). Until then, -//! this command prints the provider's billing endpoint and instructions for -//! manual balance checking, plus the current session cost summary. +//! Dispatches an async balance check via `AppAction::CheckBalance` for +//! providers with known billing endpoints. Unsupported providers return +//! a clear message immediately. use crate::config::ApiProvider; -use crate::tui::app::App; +use crate::tui::app::{App, AppAction}; use super::CommandResult; /// Query provider account balance / credits. -pub fn balance(app: &mut App) -> CommandResult { - let provider = app.api_provider; - let session_cost = app.displayed_session_cost_for_currency(app.cost_currency); - let cost_label = app.format_cost_amount(session_cost); - let token_usage = app.session.total_conversation_tokens; - - let provider_info = match provider { - ApiProvider::Deepseek | ApiProvider::DeepseekCN => { - format!( - "DeepSeek billing: https://platform.deepseek.com/usage\n\ - API balance endpoint: GET https://api.deepseek.com/user/balance\n\ - (Requires Bearer token auth with your API key)\n\n\ - Live balance dispatch is planned for a future release (#2019).\n\ - For now, check your balance on the DeepSeek Platform dashboard." - ) - } - ApiProvider::Openrouter => { - format!( - "OpenRouter credits: https://openrouter.ai/credits\n\ - Live balance dispatch is planned for a future release (#2019)." - ) - } - ApiProvider::Novita => { - format!( - "Novita billing: check your provider dashboard.\n\ - Live balance dispatch is planned for a future release (#2019)." - ) - } - ApiProvider::Fireworks => { - format!( - "Fireworks billing: https://fireworks.ai/account/billing\n\ - Live balance dispatch is planned for a future release (#2019)." - ) - } - _ => { - format!( - "Balance check is not supported for {} yet.\n\ - Check the provider dashboard for account balance details.", - provider.display_name() - ) +/// +/// `/balance` defaults to the active provider; `/balance <provider>` lets +/// the user target a specific provider by name (e.g. `deepseek`, `openrouter`). +pub fn balance(app: &mut App, arg: Option<&str>) -> CommandResult { + let provider = if let Some(arg) = arg { + match ApiProvider::parse(arg) { + Some(p) => p, + None => { + return CommandResult::error(format!( + "Unknown provider '{}'. Supported: deepseek, openrouter, novita.", + arg + )); + } } + } else { + app.api_provider }; - CommandResult::message(format!( - "{provider_info}\n\n\ - This session: {cost_label} | {token_usage} tokens" - )) + match provider { + ApiProvider::Deepseek | ApiProvider::DeepseekCN | ApiProvider::Openrouter => { + CommandResult::action(AppAction::CheckBalance { provider }) + } + ApiProvider::Novita => CommandResult::message(format!( + "Balance check for {} is not yet implemented. Check the provider dashboard for account balance details.", + provider.display_name() + )), + _ => CommandResult::message(format!( + "Balance check is not supported for {}. Check the provider dashboard for account balance details.", + provider.display_name() + )), + } } #[cfg(test)] @@ -92,20 +76,55 @@ mod tests { } #[test] - fn test_balance_shows_deepseek_info() { + fn test_balance_deepseek_returns_check_balance_action() { let mut app = create_test_app(); app.api_provider = ApiProvider::Deepseek; - let result = balance(&mut app); - let msg = result.message.unwrap(); - assert!(msg.contains("platform.deepseek.com")); - assert!(msg.contains("/user/balance")); + let result = balance(&mut app, None); + assert!( + matches!( + result.action, + Some(AppAction::CheckBalance { + provider: ApiProvider::Deepseek + }) + ), + "expected CheckBalance action for DeepSeek, got {:?}", + result.action + ); + } + + #[test] + fn test_balance_explicit_provider() { + let mut app = create_test_app(); + app.api_provider = ApiProvider::Ollama; + let result = balance(&mut app, Some("deepseek")); + assert!( + matches!( + result.action, + Some(AppAction::CheckBalance { + provider: ApiProvider::Deepseek + }) + ), + "expected CheckBalance for explicit deepseek, got {:?}", + result.action + ); + } + + #[test] + fn test_balance_unknown_provider_returns_error() { + let mut app = create_test_app(); + let result = balance(&mut app, Some("unknown_provider")); + assert!(result.is_error); + assert!(result.message.unwrap().contains("Unknown provider")); } #[test] - fn test_balance_shows_session_cost() { + fn test_balance_unsupported_provider_returns_message() { let mut app = create_test_app(); - let result = balance(&mut app); + app.api_provider = ApiProvider::Ollama; + let result = balance(&mut app, None); + assert!(result.message.is_some()); + assert!(!result.is_error); let msg = result.message.unwrap(); - assert!(msg.contains("tokens")); + assert!(msg.contains("not supported")); } } diff --git a/crates/tui/src/commands/mod.rs b/crates/tui/src/commands/mod.rs index 9b0e5abe3..fc9e48c43 100644 --- a/crates/tui/src/commands/mod.rs +++ b/crates/tui/src/commands/mod.rs @@ -611,7 +611,7 @@ pub fn execute(cmd: &str, app: &mut App) -> CommandResult { "translate" | "translation" | "transale" => core::translate(app), "tokens" => debug::tokens(app), "cost" => debug::cost(app), - "balance" => balance::balance(app), + "balance" => balance::balance(app, arg), "cache" => debug::cache(app, arg), // ChangeLog command diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index 202cd1648..a4379726b 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -122,6 +122,8 @@ pub struct EngineConfig { pub todos: SharedTodoList, /// Shared Plan state. pub plan_state: SharedPlanState, + /// Shared Goal state (LLM-as-judge). + pub goal_state: crate::tools::goal::SharedGoalState, /// Maximum sub-agent recursion depth (default 3). See /// `SubAgentRuntime::max_spawn_depth`. Override via /// `[runtime] max_spawn_depth = N` in `~/.deepseek/config.toml`. @@ -194,6 +196,7 @@ impl Default for EngineConfig { capacity: CapacityControllerConfig::default(), todos: new_shared_todo_list(), plan_state: new_shared_plan_state(), + goal_state: Arc::new(std::sync::Mutex::new(crate::tui::app::GoalState::default())), max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH, network_policy: None, snapshots_enabled: true, @@ -1006,7 +1009,9 @@ impl Engine { let plan_state = self.config.plan_state.clone(); let tool_context = self.build_tool_context(mode, auto_approve); - let builder = self.build_turn_tool_registry_builder(mode, todo_list, plan_state); + let goal_state = self.config.goal_state.clone(); + let builder = + self.build_turn_tool_registry_builder(mode, todo_list, plan_state, goal_state); let fork_context_for_runtime = if self.config.features.enabled(Feature::Subagents) { let state = StructuredState::capture( diff --git a/crates/tui/src/core/engine/tool_setup.rs b/crates/tui/src/core/engine/tool_setup.rs index 2354d6a8c..e22e17f95 100644 --- a/crates/tui/src/core/engine/tool_setup.rs +++ b/crates/tui/src/core/engine/tool_setup.rs @@ -38,6 +38,7 @@ impl Engine { mode: AppMode, todo_list: SharedTodoList, plan_state: SharedPlanState, + goal_state: crate::tools::goal::SharedGoalState, ) -> ToolRegistryBuilder { let mut builder = if mode == AppMode::Plan { ToolRegistryBuilder::new() @@ -52,11 +53,13 @@ impl Engine { .with_runtime_read_only_task_tools() .with_todo_tool(todo_list) .with_plan_tool(plan_state) + .with_goal_tools(goal_state.clone()) } else { ToolRegistryBuilder::new() .with_agent_tools(self.session.allow_shell) .with_todo_tool(todo_list) .with_plan_tool(plan_state) + .with_goal_tools(goal_state.clone()) }; builder = builder diff --git a/crates/tui/src/main.rs b/crates/tui/src/main.rs index 34cb7fce7..cd19c2ae4 100644 --- a/crates/tui/src/main.rs +++ b/crates/tui/src/main.rs @@ -5108,6 +5108,9 @@ async fn run_exec_agent( capacity: crate::core::capacity::CapacityControllerConfig::from_app_config(config), todos: new_shared_todo_list(), plan_state: new_shared_plan_state(), + goal_state: std::sync::Arc::new(std::sync::Mutex::new( + crate::tui::app::GoalState::default(), + )), max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH, network_policy, snapshots_enabled: config.snapshots_config().enabled, diff --git a/crates/tui/src/runtime_threads.rs b/crates/tui/src/runtime_threads.rs index 1a08473d6..3d47f41c5 100644 --- a/crates/tui/src/runtime_threads.rs +++ b/crates/tui/src/runtime_threads.rs @@ -1952,6 +1952,9 @@ impl RuntimeThreadManager { ), todos: new_shared_todo_list(), plan_state: new_shared_plan_state(), + goal_state: std::sync::Arc::new(std::sync::Mutex::new( + crate::tui::app::GoalState::default(), + )), max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH, network_policy, snapshots_enabled: self.config.snapshots_config().enabled, diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index 408d10df8..9ee4c4849 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -4569,6 +4569,10 @@ pub enum AppAction { SendMessage(String), ListSubAgents, FetchModels, + /// Query the provider's billing/balance endpoint (#2019). + CheckBalance { + provider: ApiProvider, + }, CacheWarmup, /// Switch the active LLM backend (DeepSeek vs NVIDIA NIM) without /// restarting the process. The runtime rebuilds its API client from diff --git a/crates/tui/src/tui/format_helpers.rs b/crates/tui/src/tui/format_helpers.rs index bcd8065ef..c1485f483 100644 --- a/crates/tui/src/tui/format_helpers.rs +++ b/crates/tui/src/tui/format_helpers.rs @@ -74,6 +74,73 @@ pub(super) fn available_models_message(current_model: &str, models: &[String]) - lines.join("\n") } +/// Build the multi-line balance result message for the `/balance` command. +pub(super) fn balance_result(balance: &crate::client::ProviderBalance, app: &App) -> String { + use crate::config::ApiProvider; + + let mut lines: Vec<String> = Vec::new(); + + match balance.provider { + ApiProvider::Deepseek | ApiProvider::DeepseekCN => { + lines.push(format!( + "{} Account Balance", + balance.provider.display_name() + )); + lines.push("─".repeat(40)); + match balance.is_available { + Some(true) => {} + Some(false) => { + lines.push("Balance endpoint reports: unavailable".to_string()); + } + None => {} + } + if let Some(ref currency) = balance.currency { + lines.push(format!("Currency: {currency}")); + } + if let Some(ref total) = balance.total_balance { + lines.push(format!("Total balance: {total}")); + } + if let Some(ref topped_up) = balance.topped_up_balance { + lines.push(format!("Topped-up balance: {topped_up}")); + } + if let Some(ref granted) = balance.granted_balance { + lines.push(format!("Granted balance: {granted}")); + } + } + ApiProvider::Openrouter => { + lines.push("OpenRouter Credits".to_string()); + lines.push("─".repeat(40)); + if let Some(credits) = balance.total_credits { + lines.push(format!("Total credits purchased: {credits:.2}")); + } + if let Some(usage) = balance.total_usage { + lines.push(format!("Total usage: {usage:.2}")); + } + if let (Some(credits), Some(usage)) = (balance.total_credits, balance.total_usage) { + let remaining = credits - usage; + lines.push(format!("Remaining credits: {remaining:.2}")); + } + } + _ => { + lines.push(format!( + "Balance info for {}", + balance.provider.display_name() + )); + } + } + + // Append session cost summary for context + let session_total = app.displayed_session_cost_for_currency(app.cost_currency); + let cost_label = app.format_cost_amount(session_total); + lines.push(String::new()); + lines.push(format!( + "This session (estimated): {cost_label} | {} tokens", + app.session.total_conversation_tokens + )); + + lines.join("\n") +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/tui/src/tui/mouse_ui.rs b/crates/tui/src/tui/mouse_ui.rs index 589c31aea..52e30dabf 100644 --- a/crates/tui/src/tui/mouse_ui.rs +++ b/crates/tui/src/tui/mouse_ui.rs @@ -92,6 +92,25 @@ pub(crate) fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEv return Vec::new(); } + // #2018: clicking on a tool-cell header line opens the detail popup + // so low-information rows (e.g. "file_search returned 0 results") are + // inspectable with a single click. Clicking on output text still starts + // a normal text selection. + if let Some(point) = selection_point_from_mouse(app, mouse) { + if let Some((cell_index, line_in_cell)) = app + .viewport + .transcript_cache + .line_meta() + .get(point.line_index) + .and_then(|m| m.cell_line()) + { + if line_in_cell == 0 && app.cell_has_detail_target(cell_index) { + open_details_pager_for_cell(app, cell_index); + return Vec::new(); + } + } + } + if let Some(point) = selection_point_from_mouse(app, mouse) { app.viewport.transcript_selection.anchor = Some(point); app.viewport.transcript_selection.head = Some(point); diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 2efd9cad4..448343d2c 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -699,6 +699,7 @@ fn build_engine_config(app: &App, config: &Config) -> EngineConfig { capacity: crate::core::capacity::CapacityControllerConfig::from_app_config(config), todos: app.todos.clone(), plan_state: app.plan_state.clone(), + goal_state: app.goal.clone(), max_spawn_depth: crate::tools::subagent::DEFAULT_MAX_SPAWN_DEPTH, network_policy: config.network.clone().map(|toml_cfg| { crate::network_policy::NetworkPolicyDecider::with_default_audit(toml_cfg.into_runtime()) @@ -3674,6 +3675,29 @@ fn apply_alt_0_shortcut(app: &mut App, modifiers: KeyModifiers) { } } +async fn fetch_balance( + config: &mut Config, + provider: ApiProvider, +) -> Result<crate::client::ProviderBalance> { + use crate::client::DeepSeekClient; + + // Temporarily switch the config to the requested provider so + // DeepSeekClient::new picks up the right API key and base URL. + let previous_provider = config.provider.clone(); + config.provider = Some(provider.as_str().to_string()); + let result = tokio::time::timeout(Duration::from_secs(20), async { + let client = DeepSeekClient::new(&*config)?; + client.check_balance().await + }) + .await; + config.provider = previous_provider; + match result { + Ok(Ok(balance)) => Ok(balance), + Ok(Err(err)) => Err(err), + Err(_elapsed) => anyhow::bail!("Balance check timed out after 20 seconds"), + } +} + async fn fetch_available_models(config: &Config) -> Result<Vec<String>> { use crate::client::DeepSeekClient; @@ -4636,6 +4660,22 @@ async fn apply_command_result( } } } + AppAction::CheckBalance { provider } => { + app.status_message = + Some(format!("Checking {} balance...", provider.display_name())); + match fetch_balance(config, provider).await { + Ok(balance) => { + let message = format_helpers::balance_result(&balance, app); + app.add_message(HistoryCell::System { content: message }); + app.status_message = Some("Balance check complete".to_string()); + } + Err(error) => { + let err_msg = format!("Balance check failed: {error}"); + app.add_message(HistoryCell::System { content: err_msg }); + app.status_message = Some("Balance check failed".to_string()); + } + } + } AppAction::CacheWarmup => { app.status_message = Some("Warming DeepSeek cache...".to_string()); match run_cache_warmup(app, config).await {