Merge branch 'main' into cursor/session-cost-bundled-fallback-0df6

This commit is contained in:
Hunter Bown
2026-08-16 16:24:49 -07:00
committed by GitHub
294 changed files with 31265 additions and 2970 deletions
+50
View File
@@ -0,0 +1,50 @@
# cargo-nextest profile for contributors (`cargo nextest run --workspace`).
#
# `cargo test --workspace --all-features --locked` remains the authoritative
# release gate. nextest is a faster local loop: it runs each test in its own
# process, so the 10k-test codewhale-tui unit suite finishes in well under
# half the wall-clock time of libtest on a multi-core machine, and slow or
# hanging tests are named instead of stalling the whole binary.
#
# Because tests no longer share a process, the suites that serialize on an
# in-process mutex today (PTY / terminal-matrix / release-runtime QA) must be
# serialized here instead: they contend for pseudo-terminals, mock-server
# ports, and wall-clock timing budgets.
[profile.default]
# Name tests that take longer than this so contributors see where the time goes.
slow-timeout = { period = "30s" }
# Keep failures visible in the final summary and never retry silently:
# a flaky test is a bug report, not something to paper over.
retries = 0
fail-fast = false
final-status-level = "slow"
# RUST_MIN_STACK is not a nextest.toml key. CI and scripts/dev-test.sh
# export 16 MiB so local nextest matches the product thread stack.
[test-groups]
pty = { max-threads = 1 }
# Integration tests that spawn the real `codewhale` binary and wait on
# service start-up deadlines; bounded so a fully parallel run on a busy
# machine cannot starve them past their 30 s budgets.
spawns-binaries = { max-threads = 4 }
[[profile.default.overrides]]
# The PTY test binary (`crates/tui/tests/pty`) drives real pseudo-terminals
# and shares mock servers; run it one test at a time, in the background of
# everything else.
filter = 'binary(pty)'
test-group = 'pty'
slow-timeout = { period = "120s" }
[[profile.default.overrides]]
filter = 'binary(integration)'
test-group = 'spawns-binaries'
# CI (profiles inherit from default): the final summary lists every failure and
# slow test in full so the run log is enough to diagnose without rerunning.
[profile.ci]
retries = 0
fail-fast = false
final-status-level = "slow"
failure-output = "immediate-final"
+5
View File
@@ -217,3 +217,8 @@ mky = mky <817223+mky@users.noreply.github.com>
cacdcaecawae = cacdcaecawae <109055297+cacdcaecawae@users.noreply.github.com>
XiaoHuo888-hue = XiaoHuo888-hue <315183888+XiaoHuo888-hue@users.noreply.github.com>
sjh00112233@outlook.com = XiaoHuo888-hue <315183888+XiaoHuo888-hue@users.noreply.github.com>
wuisabel-gif = Isabel Wu <231155141+wuisabel-gif@users.noreply.github.com>
redstar = Kai Nacke <827859+redstar@users.noreply.github.com>
kai@redstar.de = Kai Nacke <827859+redstar@users.noreply.github.com>
Shizuku = Sh1Zuku <125943630+SparkofSpike@users.noreply.github.com>
2163018547@qq.com = Sh1Zuku <125943630+SparkofSpike@users.noreply.github.com>
+26 -9
View File
@@ -1,5 +1,9 @@
#!/usr/bin/env bash
# Update the Homebrew tap at Hmbown/homebrew-deepseek-tui after a release.
# Update the Homebrew tap after a release.
#
# The tap GitHub repo is still Hmbown/homebrew-deepseek-tui until Hunter
# renames it. The formula users type is `codewhale`. The legacy
# `deepseek-tui` formula stays as a deprecated alias for one overlap release.
#
# Expected environment:
# TAG git tag, e.g. "v0.8.31"
@@ -7,6 +11,7 @@
# TAP_REPO owner/repo of the Homebrew tap
# TOKEN PAT with contents:write on TAP_REPO (optional; skips if unset)
# FORMULA_OUTPUT optional local render path used by contract tests
# FORMULA_LEGACY_OUTPUT optional local render path for the alias formula
set -euo pipefail
@@ -52,20 +57,22 @@ readonly SHA_COD_LINUX_X64 SHA_CODEW_LINUX_X64
# --- temp dirs --------------------------------------------------------
FORMULA_FILE="$(mktemp)"
LEGACY_FILE="$(mktemp)"
TAP_DIR="$(mktemp -d)"
trap 'rm -rf "${TAP_DIR}" "${FORMULA_FILE}"' EXIT
# --- generate formula --------------------------------------------------
trap 'rm -rf "${TAP_DIR}" "${FORMULA_FILE}" "${LEGACY_FILE}"' EXIT
readonly BASE_URL="https://github.com/Hmbown/CodeWhale/releases/download/${TAG}"
cat > "${FORMULA_FILE}" << EOF
class DeepseekTui < Formula
render_formula() {
local class_name="${1:?}"
local extra_header="${2:-}"
cat << EOF
class ${class_name} < Formula
desc "Agentic terminal for open-source and open-weight coding models"
homepage "https://github.com/Hmbown/CodeWhale"
version "${VERSION}"
license "MIT"
${extra_header}
on_macos do
if Hardware::CPU.arm?
url "${BASE_URL}/codewhale-macos-arm64", using: :nounzip
@@ -113,10 +120,19 @@ class DeepseekTui < Formula
end
end
EOF
}
render_formula "Codewhale" "" > "${FORMULA_FILE}"
render_formula "DeepseekTui" " deprecate! date: \"2026-08-14\", because: \"renamed to codewhale\"
" > "${LEGACY_FILE}"
if [ -n "${FORMULA_OUTPUT:-}" ]; then
cp "${FORMULA_FILE}" "${FORMULA_OUTPUT}"
echo "Rendered Homebrew formula to ${FORMULA_OUTPUT}"
if [ -n "${FORMULA_LEGACY_OUTPUT:-}" ]; then
cp "${LEGACY_FILE}" "${FORMULA_LEGACY_OUTPUT}"
echo "Rendered legacy Homebrew formula to ${FORMULA_LEGACY_OUTPUT}"
fi
exit 0
fi
@@ -128,13 +144,14 @@ TAP_URL="https://x-access-token:${ENCODED_TOKEN}@github.com/${TAP_REPO}.git"
git clone --depth 1 "${TAP_URL}" "${TAP_DIR}"
mkdir -p "${TAP_DIR}/Formula"
cp "${FORMULA_FILE}" "${TAP_DIR}/Formula/deepseek-tui.rb"
cp "${FORMULA_FILE}" "${TAP_DIR}/Formula/codewhale.rb"
cp "${LEGACY_FILE}" "${TAP_DIR}/Formula/deepseek-tui.rb"
cd "${TAP_DIR}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/deepseek-tui.rb
git add Formula/codewhale.rb Formula/deepseek-tui.rb
if git diff --cached --quiet; then
echo "Formula unchanged (already at ${VERSION}); nothing to push."
+11 -1
View File
@@ -6,7 +6,8 @@ tmp_dir="$(mktemp -d)"
trap 'rm -rf "${tmp_dir}"' EXIT
manifest="${tmp_dir}/codewhale-artifacts-sha256.txt"
formula="${tmp_dir}/deepseek-tui.rb"
formula="${tmp_dir}/codewhale.rb"
legacy="${tmp_dir}/deepseek-tui.rb"
assets=(
codewhale-macos-arm64
@@ -27,9 +28,14 @@ TAG=v1.2.3 \
MANIFEST="${manifest}" \
TAP_REPO=Hmbown/homebrew-deepseek-tui \
FORMULA_OUTPUT="${formula}" \
FORMULA_LEGACY_OUTPUT="${legacy}" \
bash "${repo_root}/.github/scripts/update-homebrew-tap.sh"
ruby -c "${formula}" >/dev/null
ruby -c "${legacy}" >/dev/null
grep -Fq 'class Codewhale < Formula' "${formula}"
grep -Fq 'class DeepseekTui < Formula' "${legacy}"
grep -Fq 'deprecate! date: "2026-08-14", because: "renamed to codewhale"' "${legacy}"
grep -Fq 'desc "Agentic terminal for open-source and open-weight coding models"' "${formula}"
test "$(grep -Fc 'resource "codew" do' "${formula}")" -eq 4
grep -Fq 'bin.install Dir["*"].first => "codew"' "${formula}"
@@ -38,5 +44,9 @@ if grep -Fq 'codewhale-tui' "${formula}"; then
echo "Homebrew formula must not install the legacy TUI compatibility asset" >&2
exit 1
fi
if grep -Fq 'class DeepseekTui' "${formula}"; then
echo "Primary Homebrew formula must be Codewhale, not DeepseekTui" >&2
exit 1
fi
echo "update-homebrew-tap tests passed"
+20 -5
View File
@@ -109,13 +109,13 @@ jobs:
# arm (fail-safe default-heavy). Light-classified scripts below
# are exercised by ALWAYS-on jobs/steps that run regardless of
# `heavy` (check-versions.sh / check-ohos-deps.sh via Version
# drift, check-coauthor-trailers.py via Lint), so no coverage is
# lost.
# drift, check-coauthor-trailers.py via Lint, dev-cache/dev-test
# self-checks via Version drift), so no coverage is lost.
case "${path}" in
scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py)
heavy=true
;;
docs/*|*.md|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py)
docs/*|*.md|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py|scripts/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh|scripts/dev-test.test.sh)
;;
*)
heavy=true
@@ -177,6 +177,8 @@ jobs:
bash scripts/release/require-release-tag-checkout.test.sh
bash scripts/release/validate-crate-publish-order.test.sh
bash scripts/release/verify-remote-tag.test.sh
sh scripts/dev-cache.test.sh
sh scripts/dev-test.test.sh
bash .github/scripts/update-homebrew-tap.test.sh
node .github/scripts/release-workflows.test.js
node --test scripts/release/assemble-release-assets.test.js
@@ -435,9 +437,17 @@ jobs:
with:
cache-bin: false
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run tests
- uses: taiki-e/install-action@nextest
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
run: cargo test --workspace --all-features --locked
- name: Run tests
# Same test binaries as `cargo test`, run by cargo-nextest: one
# process per test, all runner cores busy, slow tests named instead
# of stalling the binary. `.config/nextest.toml` serializes the PTY
# binary and bounds the integration binary that spawns the real
# executable; retries are off, so a flake is a red run, not a hidden
# one. nextest does not run doctests — the next step keeps them.
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
run: cargo nextest run --workspace --all-features --locked --profile ci
env:
# Give test threads the stack the product gives itself. main.rs runs
# the owner thread and every tokio worker at
@@ -452,6 +462,11 @@ jobs:
# this for any thread spawned without an explicit size, which covers
# both libtest's per-test threads and tokio's workers.
RUST_MIN_STACK: '16777216'
- name: Run doctests
if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
run: cargo test --workspace --all-features --locked --doc
env:
RUST_MIN_STACK: '16777216'
# The Ubuntu lint lane validates non-RSS backlog fields. Run the same
# source-bound measurement on macOS so loss or growth of RSS evidence
# fails closed instead of becoming an unsupported-field skip.
+3 -15
View File
@@ -101,21 +101,9 @@ jobs:
with:
toolchain: stable
targets: ${{ matrix.target }}
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
id: sccache
continue-on-error: true
- name: Enable sccache
if: steps.sccache.outcome == 'success'
shell: bash
run: |
{
echo "SCCACHE_GHA_ENABLED=true"
echo "RUSTC_WRAPPER=sccache"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1"
} >> "${GITHUB_ENV}"
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
# Privileged artifact builds check out inputs.source_sha. Do not
# restore or save rust-cache / sccache here — those keys are shared
# with default-branch CI (CodeQL #95#96, #104#106).
- name: Build static Linux binaries (musl)
if: endsWith(matrix.target, '-unknown-linux-musl')
shell: bash
+3 -2
View File
@@ -88,8 +88,9 @@ jobs:
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm
cache-dependency-path: web/package-lock.json
# No npm cache: this job checkouts needs.resolve.outputs.sha from
# workflow_dispatch (CodeQL #88#94). Lockfile-keyed caches would
# still be writable from that checkout into the default branch.
- name: Install web dependencies
run: npm ci
- name: Check public facts drift
+4 -15
View File
@@ -123,18 +123,10 @@ jobs:
with:
toolchain: stable
components: clippy, rustfmt
- uses: mozilla-actions/sccache-action@fc920bf0ec8de6ee65d409111f7ec508035751ba # v0.0.11
id: sccache
continue-on-error: true
- name: Enable sccache
if: steps.sccache.outcome == 'success'
shell: bash
run: |
{
echo "SCCACHE_GHA_ENABLED=true"
echo "RUSTC_WRAPPER=sccache"
echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1"
} >> "${GITHUB_ENV}"
# Privileged release jobs check out a resolved SHA (tag or
# workflow_dispatch). Do not restore or save GitHub Actions caches
# here: a shared rust-cache / sccache key would let that checkout
# write into the default-branch cache (CodeQL #88#103).
- name: Install Linux system dependencies
run: |
for i in 1 2 3 4 5; do
@@ -143,9 +135,6 @@ jobs:
sleep 15
done
sudo apt-get install -y libdbus-1-dev pkg-config
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
cache-bin: false
- name: Format check
run: cargo fmt --all -- --check
- name: Compile check
+56 -85
View File
@@ -1,98 +1,69 @@
# Repository Agent Guidance
# Codewhale agent guidance
Durable rules only. Perishable lane state — branch, milestone, known flakes,
closed investigations — lives in the private `codewhale-ops` repo, not here.
Read it there; don't trust memory of it.
Keep this file durable. Derive changing release, provider, branch, and flake
state from the repository, tests, CI, and current issue tracker rather than from
instructions or memory. The nearest scoped `AGENTS.md` adds path-specific rules.
## Intent is the artifact
## Working rules
Writing the code again is cheaper than recovering the code we wrote. Act on
that.
- Inspect status and existing consumers before editing. Preserve unrelated,
dirty, and untracked work.
- Prefer the simplest implementation that preserves observable contracts. A
rewrite is acceptable when justified by behavior and tests, not as a shortcut
around understanding existing code.
- Search for behavior and symbols before reviving work from an old branch. If a
lane is obsolete, preserve its intent and evidence rather than merging stale
code mechanically.
- Public name is **Codewhale**. Compatibility identifiers such as `CodeWhale`,
`codew`, protocol names, and storage keys change only through an explicit
migration.
- Keep providers and models first-class and provider-neutral.
- Never rewrite published history, retag a release, force-push a shared ref, or
publish without explicit authorization. Preserve human contributor credit.
- **Rewriting any part of this project is always in scope**, up to the whole
thing. Nothing is load-bearing by virtue of existing. Argue a rewrite on
merit, not sunk cost.
- **Use git; do not be governed by it.** A branch 600 commits behind is a note
describing something we once wanted, not a debt. Conflict count is a signal to
rewrite, not a task list.
- **A stranded lane becomes an issue, not a merge.** State the intent, the
behavior wanted, and evidence worth keeping; reference the dead branch for
provenance; abandon the branch; rebuild from current `main`.
- **Verify before you rebuild.** Grep for the symbols and behavior — not the
commit — to check whether `main` already does it. Re-landing landed work is
the failure mode this ethos creates, and it is the one you own.
## Current contracts
Limits: `main` stays protected and releases reproducible (never rewrite
published history, retag a shipped release, or force-push a shared ref);
contributor credit carries onto the rewrite; the do-not-delete guardrail below
still binds; and don't rewrite to avoid understanding.
- The model-facing subagent tool is `agent`. Do not revive removed
`agent_open`/`agent_eval`/`agent_close`/`delegate_to_agent` surfaces or parallel
lifecycle/tag systems.
- `BASE_PROMPT` in `crates/tui/src/prompts/text.rs` is the sole base prompt.
- The system prompt + tool catalog are a session-pinned KV-cache prefix
(`docs/CACHE.md`). Any new session-context contributor must state its
KV-cache effect: frozen prefix vs. append-only history. Never splice a
volatile fact into the prefix; append it as a user-role message.
- These active modules are repeatedly misidentified as dead; verify consumers
before removal: `tui/src/context_budget.rs`, `tui/src/model_registry.rs`,
`tui/src/prompt_zones.rs`, `tui/src/tools/remember.rs`, and
`config/src/route/`. Native memory lives in `tui/src/native_memory.rs`;
`tools/remember.rs` is its capture path.
- Environment-specific behavior belongs in `docs/ENVIRONMENTS.md`, not here.
The four bullets above are the authoritative statement of this rule. Don't
restate them elsewhere — link here. (`docs/AGENT_ETHOS.md` is about stewardship
and workflow, not about this; it is not a longer form of this section.)
## Verification
## Build and test
Always before pushing: `cargo fmt`, then targeted tests for the area.
Run formatting and focused tests for every change. Before a push, run the
relevant repository gate; release work requires the complete sequence:
```sh
cargo test -p codewhale-config
cargo test -p codewhale-protocol
cargo test --workspace # full gate
cargo build --release -p codewhale-cli -p codewhale-tui # release build
cargo fmt --all -- --check
cargo test -p codewhale-config -p codewhale-protocol
cargo test --workspace
cargo build --release -p codewhale-cli -p codewhale-tui
```
Crate-specific commands live in that crate's `AGENTS.md`. Environment quirks
(Cursor Cloud, keyless providers, dispatcher siblings) live in
`docs/ENVIRONMENTS.md`.
`cargo nextest run` (config in `.config/nextest.toml`) is the fast way to
*run* those suites locally and in CI's Test job; `cargo test --no-run` and
`cargo test -p codewhale-tui --lib` remain the compile-time measurement and
the authoritative gate, and `cargo test --doc` covers what nextest skips.
`scripts/dev-test.sh <area>` maps a code area to its fastest `-p` invocation
and applies the portable isolated build-dir topology for new worktrees
(`scripts/dev-cache.sh`, `scripts/dev-cargo.sh`). See
`docs/BUILD_PERFORMANCE.md`.
Default branch is `main`. Committing directly to `main` is fine for release-lane
work — one reviewable concern per commit, with a real body. A fresh `codex/...`
branch or worktree is still right for an isolated or risky change.
Report commands actually run and distinguish source, local tests, packaged
artifacts, CI, and public release state. A commit is WIP until its claimed
behavior has direct evidence.
Commit as **WIP** unless you actually verified the behavior — built the binary,
ran the test, reproduced the fix. "Fixed" without evidence is worse than an
honest WIP.
## Do-not-delete guardrail
These are actively imported and have been repeatedly misflagged as dead code;
deleting them broke the build. Verify consumers with `rg` before believing any
dead-code audit:
`tui/src/context_budget.rs`, `tui/src/model_registry.rs`,
`tui/src/prompt_zones.rs`, `tui/src/tools/remember.rs`, and the entire
`config/src/route/` directory.
(`tui/src/memory.rs` was deliberately deleted in v0.9.4 — the native memory
store in `tui/src/native_memory.rs` is the surviving system; `tools/remember.rs`
is its capture path and stays.)
## Surfaces that exist today
Build only on these — removed machinery stays gone. The model-facing sub-agent
surface is **`agent` only**: the `agent_open`/`agent_eval`/`agent_close`/
`delegate_to_agent` variants, capacity/coherence/runtime-tag systems, lifecycle
tools, and runtime prompt/tag injection were all removed. The constitution
(`BASE_PROMPT` in `tui/src/prompts/text.rs`) is the sole base prompt.
Configurable sub-agent depth stays; add a new limit only when clearly needed,
and explain why.
## Stewardship
CodeWhale started as a DeepSeek-only harness; it is now about building the best
possible coding harness with an open-source community. Keep CodeWhale branding
and every model/provider first-class — none privileged.
- Community PRs, issues, repros, logs, and reviews are maintainer evidence, not
queue noise. Review from code, tests, linked issues, comments, and checks.
- **Credit is CI-enforced.** `Co-authored-by` trailers are for human
contributors only — `scripts/check-coauthor-trailers.py` rejects bot/tool ones
(Claude, codex, cursor, `noreply@anthropic.com`). Use canonical identities
from `.github/AUTHOR_MAP`; note agent assistance in a plain commit body.
- Keep gates warm and dry-run unless Hunter explicitly approves enforcement.
- Leave unrelated edits by other people or agents intact.
Full ethos: `docs/AGENT_ETHOS.md`. Issue-triage standard, release queue, and
harvest procedure live in the private `codewhale-ops` repo — they are
maintainer process, not contributor-facing contract.
Community reports, PRs, logs, and reviews are evidence. Canonical human
identities come from `.github/AUTHOR_MAP`; `Co-authored-by` is for humans only.
Leave unrelated work intact and keep new enforcement dry-run unless explicitly
approved.
+239 -14
View File
@@ -7,19 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.8] - 2026-08-16
Codewhale v0.9.8 ships the remaining assigned finish. Remaining web
settings polish moves to v0.9.9. Prefab third-party templates that have
a published OpenAI-compatible host ship here (#5350).
### Fixed
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter is
gone so expanding a pane rematerializes layout the same way shrinking does.
- `sudo` (and `su`/setuid helpers) work again for wheel-group administrators
who want Codewhale to be able to escalate: the Linux startup hardening's
irreversible `PR_SET_NO_NEW_PRIVS` flag — inherited by every child process —
is now skippable with `CODEWHALE_NO_NEW_PRIVS=0` (#5413). The flag stays on
by default; the no-ptrace and no-core-dump measures are never skipped.
## [0.9.8] - 2026-08-14
Remaining web settings polish and prefab third-party templates move to
v0.9.9.
- Abort-class process deaths no longer poison the terminal (#5424). A
stack overflow, allocation failure, or double panic skips the panic hook
and every cleanup guard, which is how a v0.9.7 user's mid-turn exit left
mouse capture leaking SGR sequences into their shell. An
async-signal-safe handler now restores the terminal modes and appends a
one-line cause marker to `~/.codewhale/crashes/last-fatal-signal.log`
before re-raising, keeping the honest 128+signal wait status. A SIGKILL
(OOM killer) remains uninterceptable by design.
### Changed
- Prompt-cache prefix is pinned for the session. The tool loop no longer
recomposes the system prompt from disk on every model step, so an agent
writing a file no longer busts the provider KV prefix cache mid-turn. The
system prompt and tool catalog are re-composed only on a declared header
change (`/model`, mode, goal, session resume), which re-pins under a logged
reason; an undeclared change is reported as drift and the original pin is
kept instead of silently becoming the new baseline. Workspace, AGENTS.md,
skills, memory, and goal drift now reaches the model as one bounded
`<context_update>` user message at the next user turn — a history append,
not a header rewrite. `/cache stats` shows the pin reason, the last-miss
reason, the undeclared-drift count, and the context-update count. See
[docs/CACHE.md](docs/CACHE.md).
- Plugin compatibility is now per-component. A reviewed, trusted, enabled
bundle that mixes Skills or MCP with unsupported commands, agents, hooks,
LSP, native, filesystem-roots, or lifecycle-mutation declarations keeps the
@@ -54,8 +79,11 @@ v0.9.9.
thread** cannot paint over the session fact chips. Chips wrap instead
of sliding under the rail.
- Z.ai `GLM-5.3` is a live Coding Plan picker option (`/model` after
`/provider zai`, or `model = "GLM-5.3"`). `GLM-5.2` stays the default.
- Z.ai `GLM-5.3` is live on the Coding Plan and is now the default direct
Z.ai model: `DEFAULT_ZAI_MODEL` resolves to `GLM-5.3` in both
`codewhale-tui` and `codewhale-config`, and it is the first `/model` row
after `/provider zai`. Explicit `GLM-5.2` selections (`model = "GLM-5.2"`
and its `glm-5.2` aliases) keep their own id — only the default moved.
Limits and reasoning options still inherit from `GLM-5.2` until Z.ai
publishes distinct 5.3 numbers. No USD price is claimed. A live call
can still 429 with entitlement code 1311 on accounts that are not
@@ -72,6 +100,116 @@ v0.9.9.
the request. A clean output-limit stop continues the turn instead of
killing it (#5373).
- Ollama Cloud is a first-class hosted provider (`/provider ollama-cloud`)
on the official OpenAI-compatible `https://ollama.com/v1` route. Local
Ollama stays keyless. The exact released `ollama` + Cloud URL tuple keeps
a bounded compatibility path across saved sessions, Fleet, and nested
subagents; neighboring remotes stay custom and fail closed against
inherited official credentials.
- Homebrew ships a `codewhale` formula. `brew tap Hmbown/deepseek-tui &&
brew install codewhale` is the install path; `brew upgrade codewhale`
updates it. The legacy `deepseek-tui` formula remains a deprecated alias
for one overlap release.
- Terminal tab/window titles now carry the existing saved session name before
the live state (`Codewhale`, `reasoning…`, `using tool…`, `done`), so parallel
sessions are identifiable at a glance without a second title setting.
`/title <name>` is a discoverable alias for `/rename`; both update the one
session name shown in the picker, composer, and terminal tab. Control,
bidi, and zero-width format characters are stripped from the saved name
itself, so `/title`, `/rename`, the picker, the Runtime API,
`codewhale sessions`, and the OSC 0 tab title all carry the same
escape-free text (#5419, Sh1Zuku).
- Eden AI is a named OpenAI-compatible Chat Completions provider (`edenai`,
aliases `eden-ai` / `eden_ai`) with `EDENAI_API_KEY`, global and EU base-URL
overrides, a live provider-scoped model catalog, and
`deepseek/deepseek-v4-pro` as the verified default. Generic reasoning fields
stay omitted because Eden AI routes multiple upstream model families
(#5422, Kai Nacke).
- Children (sub-agents and Fleet workers) inherit the session's permission
posture faithfully: Auto-Review's deterministic floor and model guardian
decide a worker's held calls (fail closed when unavailable, never a
prompt); under Ask a held call is raised in the parent's approval UI and
the worker waits visibly; Full Access still fails closed on the safety
floor. Each prompt-less decision is a one-line note in that worker's
transcript (focus mode) and an audit-log record.
- Worker role defaults keep what the role does not intend to withhold:
every built-in role keeps network reads; `planner` may run read-only
shell probes; `custom` inherits the parent's write/network/shell posture
and is narrowed only by its explicit tool list or the spawning call.
Read-only roles (`scout`, `reviewer`, `planner`, `verifier`,
`consultant`) still never write the workspace. The focused worker's
header states its effective posture from the runtime snapshot.
- `/workflow status`, `/workflow cancel [run_id]`, `/workflow settings`, and
`/workflow help` are answered by Codewhale itself from the run journal and
live run state — no model turn — and `/workflow run <path>` launches a
checked-in workflow as-is. `/config workflow` and `/config goal` explain
the effective tables. The workflow tool now honors the session `[workflow]`
table (`automatic`, `auto_start_read_only`, `require_approval_for_writes`,
limits) instead of product defaults.
- Goal mode enters as readily as DeepSeek Harness: the agent may create the
session goal when a direct request describes a verifiable multi-turn end
state, and Codewhale shows a one-line `Goal set` receipt with how to pause
or clear it. Bare `/goal` shows plain progress (and how to continue when no
turn is running), prints usage on an empty session instead of asking the
model, and `/goal help|status` are reserved words.
- Whale Teams in the terminal: the six Signal Cut whale identities (Scout,
Patch, Harbor, Echo, Keel, Lantern) appear as species badges on `/fleet`
roster rows and worker rows, with an identity portrait in the roster detail
pane and a six-state word (Resting, Thinking, Working, Waiting for you,
Blocked, Offline) derived only from the child's real runtime status. Colors
come from the theme tokens, every glyph has an ASCII fallback, and the
working wake animates only under full motion. See
`docs/design/WHALE_TEAMS_TUI.md`.
- A session metrics strip on the phase row (`4 turns · 108 steps │ LLM
11m46s · Tool call 1m52s │ TTFT avg 1.5s · 120 tok/s │ Cache hit 99% │
Input 9.3M`), on by default as the `session_metrics` footer item
(`/statusline`, `[tui].status_items`). Every value comes from engine
receipts — turn starts, per-model-call usage with stream time,
time-to-first-token and whole-call time, tool start/complete edges, and
provider-reported cache and input tokens. Cells without evidence are
omitted, never estimated. `/status` prints the untrimmed line; the phase
row sheds its lowest-value groups to fit the columns it actually has.
- Auto-Review decisions nobody was prompted for are now visible in the
transcript as one-line notes: model-guardian allow/deny verdicts with
their risk tier and stated reason, guardian failures (denied, fail
closed), deterministic policy blocks, and holds Auto-Review denied
without pausing. The audit log keeps the full record. `/permissions`
ends with the active posture, what it decides on its own versus never,
and the audit-log path. The footer's `Esc to interrupt` hint is
localized. See `docs/design/AUTO_MODE_PARITY.md` for the Claude Code /
Kimi Code parity ledger and follow-ups.
- `codewhale integrations dsh status|plan|connect|update|launch|disable|enable|remove`
connects an existing official DeepSeek Harness (`dsh` 0.1.0-rc.6, verified)
through Codewhale using only its documented seams: a `--patch` overlay that
pins the exact Codewhale provider/model/endpoint identity (native
`deepseek-official` route, or a hand-declared `openai-completions` route
named `codewhale-<provider>` for OpenAI-compatible providers), the
Codewhale permission posture exported as `DSH_PERMISSION_MODE`, and an
append-only receipt. Codewhale writes only under
`$CODEWHALE_HOME/integrations/dsh/`, never copies API keys or edits DSH
files, never broadens permissions (`--allow-full-access` only mirrors an
existing Codewhale full-access posture), and reports not-installed /
offline / incompatible / detected / connected / stale-config /
stale-version / disabled honestly. Anthropic Messages and OpenAI Responses
routes are refused as not carriable. The documented DSH plugin path is an
explicit opt-in: `install-bundle` materializes a Codewhale bundle package
(`codewhale-dsh-bundle`, MIT notice retained) and installs it with
`dsh plugin --profile codewhale add <path>` into a dedicated `codewhale`
profile (pnpm required, reported truthfully when missing; `web`/`headless`
untouched), so `dsh --profile codewhale` alone carries the identity;
`update` regenerates the bundle patch and `remove-bundle` reverses it,
leaving the DSH-owned profile directory in place. `/setup tools` and `codewhale doctor`
show the read-only detection state; `doctor` also lists the DSH read-only
credential consent alongside Codex and Grok. The optional `--skin` export
writes a Codewhale token stylesheet generated from the TUI palette
(Blue Stage dark/light, ombre water column, mode/permission/state colors,
reduced-motion fallbacks); DSH exposes no custom-theme API, so the sheet is
labeled an unsupported overlay and is never injected. See
`docs/INTEGRATIONS_DSH.md`.
### Fixed
- Selecting the `google` provider kind resolved to the `antigravity`
@@ -106,11 +244,77 @@ v0.9.9.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) is
a separate provider: consent-gated read-only import of the official
CLI's login, then a text-only cloud-code stream
(`/v1internal:streamGenerateContent`). Tools, images, and unknown SSE
shapes fail closed. Gemini 3.7 Flash is not advertised until a live
turn succeeds on this wire. The website 44-count still excludes
Antigravity.
- DeepSeek Flash SSE on macOS no longer turns mid-character HTTP/2
flushes into U+FFFD replacement characters (#5374). Invalid UTF-8
fails the line instead of using lossy decode.
- `[workshop] read_result_max_bytes` and `tool_result_max_bytes` raise
the model-visible read/tool-result floor; they never lower the
compile-time defaults and cap at 2MiB (#5367).
- Fireworks and OpenCode Zen DeepSeek V4 Flash/Pro keep a bundled
family rate when the live control plane is down, so session cost is
not stuck on `unverified_live_pricing` (#5241). `kimi-k3` stays
unpriced until a published rate exists.
- Provider setup ships a SenseNova OpenAI-compatible preset (`S`) on
the published `https://token.sensenova.cn/v1` host (#5350). OpenCode
Zen/Go stay first-class rows. Agnes has no published URL, so it has
no preset.
- Privileged release workflows no longer restore rust-cache, sccache,
or npm caches after checking out a caller-supplied SHA (CodeQL
cache-poisoning #88#106). Catalog drift no longer prints raw
bundled/upstream blobs (#107).
- Cancelling a turn now cancels its foreground child agents with it.
- Empty compaction no longer wipes conversation history.
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter
is gone so expanding a pane rematerializes layout the same way shrinking
does.
- The agent tool schema rejects empty calls.
- The local web client keeps recovered stream gaps closed, user questions
answerable, manual bootstrap access intact, and streamed prose quiet for
assistive tech.
- Website zh-Hans copy now says 宪章, matching the TUI pack (#5397,
Lstarsky0).
- Public website provider facts include Google Gemini and Ollama Cloud
(44 runnable routes). Antigravity stays credential-plane-only.
Harvested from #5398 (Lstarsky0) with that correction.
- The website models page carries a truthful read-only settings preview
built from repository facts; it never implies the site can change local
configuration (#5370, #5411, mvanhorn).
- The canonical `ultra` reasoning effort now maps to each provider's
maximum tier alongside the legacy `ultracode` alias, instead of being
silently dropped (#5303, #5409, buiducnhat).
- Session titles truncate by character count, not byte offset, so
multi-byte titles (CJK, emoji) cut at the intended width and word
boundary instead of past the limit (#5415).
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter is
gone so expanding a pane rematerializes layout the same way shrinking does.
- The background verifier test drives the current libtest executable
instead of the rustup `rustc` shim, so the TUI suite no longer depends
on `$HOME` or holds the process-wide test environment lock across an
async wait (#5056, #5423, Isabel Wu).
### Removed
@@ -139,6 +343,27 @@ v0.9.9.
- Site layout uses one container, the ticker no longer implies false
provider readiness, and install links stay in the active locale.
### Contributors
- EvanProgramming (@EvanProgramming) — webhook client panic fallback
(#5381); session-index JSONL mutex (#5382).
- Lstarsky0 (@Lstarsky0) — session peek hides internal runtime events
(#5376); thinking-ladder test re-pin (#5378); provider-count follow-ups
(#5383/#5384); macOS agy fixture canonicalization (#5392); zh-Hans 宪章
terminology (#5397); regenerated website facts harvested and corrected
from #5398.
- Matt Van Horn (@mvanhorn) — read-only models settings preview on the
website (#5411, fixes #5370).
- Nhat Bui (@buiducnhat) — canonical `ultra` reasoning effort mapped across
provider effort tables (#5409); session titles truncated by character
count, not byte offset (#5415).
- Sh1Zuku (@SparkofSpike) — `/title` and the session name in the terminal
tab/window title, plus the mid-turn title deadlock fix (#5419).
- Kai Nacke (@redstar) — Eden AI provider registration, aliases,
`EDENAI_API_KEY`, and the global/EU endpoints (#5422).
- Isabel Wu (@wuisabel-gif) — background verifier test isolated from
rustup and `$HOME` (#5423, slice of #5056).
## [0.9.7] - 2026-08-12
Codewhale v0.9.7 keeps the catalog ordinary. Grok 4.6 lands as a normal catalog
+1 -7
View File
@@ -1,9 +1,3 @@
# Claude Repository Guidance
The full contract is `AGENTS.md`, imported here so it loads automatically:
# Claude entrypoint
@AGENTS.md
Nothing else belongs in this file. Rules added here instead of `AGENTS.md` are
invisible to every non-Claude agent working in this repo, and drift silently
from the copy that isn't.
+59 -4
View File
@@ -84,18 +84,73 @@ cargo clippy --workspace --all-targets --all-features --locked -- \
-A clippy::assertions_on_constants
```
#### Fast local loop
The full gate above is what CI enforces, but you do not need it for every
edit. `crates/tui` is a ~750k-line crate, so the loop that stays fast is
the one that avoids rebuilding it more than necessary (numbers and the
reasoning are in [`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md)):
```bash
# 1. Type-check first (seconds after the first build; no codegen, no link).
scripts/dev-cargo.sh check -p codewhale-tui
# 2. Run only the tests near your change (one crate, one filter).
scripts/dev-test.sh tui fleet_setup
# or: scripts/dev-test.sh crates/tui/src/elapsed.rs
# 3. Run a whole crate's unit suite. scripts/dev-test.sh uses nextest when
# it is installed (one process per test, all cores busy, slow tests
# named; ~100 s here vs ~270 s with libtest).
cargo install cargo-nextest --locked # once
scripts/dev-test.sh tui
scripts/dev-cargo.sh nextest run --workspace --all-features --locked
# 4. Before pushing, run the authoritative gate exactly as CI does:
cargo test --workspace --all-features --locked
```
`.config/nextest.toml` already serializes the PTY suite and bounds the
integration tests that spawn the real binary, so `cargo nextest run` is
safe to use on the whole workspace (nextest does not run doctests; the
authoritative `cargo test` gate does). Tests must not depend on running in
the same process as another test (nextest gives every test its own
process); if a test needs the rustls crypto provider, install it in that
test as production does at startup.
On a machine with less than 16 GB of RAM (or when cross-compiling, e.g.
for OHOS), build one rustc at a time: `CARGO_BUILD_JOBS=1` (or `-j1`), one
crate at a time, `--lib` for tests, never `--workspace`/`--all-targets`.
The tui library needs ~6 GB for its own rustc and its unit-test build ~8 GB;
`cargo test --workspace` runs both at once. Numbers and the full recipe:
[`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md#low-memory-build-recipe-machines-with--16-gb-cross-builds).
If you work in several worktrees, do **not** share one `CARGO_TARGET_DIR`
by default: two cargos on the same target flock and serialize. Use
`scripts/dev-cargo.sh` / `scripts/dev-test.sh`, which give each workspace
its own Cargo `build-dir` (`{workspace-path-hash}` under
`${CODEWHALE_CACHE_ROOT:-${XDG_CACHE_HOME:-$HOME/.cache}/codewhale}`).
`CODEWHALE_DEV_CACHE=local` keeps `./target` if you want that.
`sccache` wraps rustc only when incremental compilation is already off
(`CARGO_INCREMENTAL=0` or `CODEWHALE_SCCACHE=1`) and `sccache` is on
`PATH`; a missing binary is a printed fallback, not an error. Override
the cache root with `CODEWHALE_CACHE_ROOT` — there is no machine-specific
default. A single shared `CARGO_TARGET_DIR` remains valid only for
serialized trunk work. See
[`docs/BUILD_PERFORMANCE.md`](docs/BUILD_PERFORMANCE.md).
Some suites are slow, platform-bound, or intentionally excluded from the
default run; treat them as documented isolation cases rather than
failures of the normal gate:
- **PTY snapshots** (`cargo test -p codewhale-tui --test qa_pty
- **PTY snapshots** (`cargo test -p codewhale-tui --test pty qa_pty
--locked`) are Unix-only and internally serialized. One recovery-boot
case is `#[ignore]`d for a documented input-starvation issue. When a
PTY case fails, rerun that exact case in isolation and diagnose the
rendered frame before calling it a flake; `run_verifiers_background_*`
is the one known full-suite-parallelism flake that passes in
isolation.
- **Release runtime QA** (`cargo test -p codewhale-tui --test
- **Release runtime QA** (`cargo test -p codewhale-tui --test pty
release_runtime_qa --locked`) includes an `#[ignore]`d 32-worker storm
benchmark that is only run explicitly for evidence gathering.
- **OCR** (`image_ocr`) uses the macOS Vision framework or a locally
@@ -327,8 +382,8 @@ reopened, ask the contributor to resubmit after the allowlist PR is merged.
## Agent-Assisted Improvements
Codewhale is allowed to help improve Codewhale, but the contribution still has
to be shaped for human review. The recommended workflow is the
[recursive self-improvement prompt](the `codewhale-ops` repo): run it
to be shaped for human review. The recommended workflow is the recursive self-improvement prompt
in the private `codewhale-ops` repo: run it
from a fresh fork or branch, let the agent find exactly one small friction point,
and stop after one patch. DeepSeek V4 Pro is the reference path for this loop
today, but any configured provider works — the review shape matters more than
Generated
+68 -140
View File
@@ -877,10 +877,11 @@ dependencies = [
"codewhale-protocol",
"codewhale-state",
"codewhale-tools",
"regex",
"serde",
"serde_json",
"tempfile",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tokio",
"tokio-util",
"tracing",
@@ -978,7 +979,7 @@ dependencies = [
"serde_json",
"sha2 0.11.0",
"tempfile",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tracing",
]
@@ -1026,7 +1027,7 @@ dependencies = [
"codewhale-protocol",
"serde",
"serde_json",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tokio",
"uuid",
]
@@ -1071,6 +1072,7 @@ dependencies = [
"htmd",
"ignore",
"image",
"jsonschema",
"libc",
"lru",
"mimalloc",
@@ -1103,7 +1105,7 @@ dependencies = [
"syntect",
"tar",
"tempfile",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tiny_http",
"tokio",
"tokio-util",
@@ -1133,7 +1135,7 @@ dependencies = [
"serde_json",
"sha2 0.11.0",
"tempfile",
"thiserror 2.0.19",
"thiserror 2.0.20",
"toml 1.1.4+spec-1.1.0",
]
@@ -1142,11 +1144,11 @@ name = "codewhale-workflow-js"
version = "0.9.8"
dependencies = [
"async-trait",
"jsonschema 0.49.9",
"jsonschema",
"rquickjs",
"serde",
"serde_json",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tokio",
]
@@ -1842,17 +1844,6 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "fancy-regex"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fastrand"
version = "2.5.0"
@@ -2167,7 +2158,7 @@ dependencies = [
"serde_json",
"syn 2.0.119",
"textwrap",
"thiserror 2.0.19",
"thiserror 2.0.20",
"typed-builder",
]
@@ -2774,7 +2765,7 @@ dependencies = [
"jni-sys",
"log",
"simd_cesu8",
"thiserror 2.0.19",
"thiserror 2.0.20",
"walkdir",
"windows-link",
]
@@ -2837,11 +2828,11 @@ dependencies = [
"getrandom 0.3.4",
"idna",
"itoa",
"jsonschema-regex 0.46.10",
"jsonschema-regex",
"num-cmp",
"num-traits",
"percent-encoding",
"referencing 0.46.10",
"referencing",
"regex",
"serde",
"serde_json",
@@ -2849,35 +2840,6 @@ dependencies = [
"uuid-simd",
]
[[package]]
name = "jsonschema"
version = "0.49.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ec8a241beed129f06114aa68007e905ca350e7baeb6e17a7631bb7978d91b2"
dependencies = [
"ahash",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex 0.19.0",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"jsonschema-regex 0.49.9",
"jsonschema-value",
"num-cmp",
"num-traits",
"percent-encoding",
"referencing 0.49.9",
"regex",
"serde",
"serde_json",
"strum 0.28.0",
"unicode-general-category",
"uuid-simd",
]
[[package]]
name = "jsonschema-regex"
version = "0.46.10"
@@ -2887,29 +2849,6 @@ dependencies = [
"regex-syntax",
]
[[package]]
name = "jsonschema-regex"
version = "0.49.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91994f45017ed5e66aa8e59b8415f4cb033a6380d7200387b7cf117595fbdf85"
dependencies = [
"regex-syntax",
]
[[package]]
name = "jsonschema-value"
version = "0.49.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ec7637f83e510868ae6ed625f7ebfbbde4554ee8ce49854caa5126a8b9b9ecb"
dependencies = [
"ahash",
"bytecount",
"fraction",
"num-cmp",
"num-traits",
"serde_json",
]
[[package]]
name = "kasuari"
version = "0.4.12"
@@ -2918,7 +2857,7 @@ checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899"
dependencies = [
"hashbrown 0.16.1",
"portable-atomic",
"thiserror 2.0.19",
"thiserror 2.0.20",
]
[[package]]
@@ -4037,7 +3976,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tokio",
"tracing",
"web-time",
@@ -4059,7 +3998,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tinyvec",
"tracing",
"web-time",
@@ -4158,16 +4097,18 @@ dependencies = [
[[package]]
name = "ratatui"
version = "0.30.0"
version = "0.30.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
dependencies = [
"instability",
"ratatui-core",
"ratatui-crossterm",
"ratatui-macros",
"ratatui-termina",
"ratatui-termwiz",
"ratatui-widgets",
"serde",
]
[[package]]
@@ -4185,8 +4126,8 @@ dependencies = [
"lru",
"palette",
"serde",
"strum 0.28.0",
"thiserror 2.0.19",
"strum",
"thiserror 2.0.20",
"unicode-segmentation",
"unicode-truncate",
"unicode-width",
@@ -4194,9 +4135,9 @@ dependencies = [
[[package]]
name = "ratatui-crossterm"
version = "0.1.0"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3"
checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
dependencies = [
"cfg-if 1.0.4",
"crossterm",
@@ -4206,19 +4147,30 @@ dependencies = [
[[package]]
name = "ratatui-macros"
version = "0.7.0"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4"
checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814"
dependencies = [
"ratatui-core",
"ratatui-widgets",
]
[[package]]
name = "ratatui-termwiz"
name = "ratatui-termina"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c"
checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2"
dependencies = [
"instability",
"ratatui-core",
"termina",
]
[[package]]
name = "ratatui-termwiz"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977"
dependencies = [
"ratatui-core",
"termwiz",
@@ -4226,18 +4178,19 @@ dependencies = [
[[package]]
name = "ratatui-widgets"
version = "0.3.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
dependencies = [
"bitflags 2.13.1",
"hashbrown 0.16.1",
"hashbrown 0.17.1",
"indoc",
"instability",
"itertools 0.14.0",
"line-clipping",
"ratatui-core",
"strum 0.27.2",
"serde",
"strum",
"time",
"unicode-segmentation",
"unicode-width",
@@ -4260,7 +4213,7 @@ checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror 2.0.19",
"thiserror 2.0.20",
]
[[package]]
@@ -4300,23 +4253,6 @@ dependencies = [
"serde_json",
]
[[package]]
name = "referencing"
version = "0.49.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6efa2154ea6f5ce0fdecdd2a8d18f2fa1a39a8fbba91564f555a592e4dce8278"
dependencies = [
"ahash",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.17.1",
"itoa",
"micromap",
"parking_lot",
"percent-encoding",
"serde_json",
]
[[package]]
name = "regex"
version = "1.13.1"
@@ -4511,7 +4447,7 @@ dependencies = [
"reqwest 0.13.4",
"serde",
"serde_json",
"thiserror 2.0.19",
"thiserror 2.0.20",
"tokio",
"tokio-stream",
"tokio-util",
@@ -4575,7 +4511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c"
dependencies = [
"hashbrown 0.16.1",
"thiserror 2.0.19",
"thiserror 2.0.20",
]
[[package]]
@@ -4808,7 +4744,7 @@ dependencies = [
"crossterm",
"include_dir",
"indexmap",
"jsonschema 0.46.10",
"jsonschema",
"percent-encoding",
"ratatui",
"regex",
@@ -5296,34 +5232,13 @@ version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
dependencies = [
"strum_macros 0.27.2",
]
[[package]]
name = "strum"
version = "0.28.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
dependencies = [
"strum_macros 0.28.0",
]
[[package]]
name = "strum_macros"
version = "0.27.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.119",
"strum_macros",
]
[[package]]
@@ -5413,7 +5328,7 @@ dependencies = [
"serde",
"serde_derive",
"serde_json",
"thiserror 2.0.19",
"thiserror 2.0.20",
"walkdir",
"yaml-rust",
]
@@ -5501,6 +5416,19 @@ dependencies = [
"new_debug_unreachable",
]
[[package]]
name = "termina"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e"
dependencies = [
"bitflags 2.13.1",
"parking_lot",
"rustix",
"signal-hook 0.3.18",
"windows-sys 0.61.2",
]
[[package]]
name = "terminal_size"
version = "0.4.4"
@@ -5596,11 +5524,11 @@ dependencies = [
[[package]]
name = "thiserror"
version = "2.0.19"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f"
dependencies = [
"thiserror-impl 2.0.19",
"thiserror-impl 2.0.20",
]
[[package]]
@@ -5616,9 +5544,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
version = "2.0.19"
version = "2.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af"
dependencies = [
"proc-macro2",
"quote",
+4 -1
View File
@@ -45,7 +45,10 @@ clap = { version = "4.5.54", features = ["derive"] }
clap_complete = "4.5"
dirs = "6.0.0"
encoding_rs = "0.8.35"
jsonschema = { version = "0.49", default-features = false }
# Pinned to the jsonschema line schemaui 0.12 requires (^0.46) so the graph
# carries one jsonschema/jsonschema-regex/referencing/fancy-regex stack.
# Move both together when schemaui catches up (dependabot: keep in step).
jsonschema = { version = "0.46", default-features = false }
reqwest = { version = "0.13.1", default-features = false, features = ["json", "rustls-no-provider", "socks"] }
# NOT "parallel": the Workflow VM stays single-threaded and bridges to the
# multi-thread engine over channels (see crates/workflow-js).
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Un agente de programación de código abierto para tu terminal — trae tu propio modelo.
@@ -90,6 +90,21 @@ aprobación.
- **Trabajo que puedes retomar.** Un fleet registra cada paso en un libro mayor
de solo agregado, así que `fleet resume` retoma donde te detuviste.
## Integraciones
- **DeepSeek Harness (dsh) — conectado a través de Codewhale.**
`codewhale integrations dsh connect` vincula una instalación existente de
`@deepseek-ai/dsh` a tu ruta de proveedor, permisos y espacio de trabajo de
Codewhale; `integrations dsh install-bundle` añade el paquete de plugin de
DSH opcional para que `dsh --profile codewhale` lleve esa identidad por sí
mismo. Codewhale tiene la autoridad sobre permisos y ciclo de vida; dsh
conserva sus sesiones, perfiles y credenciales intactos. Consulta
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** El andamiaje oficial de la extensión (`extensions/vscode`)
abre Codewhale en una terminal integrada y expone una Agent View de solo
lectura sobre el runtime local. Es una vista previa de desarrollo local, no
un lanzamiento en marketplace.
## Para saber más
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — cada ruta de proveedor: alojada,
@@ -140,4 +155,4 @@ experiencia de agente en terminal.
[MIT](LICENSE). Proyecto comunitario independiente; sin afiliación con ningún
proveedor de modelos.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale desplegando tres subagentes scout de solo lectura en una terminal](assets/fanout.gif)
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Sebuah coding agent sumber terbuka untuk terminal Anda — bawa model pilihan Anda sendiri.
@@ -47,6 +47,21 @@ Di dalam TUI: `/model` mengganti penyedia dan model sekaligus, `/fleet` menjalan
- **Read-only sampai Anda memberi izin lebih.** Mode Plan tidak dapat mengubah berkas, dan gerbang persetujuan memproteksi perintah berisiko. Ketika sandbox OS membungkus perintah, Codewhale akan menginformasikannya: Seatbelt pada macOS (jika tersedia), serta opsi bubblewrap di Linux. Berkas `constitution.json` repositori dikompilasi menjadi pembatas penulisan yang bahkan tidak dapat dilewati oleh mode Full Access.
- **Pekerjaan yang dapat dilanjutkan.** Fleet mencatat setiap langkah ke ledger bertipe append-only, sehingga `fleet resume` dapat melanjutkan pekerjaan tepat di mana Anda meninggalkannya.
## Integrasi
- **DeepSeek Harness (dsh) — terhubung melalui Codewhale.**
`codewhale integrations dsh connect` menghubungkan instalasi
`@deepseek-ai/dsh` yang sudah ada ke rute provider, izin, dan ruang kerja
Codewhale Anda; `integrations dsh install-bundle` menambahkan bundel plugin
DSH opsional sehingga `dsh --profile codewhale` membawa identitas itu secara
mandiri. Codewhale memegang izin dan otoritas siklus hidup; dsh tetap
mempertahankan sesi, profil, dan kredensialnya sendiri tanpa tersentuh.
Lihat [docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** Kerangka ekstensi resmi (`extensions/vscode`) membuka
Codewhale di terminal terintegrasi dan menyajikan Agent View hanya-baca
melalui runtime lokal. Ini adalah pratinjau pengembangan lokal, bukan rilis
marketplace.
## Pelajari Lebih Lanjut
- [docs/PROVIDERS.id.md](docs/PROVIDERS.id.md) ([English](docs/PROVIDERS.md)) — setiap rute penyedia: hosted, gateway, dan lokal
@@ -75,4 +90,4 @@ Terima kasih kepada [DeepSeek](https://github.com/deepseek-ai) untuk model dan d
[MIT](LICENSE). Sebuah proyek komunitas independen, tidak terafiliasi dengan penyedia model mana pun.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale memecah tiga subagent scout hanya-baca di terminal](assets/fanout.gif)
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
ターミナルで動くオープンソースのコーディングエージェント — モデルはあなたが持ち込む。
@@ -47,6 +47,21 @@ TUI では、`/model` がプロバイダとモデルをまとめて切り替え
- **許可するまでは読み取り専用。** Plan モードはファイルを変更せず、リスクのあるコマンドは承認でゲートされます。OS サンドボックスが実際にコマンドをラップするとき、Codewhale はそれを明示します。macOS では利用可能な Seatbelt、Linux ではオプトインの bubblewrap です。リポジトリの `constitution.json` は書き込みホールドへとコンパイルされ、Full Access でもスキップできません。
- **再開できる作業。** Fleet はすべてのステップを追記専用の台帳に記録するので、`fleet resume` で止めたところから再開できます。
## インテグレーション
- **DeepSeek Harnessdsh)— Codewhale 経由で接続。**
`codewhale integrations dsh connect` は既存の `@deepseek-ai/dsh`
インストールを Codewhale のプロバイダールート、権限、ワークスペースに
接続し、`integrations dsh install-bundle` はオプトインの DSH プラグイン
バンドルを追加して、`dsh --profile codewhale` が単独で同じ ID を持てる
ようにします。権限とライフサイクルは Codewhale が管理し、dsh の
セッション、プロファイル、認証情報は一切変更されません。
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md) を参照。
- **VS Code。** 公式拡張機能の雛形(`extensions/vscode`)は Codewhale を
統合ターミナルで開き、ローカルランタイム経由の読み取り専用 Agent View
を提供します。現在はローカル開発プレビューであり、マーケットプレイス
版ではありません。
## さらに詳しく
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — ホスト型・ゲートウェイ・ローカル
@@ -82,4 +97,4 @@ Issue、PR、再現手順、ログ、機能要望は、どれもここでは本
[MIT](LICENSE)。独立したコミュニティプロジェクトであり、いかなるモデルプロバイダとも提携していません。
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![ターミナルで 3 つの読み取り専用 scout サブエージェントを並列起動する Codewhale](assets/fanout.gif)
+16 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
터미널에서 쓰는 오픈소스 코딩 에이전트 — 모델은 당신이 가져옵니다.
@@ -47,6 +47,20 @@ TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/f
- **허용하기 전까지는 읽기 전용.** Plan 모드는 파일을 바꾸지 않고, 위험한 명령은 승인을 거칩니다. OS 샌드박스가 실제로 명령을 래핑할 때 Codewhale은 이를 그대로 표시합니다. macOS에서는 사용 가능한 Seatbelt, Linux에서는 옵트인 bubblewrap입니다. 저장소의 `constitution.json`은 Full Access조차 건너뛸 수 없는 쓰기 홀드로 컴파일됩니다.
- **이어서 할 수 있는 작업.** Fleet은 모든 단계를 추가 전용 원장에 기록하므로, `fleet resume`으로 멈춘 지점부터 이어갈 수 있습니다.
## 통합
- **DeepSeek Harness(dsh) — Codewhale로 연결.**
`codewhale integrations dsh connect`는 기존 `@deepseek-ai/dsh` 설치를
Codewhale의 제공자 라우트·권한·작업 공간에 연결하고,
`integrations dsh install-bundle`은 옵트인 DSH 플러그인 번들을 추가해
`dsh --profile codewhale`이 해당 정체성을 단독으로 유지하게 합니다.
권한과 수명 주기는 Codewhale이 담당하며, dsh 고유의 세션·프로필·자격
증명은 그대로 유지됩니다.
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md) 참조.
- **VS Code.** 공식 확장 스캐폴드(`extensions/vscode`)는 통합 터미널에서
Codewhale을 열고 로컬 런타임 기반의 읽기 전용 Agent View를 제공합니다.
현재는 로컬 개발 프리뷰이며 마켓플레이스 릴리스가 아닙니다.
## 더 알아보기
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — 호스팅·게이트웨이·로컬까지 모든
@@ -84,4 +98,4 @@ TUI 안에서: `/model`은 프로바이더와 모델을 함께 전환하고, `/f
[MIT](LICENSE). 독립 커뮤니티 프로젝트이며, 어떤 모델 프로바이더와도 제휴 관계가 없습니다.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![터미널에서 읽기 전용 scout 하위 에이전트 세 개를 병렬로 펼치는 Codewhale](assets/fanout.gif)
+16 -1
View File
@@ -87,6 +87,21 @@ shell command through the normal approval path.
- **Work you can resume.** A fleet records every step to an append-only ledger,
so `fleet resume` picks up where you left off.
## Integrations
- **DeepSeek Harness (dsh) — connected through Codewhale.**
`codewhale integrations dsh connect` links an existing `@deepseek-ai/dsh`
install to your Codewhale provider route, permissions, and workspace, and
`integrations dsh install-bundle` adds the opt-in DSH plugin bundle so
`dsh --profile codewhale` carries that identity on its own. Codewhale owns
permissions and lifecycle authority; dsh keeps its own sessions, profiles,
and credentials untouched. See
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** The official extension scaffold (`extensions/vscode`) opens
Codewhale in an integrated terminal and exposes a read-only Agent View over
the local runtime. It is a local-development preview, not a marketplace
release yet.
## Learn more
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — every provider route: hosted,
@@ -133,4 +148,4 @@ terminal-agent experience.
[MIT](LICENSE). An independent community project, not affiliated with any model
provider.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale fanning out three read-only scout subagents in a terminal](assets/fanout.gif)
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Um agente de programação de código aberto para o seu terminal — traga o seu próprio modelo.
@@ -87,6 +87,21 @@ executa um comando de shell pelo caminho normal de aprovação.
livro-razão de apenas inclusão, então `fleet resume` retoma de onde você
parou.
## Integrações
- **DeepSeek Harness (dsh) — conectado via Codewhale.**
`codewhale integrations dsh connect` vincula uma instalação existente do
`@deepseek-ai/dsh` à sua rota de provedor, permissões e espaço de trabalho
do Codewhale; `integrations dsh install-bundle` adiciona o pacote de plugin
DSH opcional para que `dsh --profile codewhale` carregue essa identidade por
conta própria. O Codewhale detém a autoridade sobre permissões e ciclo de
vida; o dsh mantém suas sessões, perfis e credenciais intactos. Veja
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** O scaffold oficial da extensão (`extensions/vscode`) abre o
Codewhale em um terminal integrado e expõe uma Agent View somente leitura
sobre o runtime local. É uma prévia de desenvolvimento local, não um
lançamento no marketplace.
## Saiba mais
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — cada rota de provedor: hospedada,
@@ -137,4 +152,4 @@ experiência de agente no terminal.
[MIT](LICENSE). Projeto comunitário independente; sem afiliação com nenhum
provedor de modelos.
[![Gráfico de Star History](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale distribuindo três subagentes scout somente leitura em um terminal](assets/fanout.gif)
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Открытый агент для программирования в вашем терминале — модель приносите с собой.
@@ -89,6 +89,21 @@ Work / Operate; если в поле есть текст, `Tab` дополняе
доступный только на добавление, поэтому `fleet resume` продолжает с того места,
где вы остановились.
## Интеграции
- **DeepSeek Harness (dsh) — подключается через Codewhale.**
`codewhale integrations dsh connect` связывает существующую установку
`@deepseek-ai/dsh` с вашим маршрутом провайдера, правами и рабочей
областью Codewhale; `integrations dsh install-bundle` добавляет
опциональный бандл-плагин DSH, чтобы `dsh --profile codewhale` нёс эту
идентичность самостоятельно. Права и жизненный цикл остаются за
Codewhale; сессии, профили и учётные данные dsh не затрагиваются. См.
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** Официальный каркас расширения (`extensions/vscode`) открывает
Codewhale во встроенном терминале и даёт read-only Agent View поверх
локального рантайма. Это превью для локальной разработки, а не релиз в
маркетплейсе.
## Узнать больше
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — все маршруты провайдеров: облачные,
@@ -137,4 +152,4 @@ Work / Operate; если в поле есть текст, `Tab` дополняе
[MIT](LICENSE). Независимый проект сообщества, не аффилированный ни с одним
провайдером моделей.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale запускает три read-only scout-субагента параллельно в терминале](assets/fanout.gif)
+17 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Агент для програмування з відкритим кодом у вашому терміналі — модель приносите ви.
@@ -90,6 +90,21 @@ Operate; якщо в полі є текст, `Tab` доповнює слеш-к
- **Робота, яку можна відновити.** Флот записує кожен крок до журналу, що лише
доповнюється, тож `fleet resume` підхоплює роботу з місця, де ви зупинились.
## Інтеграції
- **DeepSeek Harness (dsh) — підключається через Codewhale.**
`codewhale integrations dsh connect` зв'язує наявну інсталяцію
`@deepseek-ai/dsh` з вашим маршрутом провайдера, дозволами та робочою
областю Codewhale; `integrations dsh install-bundle` додає опціональний
бандл-плагін DSH, щоб `dsh --profile codewhale` ніс цю ідентичність
самостійно. Дозволи та життєвий цикл лишаються за Codewhale; сесії,
профілі та облікові дані dsh не зачіпаються. Див.
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** Офіційний каркас розширення (`extensions/vscode`) відкриває
Codewhale у вбудованому терміналі та дає Agent View лише для читання
поверх локального рантайму. Це прев'ю для локальної розробки, а не реліз
у маркетплейсі.
## Дізнатися більше
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — кожен маршрут провайдера: хмарний,
@@ -137,4 +152,4 @@ Operate; якщо в полі є текст, `Tab` доповнює слеш-к
[MIT](LICENSE). Незалежний проєкт спільноти, не пов'язаний із жодним
провайдером моделей.
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale розгалужує три read-only scout-субагенти паралельно в терміналі](assets/fanout.gif)
+16 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
Một coding agent mã nguồn mở cho terminal của bạn — mang theo model của riêng bạn.
@@ -83,6 +83,20 @@ shell qua đường phê duyệt bình thường.
- **Công việc bạn có thể tiếp tục.** Fleet ghi lại từng bước vào sổ cái chỉ ghi
thêm, nên `fleet resume` tiếp tục từ chỗ bạn dừng.
## Tích hợp
- **DeepSeek Harness (dsh) — kết nối qua Codewhale.**
`codewhale integrations dsh connect` liên kết bản cài `@deepseek-ai/dsh`
hiện có với tuyến provider, quyền và workspace Codewhale của bạn;
`integrations dsh install-bundle` thêm gói plugin DSH tùy chọn để
`dsh --profile codewhale` tự mang danh tính đó. Codewhale nắm quyền và
vòng đời; dsh giữ nguyên phiên, profile và thông tin xác thực của riêng nó.
Xem [docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md).
- **VS Code.** Bộ khung extension chính thức (`extensions/vscode`) mở
Codewhale trong terminal tích hợp và cung cấp Agent View chỉ đọc qua
runtime cục bộ. Đây là bản xem trước phát triển cục bộ, chưa phải bản phát
hành marketplace.
## Tìm hiểu thêm
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — mọi route provider: dịch vụ,
@@ -130,4 +144,4 @@ trải nghiệm agent trên terminal.
[MIT](LICENSE). Dự án cộng đồng độc lập; không trực thuộc bất kỳ nhà cung cấp
model nào.
[![Biểu đồ Star History](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale phân nhánh ba subagent scout chỉ đọc trong terminal](assets/fanout.gif)
+15 -2
View File
@@ -1,4 +1,4 @@
<!-- source: README.md sha256:3a759de92f8b -->
<!-- source: README.md sha256:1569156eb887 -->
# Codewhale
一个面向终端的开源编程智能体——模型由你自带。
@@ -47,6 +47,19 @@ codewhale web # local browser client on 127.0.0.1
- **默认只读,放开权限才更进一步。** Plan 模式不改动文件,审批把关每一次高风险命令。只有当命令确实被 OS 沙箱包装时,Codewhale 才会如实标明:macOS 上是可用时启用的 Seatbelt,Linux 上是需显式启用的 bubblewrap。仓库的 `constitution.json` 会编译成写入拦截,连 Full Access 也无法跳过。
- **随时可以续跑的工作。** Fleet 把每一步记录在只追加的账本里,`fleet resume` 从你停下的地方继续。
## 集成
- **DeepSeek Harnessdsh)——通过 Codewhale 连接。**
`codewhale integrations dsh connect` 可将现有的 `@deepseek-ai/dsh` 安装
连接到你的 Codewhale 提供商路由、权限和工作区;`integrations dsh
install-bundle` 会添加可选的 DSH 插件包,让 `dsh --profile codewhale`
独立携带同一身份。Codewhale 负责权限与生命周期;dsh 保留自己的会话、
配置文件和凭据,不会被改动。详见
[docs/INTEGRATIONS_DSH.md](docs/INTEGRATIONS_DSH.md)。
- **VS Code。** 官方扩展脚手架(`extensions/vscode`)可在集成终端中打开
Codewhale,并通过本地运行时提供只读的 Agent View。目前仍是本地开发
预览版,尚未发布到插件市场。
## 了解更多
- [docs/PROVIDERS.md](docs/PROVIDERS.md) — 每一条 provider 路由:托管、网关与本地
@@ -74,4 +87,4 @@ Issue、PR、复现步骤、日志和功能请求,在这里都算真实的项目
[MIT](LICENSE)。独立的社区项目,与任何模型 provider 均无隶属关系。
[![Star History Chart](https://api.star-history.com/chart?repos=Hmbown/CodeWhale&type=date&legend=top-left)](https://www.star-history.com/?repos=Hmbown%2FCodeWhale&type=date)
![Codewhale 在终端中并行派出三个只读 scout 子代理](assets/fanout.gif)
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

+18 -12
View File
@@ -43,14 +43,14 @@ base_url = "https://api.deepseek.com/beta"
# xiaomi/mimo-v2.5-pro — OpenRouter Xiaomi MiMo 2.5 Pro
# xiaomi/mimo-v2.5 — OpenRouter Xiaomi MiMo 2.5
# z-ai/glm-5.1 — OpenRouter Z.AI GLM 5.1
# z-ai/glm-5.2 — OpenRouter Z.AI GLM 5.2 (default)
# z-ai/glm-5.3 — OpenRouter Z.AI GLM 5.3 (registered only; not released by Z.ai
# as of 2026-08-03 — metadata inherited from 5.2, unpriced)
# z-ai/glm-5.2 — OpenRouter Z.AI GLM 5.2
# z-ai/glm-5.3 — OpenRouter Z.AI GLM 5.3 (live on Z.ai since 2026-08-13;
# metadata inherited from 5.2, unpriced)
# z-ai/glm-5-turbo — OpenRouter Z.AI GLM 5 Turbo (scout fast sibling)
# GLM-5.2 — default direct Z.AI Coding Plan model
# GLM-5.3 — default direct Z.AI Coding Plan model (live since 2026-08-13;
# metadata inherited from 5.2, unpriced)
# GLM-5.2 — direct Z.AI GLM 5.2 (previous default; explicit selections keep it)
# GLM-5.1 — direct Z.AI smaller model
# GLM-5.3 — direct Z.AI GLM 5.3 (registered only; not live on the Z.ai API
# as of 2026-08-03 — metadata inherited from 5.2, unpriced)
# GLM-5-Turbo — direct Z.AI fast model (scout fast sibling)
# step-3.7-flash — default direct StepFun / StepFlash model ID
# kimi-k3 — direct Moonshot K3 model ID (1M context)
@@ -615,12 +615,12 @@ max_subagents = 10 # optional (1-20)
# base_url = "https://api.z.ai/api/coding/paas/v4"
# # General API endpoint, if you are not using the Coding Plan:
# # base_url = "https://api.z.ai/api/paas/v4"
# model = "GLM-5.2" # default; GLM-5.1 is the smaller model, GLM-5-Turbo the fast sub-agent sibling
# # GLM-5.3 is registered/selectable (model = "GLM-5.3") so the id resolves to
# # Z.ai instead of being rewritten to another model, but it was NOT live on the
# # Z.ai API as of 2026-08-03 and will fail upstream until Z.ai ships it. Its
# # catalog metadata is inherited from GLM-5.2 pending official Z.ai release
# # metadata, and it carries no price. GLM-5.2 remains the default.
# model = "GLM-5.3" # default; GLM-5.2 is the previous default, GLM-5.1 the smaller model, GLM-5-Turbo the fast sub-agent sibling
# # GLM-5.3 is live on the Z.ai Coding Plan (2026-08-13). Its catalog metadata
# # (limits, reasoning options) is inherited from GLM-5.2 until Z.ai publishes
# # distinct 5.3 numbers, and it carries no price. An explicit model = "GLM-5.2"
# # keeps sending GLM-5.2; only the default moved. Accounts not provisioned for
# # 5.3 can still see a 429 with entitlement code 1311.
# StepFun / StepFlash direct OpenAI-compatible endpoint (https://platform.stepfun.ai)
[providers.stepfun]
@@ -1085,6 +1085,12 @@ exponential_base = 2.0
#
# [workshop]
# large_output_threshold_tokens = 4096
# # Optional model-visible byte ceilings (#5367). Absent keeps the
# # compile-time defaults (read 50KiB / read_file 16KiB, then the
# # compact 12K-char floor). Values raise the floor; they never lower
# # it. Hard cap is 2MiB.
# # read_result_max_bytes = 102400
# # tool_result_max_bytes = 102400
# [workshop.per_tool_thresholds]
# Bash = 2048 # shell output synthesised aggressively
# Web = 8192 # web results can be large; give them more room
+41 -18
View File
@@ -432,20 +432,6 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "GLM-5.2".to_string(),
provider: ProviderKind::Zai,
aliases: vec![
"glm-5.2".to_string(),
"glm-5-2".to_string(),
"zai-glm-5.2".to_string(),
"zai-glm-5-2".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
// Listed after GLM-5.2 on purpose: the first Zai row is the
// provider default and GLM-5.2 keeps that seat.
ModelInfo {
id: "GLM-5.3".to_string(),
provider: ProviderKind::Zai,
@@ -458,6 +444,20 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
// The first Z.ai row is the provider default. Keep this ordering
// aligned with `DEFAULT_ZAI_MODEL` in codewhale-config.
ModelInfo {
id: "GLM-5.2".to_string(),
provider: ProviderKind::Zai,
aliases: vec![
"glm-5.2".to_string(),
"glm-5-2".to_string(),
"zai-glm-5.2".to_string(),
"zai-glm-5-2".to_string(),
],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "GLM-5.1".to_string(),
provider: ProviderKind::Zai,
@@ -749,6 +749,13 @@ impl Default for ModelRegistry {
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "gpt-oss:120b".to_string(),
provider: ProviderKind::OllamaCloud,
aliases: vec![],
supports_tools: true,
supports_reasoning: true,
},
ModelInfo {
id: "deepseek-ai/DeepSeek-V4-Pro".to_string(),
provider: ProviderKind::Huggingface,
@@ -1340,12 +1347,15 @@ impl ModelRegistry {
if let Some(name) = requested {
fallback_chain.push(format!("requested:{name}"));
if provider_hint == Some(ProviderKind::Ollama) {
if matches!(
provider_hint,
Some(ProviderKind::Ollama | ProviderKind::OllamaCloud)
) {
return ModelResolution {
requested: Some(name.to_string()),
resolved: ModelInfo {
id: name.trim().to_string(),
provider: ProviderKind::Ollama,
provider: provider_hint.expect("matched provider hint"),
aliases: Vec::new(),
supports_tools: true,
supports_reasoning: false,
@@ -1993,10 +2003,13 @@ mod tests {
fn zai_direct_models_resolve_when_provider_hinted() {
let registry = ModelRegistry::default();
// GLM-5.2 is now the default direct Z.AI model.
// Keep the agent registry fallback aligned with codewhale-config's
// DEFAULT_ZAI_MODEL.
let default = registry.resolve(None, Some(ProviderKind::Zai));
assert_eq!(default.resolved.provider, ProviderKind::Zai);
assert_eq!(default.resolved.id, "GLM-5.2");
assert_eq!(default.resolved.id, "GLM-5.3");
assert!(default.used_fallback);
assert_eq!(default.fallback_chain, ["provider_default:zai"]);
for (alias, expected) in [
("GLM-5.1", "GLM-5.1"),
@@ -2358,6 +2371,16 @@ mod tests {
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn ollama_cloud_default_uses_the_hosted_catalog_model_id() {
let registry = ModelRegistry::default();
let resolved = registry.resolve(None, Some(ProviderKind::OllamaCloud));
assert_eq!(resolved.resolved.provider, ProviderKind::OllamaCloud);
assert_eq!(resolved.resolved.id, "gpt-oss:120b");
assert!(resolved.resolved.supports_reasoning);
}
#[test]
fn ollama_requested_model_tag_is_preserved() {
let registry = ModelRegistry::default();
+2 -5
View File
@@ -224,6 +224,7 @@ fn endpoint_preserves_raw_model_ids(provider: ProviderKind, base_url: &str) -> b
provider,
ProviderKind::Custom
| ProviderKind::Ollama
| ProviderKind::OllamaCloud
| ProviderKind::Vllm
| ProviderKind::Sglang
| ProviderKind::OpencodeZen
@@ -485,17 +486,13 @@ mod tests {
use axum::http::{Method, Request};
use codewhale_config::provider::WireFormat;
use std::fs;
use std::sync::OnceLock;
use tokio::sync::mpsc;
use tower::ServiceExt;
use super::super::{app_router, build_state};
fn install_crypto_provider() {
static INIT: OnceLock<()> = OnceLock::new();
INIT.get_or_init(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
crate::install_test_crypto_provider();
}
/// Start a minimal upstream mock server that echoes back what it received.
+14
View File
@@ -2009,6 +2009,18 @@ async fn persist_config(state: &AppState, config: codewhale_config::ConfigToml)
store.save()
}
/// Install the process-wide rustls crypto provider once for tests that build
/// an HTTP client. Production installs it at startup; each test must do the
/// same instead of relying on another test in the process having run first
/// (nextest runs every test in its own process).
#[cfg(test)]
pub(crate) fn install_test_crypto_provider() {
static INIT: std::sync::OnceLock<()> = std::sync::OnceLock::new();
INIT.get_or_init(|| {
let _ = rustls::crypto::ring::default_provider().install_default();
});
}
#[cfg(test)]
mod tests {
use super::*;
@@ -2336,6 +2348,7 @@ mod tests {
#[tokio::test]
async fn failed_config_set_keeps_the_stdio_bridge() {
crate::install_test_crypto_provider();
// #4737: `set_value` rejects an invalid value before assigning, so the
// request is a no-op — but `apply_config_update` ran anyway and
// invalidated the cached bridge, dropping the child runtime along with
@@ -2379,6 +2392,7 @@ mod tests {
#[tokio::test]
async fn successful_config_set_still_invalidates_the_stdio_bridge() {
crate::install_test_crypto_provider();
// The other half of #4737: a mutation that *did* happen must still
// rebuild the bridge, or the runtime keeps serving the old config.
let tmp = tempfile::tempdir().expect("tempdir");
+40 -2
View File
@@ -60,6 +60,8 @@ enum ProviderArg {
Sglang,
Vllm,
Ollama,
#[value(alias = "ollama_cloud")]
OllamaCloud,
Huggingface,
Together,
OpenaiCodex,
@@ -110,6 +112,11 @@ enum ProviderArg {
Mistral,
/// Google Gemini (official OpenAI-compatible endpoint).
Google,
/// Google Antigravity (`agy`) — consent-gated OAuth import.
#[value(alias = "agy")]
Antigravity,
#[value(alias = "eden-ai", alias = "eden_ai")]
Edenai,
}
impl From<ProviderArg> for ProviderKind {
@@ -133,6 +140,7 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Sglang => ProviderKind::Sglang,
ProviderArg::Vllm => ProviderKind::Vllm,
ProviderArg::Ollama => ProviderKind::Ollama,
ProviderArg::OllamaCloud => ProviderKind::OllamaCloud,
ProviderArg::Huggingface => ProviderKind::Huggingface,
ProviderArg::Together => ProviderKind::Together,
ProviderArg::OpenaiCodex => ProviderKind::OpenaiCodex,
@@ -151,6 +159,8 @@ impl From<ProviderArg> for ProviderKind {
ProviderArg::Xai => ProviderKind::Xai,
ProviderArg::Mistral => ProviderKind::Mistral,
ProviderArg::Google => ProviderKind::Google,
ProviderArg::Antigravity => ProviderKind::Antigravity,
ProviderArg::Edenai => ProviderKind::Edenai,
}
}
}
@@ -348,6 +358,8 @@ lifecycle generation you observed.
Mcp(TuiPassthroughArgs),
/// Inspect feature flags.
Features(TuiPassthroughArgs),
/// Connect third-party harnesses through Codewhale (e.g. `integrations dsh status`).
Integrations(TuiPassthroughArgs),
/// Run a local Codewhale server.
#[command(after_help = "\
Forwarded serve options:
@@ -1839,6 +1851,14 @@ fn run() -> Result<()> {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("mcp", args))
}
Some(Commands::Integrations(args)) => {
// Integrations only need route *identity*. Do not recover or
// export a stored credential just to plan/launch a third-party
// harness: it resolves its own keys from its own environment.
let resolved_runtime =
resolve_runtime_for_diagnostic_dispatch(&store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("integrations", args))
}
Some(Commands::Features(args)) => {
let resolved_runtime = resolve_runtime_for_dispatch(&mut store, &runtime_overrides);
run_tui_in_process(&cli, &resolved_runtime, tui_args("features", args))
@@ -2482,6 +2502,10 @@ fn external_credential_target(
codewhale_config::ExternalCredentialSource::DshCli,
codewhale_config::default_dsh_credentials_path(),
),
ProviderKind::Antigravity => (
codewhale_config::ExternalCredentialSource::AgyCli,
codewhale_config::default_agy_credentials_path(),
),
ProviderKind::Moonshot => bail!(
"Kimi is API-key-only in Codewhale. Create a key at https://platform.kimi.ai/console/api-keys; Kimi CLI OAuth import is unsupported."
),
@@ -5437,6 +5461,20 @@ mod tests {
}
}
#[test]
fn ollama_cloud_provider_aliases_parse_as_builtin() {
for alias in ["ollama-cloud", "ollama_cloud"] {
assert_eq!(builtin_provider_arg(alias), Some(ProviderArg::OllamaCloud));
}
}
#[test]
fn antigravity_provider_aliases_parse_as_builtin() {
for alias in ["antigravity", "agy"] {
assert_eq!(builtin_provider_arg(alias), Some(ProviderArg::Antigravity));
}
}
#[test]
fn legacy_dual_wire_provider_flag_keeps_named_table_kind() {
// The CLI flag must resolve legacy spellings to the table-owning
@@ -7849,8 +7887,8 @@ mod tests {
.map(|provider| provider.kind())
.collect();
// Full registry keeps legacy dialect/plan kinds; ALL is the catalog surface.
assert_eq!(registry_kinds.len(), 45);
assert_eq!(ProviderKind::ALL.len(), 40);
assert_eq!(registry_kinds.len(), 47);
assert_eq!(ProviderKind::ALL.len(), 42);
for kind in ProviderKind::ALL {
assert!(
registry_kinds.contains(&kind),
+4 -1
View File
@@ -646,6 +646,8 @@ original install method:
cargo install codewhale-cli --locked
Homebrew:
brew upgrade codewhale
# existing Cellar/deepseek-tui installs can still:
brew upgrade deepseek-tui
Manual binary:
@@ -1941,7 +1943,7 @@ mod tests {
let brew =
managed_install_warning(InstallMethod::Homebrew).expect("brew is package-managed");
assert!(brew.contains("brew upgrade deepseek-tui"));
assert!(brew.contains("brew upgrade codewhale"));
assert!(managed_install_warning(InstallMethod::Cargo).is_some());
@@ -1963,6 +1965,7 @@ mod tests {
assert!(message.contains("cargo uninstall deepseek-tui 2>/dev/null || true"));
assert!(message.contains("cargo install codewhale-cli --locked"));
assert!(!message.contains("cargo install codewhale-tui --locked"));
assert!(message.contains("brew upgrade codewhale"));
assert!(message.contains("brew upgrade deepseek-tui"));
assert!(message.contains("https://github.com/Hmbown/CodeWhale/releases/latest"));
}
+27 -7
View File
@@ -371,12 +371,11 @@ fn the_openrouter_glm_sibling_resolves_to_its_own_gateway_wire_id() {
);
}
/// Adding a sibling must not move anyone's route. A Z.ai config that names no
/// model still has to land on `GLM-5.2`: the newer `glm-5.3` is catalogued but
/// deliberately not the default, and this is the surface where that would
/// silently change under a user.
/// A Z.ai config that names no model lands on the deliberate default,
/// `GLM-5.3`, with `provider default` provenance. This is the surface where a
/// default move would otherwise change silently under a user.
#[test]
fn adding_a_glm_sibling_leaves_the_zai_default_route_untouched() {
fn zai_default_route_resolves_to_glm_5_3_with_provider_default_provenance() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\n",
&[],
@@ -384,8 +383,8 @@ fn adding_a_glm_sibling_leaves_the_zai_default_route_untouched() {
assert_eq!(
report.get("resolved").map(String::as_str),
Some("GLM-5.2"),
"the Z.ai default must stay GLM-5.2 after a newer sibling is added: {report:?}"
Some("GLM-5.3"),
"the Z.ai default is GLM-5.3: {report:?}"
);
assert_eq!(
report.get("model_source").map(String::as_str),
@@ -394,6 +393,27 @@ fn adding_a_glm_sibling_leaves_the_zai_default_route_untouched() {
);
}
/// An explicit `GLM-5.2` selection keeps its own id after the default moved
/// to `GLM-5.3`: only the default changed, never a user's saved route.
#[test]
fn explicit_glm_5_2_selection_survives_the_default_move() {
let report = resolve_with_config(
"provider = \"zai\"\n\n[providers.zai]\napi_key = \"k\"\nmodel = \"GLM-5.2\"\n",
&[],
);
assert_eq!(
report.get("resolved").map(String::as_str),
Some("GLM-5.2"),
"an explicit GLM-5.2 route must not be upgraded: {report:?}"
);
assert_ne!(
report.get("model_source").map(String::as_str),
Some("provider default"),
"{report:?}"
);
}
fn codewhale_binary() -> PathBuf {
if let Some(path) = option_env!("CARGO_BIN_EXE_codewhale") {
return PathBuf::from(path);
+2 -2
View File
@@ -7,7 +7,7 @@
"honesty": "Pricing is intentionally OMITTED where a flat catalog row would be misleading: DeepSeek-native rows (priced via the time-aware DeepSeek table elsewhere, kept UnknownOrStale at the route layer), Grok 4.6 (rates double once a prompt reaches 200K tokens), aggregator-hosted DeepSeek rows (aggregator account terms, not DeepSeek Platform pricing), Xiaomi MiMo rows (published PAYG rates apply only to sk- pay-as-you-go keys; the catalog cannot distinguish that billing surface from credit/quota Token Plan keys, so MiMo stays unpriced), and Alibaba Model Studio Token/Coding Plan rows (upstream lists zero per-token cost because usage draws on plan quota, not per-token billing; a zero here would read as 'free'). Absent pricing surfaces as PricingSku::UnknownOrStale, never a fabricated zero.",
"default_rows": "Each provider's `default: true` wire id equals that provider's built-in DEFAULT_*_MODEL so RouteResolver::new() and the descriptor stay in agreement when offline.",
"curated": "qwen3.8-max (GA) is curated ahead of upstream Models.dev, which as of 2026-08-03 lists only qwen3.8-max-preview; facts verified against the owner's Token Plan console (2026-08-03): ~1M context, 128K output, image understanding, always-on reasoning. deepseek-v4-flash-0731 keeps the console/in-repo wire id for the row upstream serves as deepseek-v4-flash. Coding Plan rows for qwen3.8-max-preview, deepseek-v4-pro, deepseek-v4-flash-0731, and glm-5.2 are curated from the Token Plan upstream entries (upstream alibaba-coding-plan does not list them yet); the in-repo route layer already offers the same model set on both plans. Upstream provider ids alibaba-token-plan(-cn) / alibaba-coding-plan(-cn) were merged onto the CodeWhale provider ids (live refresh normalizes them via ProviderKind aliases; the -cn regional variants stay upstream-id browse rows until Codewhale ships China endpoints).",
"pending_release_metadata": "GLM-5.3 is live on the Z.ai Coding Plan (docs.z.ai/devpack/overview and docs.z.ai/devpack/latest-model, recorded 2026-08-13). First-party wire id is GLM-5.3; OpenRouter mirror is z-ai/glm-5.3. Capability/limit/dialect values still inherit from GLM-5.2 until Z.ai publishes distinct 5.3 numbers. Pricing stays absent: Coding Plan publishes credit multipliers, not a USD PAYG row we can stand behind. Z.ai may auto-route GLM-5.2/GLM-5.1 requests to GLM-5.3 on their side; Codewhale still sends the selected picker id. Do not send a [1m] suffix. Scope stays first-party Z.ai plus the OpenRouter mirror; add third-party gateway rows only against that gateway's own published roster.",
"pending_release_metadata": "GLM-5.3 is live on the Z.ai Coding Plan (docs.z.ai/devpack/overview and docs.z.ai/devpack/latest-model, recorded 2026-08-13) and is the default direct Z.ai model (DEFAULT_ZAI_MODEL); explicit GLM-5.2 selections keep their own id. First-party wire id is GLM-5.3; OpenRouter mirror is z-ai/glm-5.3. Capability/limit/dialect values still inherit from GLM-5.2 until Z.ai publishes distinct 5.3 numbers. Pricing stays absent: Coding Plan publishes credit multipliers, not a USD PAYG row we can stand behind. Z.ai may auto-route GLM-5.2/GLM-5.1 requests to GLM-5.3 on their side; Codewhale still sends the selected picker id. Do not send a [1m] suffix. Scope stays first-party Z.ai plus the OpenRouter mirror; add third-party gateway rows only against that gateway's own published roster.",
"coverage": "20 providers, 81 chat offerings (offline seed only)."
},
"models": {
@@ -74,7 +74,6 @@
"id": "GLM-5.2",
"name": "GLM-5.2",
"family": "glm",
"default": true,
"reasoning": true,
"reasoning_options": [{ "type": "effort", "values": ["high", "max"] }],
"tool_call": true,
@@ -86,6 +85,7 @@
"id": "GLM-5.3",
"name": "GLM-5.3",
"family": "glm",
"default": true,
"reasoning": true,
"reasoning_options": [{ "type": "effort", "values": ["high", "max"] }],
"tool_call": true,
+137
View File
@@ -0,0 +1,137 @@
//! The TUI's user-facing operating mode. Lives in codewhale-config so
//! settings, receipts, and other crates can name it without depending on
//! the TUI; the TUI adds the localized picker strings through an extension
//! trait.
/// Supported application modes for the TUI.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
Agent,
#[allow(dead_code)]
Auto,
/// Legacy compatibility alias; resolves to [`Self::Agent`] + bypass approvals.
Yolo,
Plan,
Operate,
}
impl AppMode {
/// Productive keyboard cycle: Plan -> Act -> Operate -> Plan.
///
/// `Auto` remains an internal variant while the real implementation is
/// redesigned; do not expose it through user-facing mode selection (#3733).
/// `Yolo` is kept for parse/back-compat only and is not in the Tab cycle.
/// Operate joins the visible cycle because ordinary messages can now
/// coordinate background workers without requiring a Workflow definition.
pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate];
#[must_use]
pub fn parse(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"agent" | "act" | "work" | "auto" | "1" => Some(Self::Agent),
"plan" | "2" => Some(Self::Plan),
"operate" | "operation" | "ops" | "3" => Some(Self::Operate),
// Invisible one-way permission shorthand only — never a visible mode.
"yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" => {
Some(Self::Yolo)
}
_ => None,
}
}
#[must_use]
pub fn from_setting(value: &str) -> Self {
// Unreleased Multitask never shipped; normalize leftover settings to Operate.
match value.trim().to_ascii_lowercase().as_str() {
"multitask" | "multi" | "5" => Self::Operate,
other => Self::parse(other).unwrap_or(Self::Agent),
}
}
#[must_use]
pub fn as_setting(self) -> &'static str {
match self {
Self::Agent => "agent",
Self::Auto => "agent",
// Write current permission vocabulary, not the legacy YOLO label.
Self::Yolo => "agent",
Self::Plan => "plan",
Self::Operate => "operate",
}
}
/// Short label used in the UI footer.
pub fn label(self) -> &'static str {
match self {
AppMode::Agent => "ACT",
AppMode::Auto => "ACT",
AppMode::Yolo => "ACT",
AppMode::Plan => "PLAN",
AppMode::Operate => "OPERATE",
}
}
#[must_use]
pub fn display_name(self) -> &'static str {
match self {
AppMode::Agent => "Act",
AppMode::Auto => "Act",
AppMode::Yolo => "Act",
AppMode::Plan => "Plan",
AppMode::Operate => "Operate",
}
}
#[must_use]
pub fn number(self) -> char {
match self {
AppMode::Agent | AppMode::Auto | AppMode::Yolo => '1',
AppMode::Plan => '2',
AppMode::Operate => '3',
}
}
#[must_use]
pub fn uses_agent_baseline(self) -> bool {
matches!(self, Self::Agent | Self::Auto | Self::Operate)
}
/// Operate gets a higher parallel launch floor so background fan-out is
/// not throttled to a single slot when config is low.
#[must_use]
pub fn mode_delegation_launch_floor(self) -> usize {
match self {
Self::Operate => 4,
_ => 1,
}
}
#[allow(dead_code)]
/// Description shown in help or onboarding text.
pub fn description(self) -> &'static str {
match self {
AppMode::Agent | AppMode::Auto => {
"Act mode - direct work in the current session with tools"
}
AppMode::Yolo => "Act mode with Full Access (legacy compatibility setting)",
AppMode::Plan => "Plan mode - research and design before implementing",
AppMode::Operate => "Operate mode - send tasks while Fleet workers run in parallel",
}
}
#[must_use]
pub fn next(self) -> Self {
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
return Self::Agent;
};
Self::CYCLE[(index + 1) % Self::CYCLE.len()]
}
#[must_use]
pub fn previous(self) -> Self {
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
return Self::Agent;
};
Self::CYCLE[(index + Self::CYCLE.len() - 1) % Self::CYCLE.len()]
}
}
+8 -5
View File
@@ -621,10 +621,13 @@ fn bundled_asset_yields_real_chat_offerings_for_key_models() {
// proving real facts flow rather than `RouteLimits::default()` (unknown).
let glm = find(&rows, "zai", "GLM-5.2");
assert_eq!(glm.limit.as_ref().and_then(|l| l.context), Some(1_000_000));
assert!(glm.default_for_provider);
assert!(
!glm.default_for_provider,
"GLM-5.2 is no longer the Z.ai default"
);
// GLM-5.3 is a live peer whose limits still inherit from glm-5.2 until
// Z.ai publishes distinct 5.3 numbers. Adding it must not move the default.
// GLM-5.3 is the Z.ai default (matching DEFAULT_ZAI_MODEL); its limits
// still inherit from glm-5.2 until Z.ai publishes distinct 5.3 numbers.
let glm53 = find(&rows, "zai", "GLM-5.3");
assert_eq!(
glm53.limit.as_ref().and_then(|l| l.context),
@@ -635,8 +638,8 @@ fn bundled_asset_yields_real_chat_offerings_for_key_models() {
glm.limit.as_ref().and_then(|l| l.output)
);
assert!(
!glm53.default_for_provider,
"GLM-5.3 must not become the Z.ai default"
glm53.default_for_provider,
"GLM-5.3 must be the Z.ai default"
);
let kimi_k27 = find(&rows, "moonshot", "kimi-k2.7-code");
+100 -8
View File
@@ -1,3 +1,4 @@
pub mod app_mode;
pub mod auth_source;
pub mod auto_model;
pub mod catalog;
@@ -57,6 +58,7 @@ use std::path::{Component, Path, PathBuf};
use std::sync::OnceLock;
use anyhow::{Context, Result, bail};
pub use app_mode::AppMode;
pub use auth_source::{AuthSourceKind, ProviderAuthSourceToml};
pub use codewhale_execpolicy::ToolAskRule;
use codewhale_execpolicy::{ExecPolicyEngine, PermissionAction, Ruleset};
@@ -274,6 +276,12 @@ pub struct ProvidersToml {
pub vllm: ProviderConfigToml,
#[serde(default, skip_serializing_if = "ProviderConfigToml::is_empty")]
pub ollama: ProviderConfigToml,
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "ollama-cloud"
)]
pub ollama_cloud: ProviderConfigToml,
#[serde(default, skip_serializing_if = "ProviderConfigToml::is_empty")]
pub huggingface: ProviderConfigToml,
#[serde(default, skip_serializing_if = "ProviderConfigToml::is_empty")]
@@ -441,6 +449,14 @@ pub struct ProvidersToml {
alias = "tokenhub"
)]
pub telecomjs: ProviderConfigToml,
/// Eden AI — OpenAI-compatible AI gateway (aggregator).
#[serde(
default,
skip_serializing_if = "ProviderConfigToml::is_empty",
alias = "eden-ai",
alias = "eden_ai"
)]
pub edenai: ProviderConfigToml,
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible endpoint).
#[serde(
default,
@@ -642,6 +658,7 @@ impl ProvidersToml {
ProviderKind::Sglang => &self.sglang,
ProviderKind::Vllm => &self.vllm,
ProviderKind::Ollama => &self.ollama,
ProviderKind::OllamaCloud => &self.ollama_cloud,
ProviderKind::Huggingface => &self.huggingface,
ProviderKind::Together => &self.together,
ProviderKind::Qianfan => &self.qianfan,
@@ -663,6 +680,7 @@ impl ProvidersToml {
ProviderKind::Google => &self.google,
ProviderKind::Antigravity => &self.antigravity,
ProviderKind::Telecomjs => &self.telecomjs,
ProviderKind::Edenai => &self.edenai,
ProviderKind::ModelstudioTokenPlan => &self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => &self.modelstudio_token_plan_anthropic,
ProviderKind::ModelstudioCodingPlan => &self.modelstudio_coding_plan,
@@ -692,6 +710,7 @@ impl ProvidersToml {
ProviderKind::Sglang => &mut self.sglang,
ProviderKind::Vllm => &mut self.vllm,
ProviderKind::Ollama => &mut self.ollama,
ProviderKind::OllamaCloud => &mut self.ollama_cloud,
ProviderKind::Huggingface => &mut self.huggingface,
ProviderKind::Together => &mut self.together,
ProviderKind::Qianfan => &mut self.qianfan,
@@ -713,6 +732,7 @@ impl ProvidersToml {
ProviderKind::Google => &mut self.google,
ProviderKind::Antigravity => &mut self.antigravity,
ProviderKind::Telecomjs => &mut self.telecomjs,
ProviderKind::Edenai => &mut self.edenai,
ProviderKind::ModelstudioTokenPlan => &mut self.modelstudio_token_plan,
ProviderKind::ModelstudioTokenPlanAnthropic => {
&mut self.modelstudio_token_plan_anthropic
@@ -3214,6 +3234,7 @@ impl ConfigToml {
ProviderKind::Sglang => DEFAULT_SGLANG_BASE_URL.to_string(),
ProviderKind::Vllm => DEFAULT_VLLM_BASE_URL.to_string(),
ProviderKind::Ollama => DEFAULT_OLLAMA_BASE_URL.to_string(),
ProviderKind::OllamaCloud => DEFAULT_OLLAMA_CLOUD_BASE_URL.to_string(),
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_BASE_URL.to_string(),
ProviderKind::Together => DEFAULT_TOGETHER_BASE_URL.to_string(),
ProviderKind::Qianfan => DEFAULT_QIANFAN_BASE_URL.to_string(),
@@ -3235,6 +3256,7 @@ impl ConfigToml {
ProviderKind::Google => DEFAULT_GOOGLE_BASE_URL.to_string(),
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL.to_string(),
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL.to_string(),
ProviderKind::Edenai => DEFAULT_EDENAI_BASE_URL.to_string(),
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
@@ -3247,6 +3269,16 @@ impl ConfigToml {
ProviderKind::Custom => provider.provider().default_base_url().to_string(),
})
};
// Released builds represented Ollama Cloud as the local `ollama`
// identity plus one exact hosted base URL. Upgrade only that tuple in
// memory: the parsed config and secret store are never rewritten, and
// neighboring/custom routes retain the local/custom identity.
let legacy_ollama_cloud = provider::migrates_legacy_ollama_cloud_route(provider, &base_url);
let provider = if legacy_ollama_cloud {
ProviderKind::OllamaCloud
} else {
provider
};
// `auth_mode = "none"` is an endpoint contract, so it suppresses every
// credential source (including explicit CLI/config values). Otherwise
// CLI and route-local config win outright. Ambient provider credentials
@@ -3284,7 +3316,7 @@ impl ConfigToml {
None => (None, None),
}
} else {
match secrets.resolve_with_source(provider.secret_store_slot()) {
match stored_api_key_for_provider(secrets, provider, legacy_ollama_cloud) {
Some((value, source)) => {
let source = match source {
SecretSource::Keyring => RuntimeApiKeySource::Keyring,
@@ -3753,10 +3785,12 @@ fn provider_passes_model_through(provider: ProviderKind) -> bool {
| ProviderKind::Qianfan
| ProviderKind::Openmodel
| ProviderKind::Ollama
| ProviderKind::OllamaCloud
| ProviderKind::Huggingface
| ProviderKind::Meta
| ProviderKind::Xai
| ProviderKind::Telecomjs
| ProviderKind::Edenai
| ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
@@ -3897,6 +3931,7 @@ fn normalize_model_for_provider(provider: ProviderKind, model: &str) -> String {
| ProviderKind::MinimaxAnthropic
| ProviderKind::Qianfan
| ProviderKind::Ollama
| ProviderKind::OllamaCloud
| ProviderKind::Meta
| ProviderKind::Xai
) {
@@ -4171,9 +4206,10 @@ fn canonical_zai_model_id(model: &str) -> Option<&'static str> {
let normalized = normalized.replace(['_', ' '], "-");
match normalized.as_str() {
"glm-5.1" | "glm-5-1" | "zai-glm-5.1" | "zai-glm-5-1" => Some(ZAI_GLM_5_1_MODEL),
"glm-5.2" | "glm-5-2" | "zai-glm-5.2" | "zai-glm-5-2" => Some(DEFAULT_ZAI_MODEL),
// GLM-5.3 resolves to its own id, never to DEFAULT_ZAI_MODEL: adding a
// model must not silently re-point a route at the default.
// Every alias resolves to its own id, never through DEFAULT_ZAI_MODEL:
// moving the default (now GLM-5.3) must not silently re-point an
// explicit GLM-5.2 route.
"glm-5.2" | "glm-5-2" | "zai-glm-5.2" | "zai-glm-5-2" => Some(ZAI_GLM_5_2_MODEL),
"glm-5.3" | "glm-5-3" | "zai-glm-5.3" | "zai-glm-5-3" => Some(ZAI_GLM_5_3_MODEL),
"glm-5-turbo" | "glm-5turbo" | "zai-glm-5-turbo" => Some(ZAI_GLM_5_TURBO_MODEL),
_ => None,
@@ -4308,6 +4344,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Sglang => DEFAULT_SGLANG_MODEL,
ProviderKind::Vllm => DEFAULT_VLLM_MODEL,
ProviderKind::Ollama => DEFAULT_OLLAMA_MODEL,
ProviderKind::OllamaCloud => DEFAULT_OLLAMA_CLOUD_MODEL,
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_MODEL,
ProviderKind::Together => DEFAULT_TOGETHER_MODEL,
ProviderKind::Qianfan => DEFAULT_QIANFAN_MODEL,
@@ -4328,6 +4365,7 @@ fn default_model_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Google => DEFAULT_GOOGLE_MODEL,
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_MODEL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ProviderKind::Edenai => DEFAULT_EDENAI_MODEL,
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
@@ -4358,6 +4396,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Sglang => DEFAULT_SGLANG_BASE_URL,
ProviderKind::Vllm => DEFAULT_VLLM_BASE_URL,
ProviderKind::Ollama => DEFAULT_OLLAMA_BASE_URL,
ProviderKind::OllamaCloud => DEFAULT_OLLAMA_CLOUD_BASE_URL,
ProviderKind::Huggingface => DEFAULT_HUGGINGFACE_BASE_URL,
ProviderKind::Together => DEFAULT_TOGETHER_BASE_URL,
ProviderKind::Qianfan => DEFAULT_QIANFAN_BASE_URL,
@@ -4379,6 +4418,7 @@ fn default_base_url_for_provider(provider: ProviderKind) -> &'static str {
ProviderKind::Google => DEFAULT_GOOGLE_BASE_URL,
ProviderKind::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL,
ProviderKind::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ProviderKind::Edenai => DEFAULT_EDENAI_BASE_URL,
ProviderKind::ModelstudioTokenPlan => DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL,
ProviderKind::ModelstudioTokenPlanAnthropic => MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
ProviderKind::ModelstudioCodingPlan => DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
@@ -4659,6 +4699,15 @@ pub fn provider_base_url_is_official(provider: ProviderKind, base_url: &str) ->
xiaomi_mimo_base_url_uses_token_plan(base_url)
|| xiaomi_mimo_base_url_is_pay_as_you_go(base_url)
}
ProviderKind::Ollama => {
normalized == DEFAULT_OLLAMA_BASE_URL
|| provider::is_exact_ollama_cloud_route(provider, base_url)
}
ProviderKind::OllamaCloud => provider::is_exact_ollama_cloud_route(provider, base_url),
ProviderKind::Edenai => matches!(
normalized.as_str(),
"https://api.edenai.run/v3" | "https://api.eu.edenai.run/v3"
),
// Custom routes have no Codewhale-owned official endpoint. The
// descriptor URL is a schema placeholder, never a credential scope.
ProviderKind::Custom => false,
@@ -4702,10 +4751,33 @@ fn should_skip_secret_store_for_provider(
return false;
}
matches!(
provider,
ProviderKind::Sglang | ProviderKind::Vllm | ProviderKind::Ollama
) || base_url_uses_local_host(base_url)
matches!(provider, ProviderKind::Sglang | ProviderKind::Vllm)
|| (provider == ProviderKind::Ollama
&& !provider::is_exact_ollama_cloud_route(provider, base_url))
|| base_url_uses_local_host(base_url)
}
/// Read the durable provider slot without allowing environment fallback to
/// jump ahead of the bounded legacy slot. The old `ollama` slot is consulted
/// only for the exact route tuple migrated above; selecting `ollama-cloud`
/// directly never consumes a local provider credential.
fn stored_api_key_for_provider(
secrets: &Secrets,
provider: ProviderKind,
legacy_ollama_cloud: bool,
) -> Option<(String, SecretSource)> {
let mut slots = vec![provider.secret_store_slot()];
if provider == ProviderKind::OllamaCloud && legacy_ollama_cloud {
slots.push(ProviderKind::Ollama.secret_store_slot());
}
slots.into_iter().find_map(|slot| {
secrets
.get(slot)
.ok()
.flatten()
.filter(|value| !value.trim().is_empty())
.map(|value| (value, SecretSource::Keyring))
})
}
fn env_api_key_for_provider(provider: ProviderKind) -> Option<String> {
@@ -6602,6 +6674,8 @@ struct EnvRuntimeOverrides {
sglang_base_url: Option<String>,
vllm_base_url: Option<String>,
ollama_base_url: Option<String>,
ollama_cloud_base_url: Option<String>,
ollama_cloud_model: Option<String>,
huggingface_base_url: Option<String>,
huggingface_model: Option<String>,
together_base_url: Option<String>,
@@ -6643,6 +6717,8 @@ struct EnvRuntimeOverrides {
antigravity_model: Option<String>,
telecomjs_base_url: Option<String>,
telecomjs_model: Option<String>,
edenai_base_url: Option<String>,
edenai_model: Option<String>,
modelstudio_token_plan_base_url: Option<String>,
modelstudio_token_plan_model: Option<String>,
modelstudio_coding_plan_base_url: Option<String>,
@@ -6817,6 +6893,12 @@ impl EnvRuntimeOverrides {
ollama_base_url: std::env::var("OLLAMA_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
ollama_cloud_base_url: std::env::var("OLLAMA_CLOUD_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
ollama_cloud_model: std::env::var("OLLAMA_CLOUD_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
huggingface_base_url: std::env::var("HUGGINGFACE_BASE_URL")
.or_else(|_| std::env::var("HF_BASE_URL"))
.ok()
@@ -6985,6 +7067,12 @@ impl EnvRuntimeOverrides {
telecomjs_model: std::env::var("TELECOMJS_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
edenai_base_url: std::env::var("EDENAI_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
edenai_model: std::env::var("EDENAI_MODEL")
.ok()
.filter(|v| !v.trim().is_empty()),
modelstudio_token_plan_base_url: std::env::var("MODELSTUDIO_TOKEN_PLAN_BASE_URL")
.ok()
.filter(|v| !v.trim().is_empty()),
@@ -7063,6 +7151,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Sglang => self.sglang_base_url.clone(),
ProviderKind::Vllm => self.vllm_base_url.clone(),
ProviderKind::Ollama => self.ollama_base_url.clone(),
ProviderKind::OllamaCloud => self.ollama_cloud_base_url.clone(),
ProviderKind::Huggingface => self.huggingface_base_url.clone(),
ProviderKind::Together => self.together_base_url.clone(),
ProviderKind::Qianfan => self.qianfan_base_url.clone(),
@@ -7084,6 +7173,7 @@ impl EnvRuntimeOverrides {
ProviderKind::Google => self.google_base_url.clone(),
ProviderKind::Antigravity => self.antigravity_base_url.clone(),
ProviderKind::Telecomjs => self.telecomjs_base_url.clone(),
ProviderKind::Edenai => self.edenai_base_url.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_base_url.clone()
}
@@ -7130,12 +7220,14 @@ impl EnvRuntimeOverrides {
ProviderKind::Google => self.google_model.clone(),
ProviderKind::Antigravity => self.antigravity_model.clone(),
ProviderKind::Telecomjs => self.telecomjs_model.clone(),
ProviderKind::Edenai => self.edenai_model.clone(),
ProviderKind::ModelstudioTokenPlan | ProviderKind::ModelstudioTokenPlanAnthropic => {
self.modelstudio_token_plan_model.clone()
}
ProviderKind::ModelstudioCodingPlan | ProviderKind::ModelstudioCodingPlanAnthropic => {
self.modelstudio_coding_plan_model.clone()
}
ProviderKind::OllamaCloud => self.ollama_cloud_model.clone(),
_ => None,
}?;
+143 -20
View File
@@ -9,30 +9,31 @@ use super::{
DEFAULT_ARCEE_MODEL, DEFAULT_ATLASCLOUD_BASE_URL, DEFAULT_ATLASCLOUD_MODEL,
DEFAULT_DEEPINFRA_BASE_URL, DEFAULT_DEEPINFRA_MODEL, DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL,
DEFAULT_DEEPSEEK_ANTHROPIC_MODEL, DEFAULT_DEEPSEEK_BASE_URL, DEFAULT_DEEPSEEK_MODEL,
DEFAULT_FIREWORKS_BASE_URL, DEFAULT_FIREWORKS_MODEL, DEFAULT_GOOGLE_BASE_URL,
DEFAULT_GOOGLE_MODEL, DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL,
DEFAULT_LONGCAT_BASE_URL, DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
DEFAULT_EDENAI_BASE_URL, DEFAULT_EDENAI_MODEL, DEFAULT_FIREWORKS_BASE_URL,
DEFAULT_FIREWORKS_MODEL, DEFAULT_GOOGLE_BASE_URL, DEFAULT_GOOGLE_MODEL,
DEFAULT_HUGGINGFACE_BASE_URL, DEFAULT_HUGGINGFACE_MODEL, DEFAULT_LONGCAT_BASE_URL,
DEFAULT_LONGCAT_MODEL, DEFAULT_META_BASE_URL, DEFAULT_META_MODEL,
DEFAULT_MINIMAX_ANTHROPIC_BASE_URL, DEFAULT_MINIMAX_BASE_URL, DEFAULT_MINIMAX_MODEL,
DEFAULT_MISTRAL_BASE_URL, DEFAULT_MISTRAL_MODEL, DEFAULT_MODELSTUDIO_CODING_PLAN_BASE_URL,
DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL, DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL,
DEFAULT_MOONSHOT_BASE_URL, DEFAULT_MOONSHOT_MODEL, DEFAULT_NOVITA_BASE_URL,
DEFAULT_NOVITA_MODEL, DEFAULT_NVIDIA_NIM_BASE_URL, DEFAULT_NVIDIA_NIM_MODEL,
DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL,
DEFAULT_OPENAI_CODEX_BASE_URL, DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL,
DEFAULT_OPENCODE_GO_BASE_URL, DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL,
DEFAULT_OPENCODE_ZEN_MODEL, DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL,
DEFAULT_OPENROUTER_BASE_URL, DEFAULT_OPENROUTER_MODEL, DEFAULT_ORCAROUTER_BASE_URL,
DEFAULT_ORCAROUTER_MODEL, DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL,
DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL, DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL,
DEFAULT_SILICONFLOW_BASE_URL, DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL,
DEFAULT_STEPFUN_BASE_URL, DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL,
DEFAULT_TELECOMJS_MODEL, DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL,
DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL, DEFAULT_VOLCENGINE_BASE_URL,
DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL, DEFAULT_WANJIE_ARK_MODEL,
DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL, DEFAULT_XIAOMI_MIMO_BASE_URL,
DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL, DEFAULT_ZAI_MODEL,
MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL, MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL,
ProviderKind,
DEFAULT_OLLAMA_BASE_URL, DEFAULT_OLLAMA_CLOUD_BASE_URL, DEFAULT_OLLAMA_CLOUD_MODEL,
DEFAULT_OLLAMA_MODEL, DEFAULT_OPENAI_BASE_URL, DEFAULT_OPENAI_CODEX_BASE_URL,
DEFAULT_OPENAI_CODEX_MODEL, DEFAULT_OPENAI_MODEL, DEFAULT_OPENCODE_GO_BASE_URL,
DEFAULT_OPENCODE_GO_MODEL, DEFAULT_OPENCODE_ZEN_BASE_URL, DEFAULT_OPENCODE_ZEN_MODEL,
DEFAULT_OPENMODEL_BASE_URL, DEFAULT_OPENMODEL_MODEL, DEFAULT_OPENROUTER_BASE_URL,
DEFAULT_OPENROUTER_MODEL, DEFAULT_ORCAROUTER_BASE_URL, DEFAULT_ORCAROUTER_MODEL,
DEFAULT_QIANFAN_BASE_URL, DEFAULT_QIANFAN_MODEL, DEFAULT_SAKANA_BASE_URL, DEFAULT_SAKANA_MODEL,
DEFAULT_SGLANG_BASE_URL, DEFAULT_SGLANG_MODEL, DEFAULT_SILICONFLOW_BASE_URL,
DEFAULT_SILICONFLOW_CN_BASE_URL, DEFAULT_SILICONFLOW_MODEL, DEFAULT_STEPFUN_BASE_URL,
DEFAULT_STEPFUN_MODEL, DEFAULT_TELECOMJS_BASE_URL, DEFAULT_TELECOMJS_MODEL,
DEFAULT_TOGETHER_BASE_URL, DEFAULT_TOGETHER_MODEL, DEFAULT_VLLM_BASE_URL, DEFAULT_VLLM_MODEL,
DEFAULT_VOLCENGINE_BASE_URL, DEFAULT_VOLCENGINE_MODEL, DEFAULT_WANJIE_ARK_BASE_URL,
DEFAULT_WANJIE_ARK_MODEL, DEFAULT_XAI_BASE_URL, DEFAULT_XAI_MODEL,
DEFAULT_XIAOMI_MIMO_BASE_URL, DEFAULT_XIAOMI_MIMO_MODEL, DEFAULT_ZAI_BASE_URL,
DEFAULT_ZAI_MODEL, MODELSTUDIO_CODING_PLAN_ANTHROPIC_BASE_URL,
MODELSTUDIO_TOKEN_PLAN_ANTHROPIC_BASE_URL, ProviderKind,
};
/// Wire protocol spoken by a provider.
@@ -139,6 +140,12 @@ pub struct CredentialHelp {
/// is never described as a generic Moonshot route.
pub const KIMI_CODE_MEMBERSHIP_PLAN_CONSOLE_URL: &str = "https://www.kimi.com/code/console";
/// Ollama's account page for creating API keys used by the hosted API.
pub const OLLAMA_CLOUD_API_KEY_URL: &str = "https://ollama.com/settings/keys";
/// Ollama Cloud's exact OpenAI-compatible API base URL.
pub const OLLAMA_CLOUD_BASE_URL: &str = DEFAULT_OLLAMA_CLOUD_BASE_URL;
/// Static metadata for a built-in model provider.
pub trait Provider: Send + Sync {
/// Provider enum variant represented by this entry.
@@ -302,6 +309,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
docs_url: Some("https://docs.ollama.com/api"),
guidance: "Local Ollama is keyless by default; configure a key only if your server requires one.",
},
ProviderKind::OllamaCloud => CredentialHelp {
acquisition: ApiKey,
credential_url: Some(OLLAMA_CLOUD_API_KEY_URL),
docs_url: Some("https://docs.ollama.com/api/authentication"),
guidance: "Ollama Cloud requires an API key. Save it for the ollama-cloud provider, set OLLAMA_CLOUD_API_KEY for Pi compatibility, or set Ollama's official OLLAMA_API_KEY.",
},
ProviderKind::Huggingface => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://huggingface.co/settings/tokens"),
@@ -412,6 +425,12 @@ pub const fn credential_help(kind: ProviderKind) -> CredentialHelp {
docs_url: None,
guidance: "Create a TelecomJS TokenHub API key, then use the provider's live model catalog to discover the models available to that key.",
},
ProviderKind::Edenai => CredentialHelp {
acquisition: ApiKey,
credential_url: Some("https://app.edenai.run/settings/api-keys"),
docs_url: Some("https://www.edenai.co/docs"),
guidance: "Create an Eden AI API key from the Eden AI dashboard, then select models by their provider/model namespaced id.",
},
ProviderKind::ModelstudioTokenPlan
| ProviderKind::ModelstudioTokenPlanAnthropic
| ProviderKind::ModelstudioCodingPlan
@@ -475,6 +494,28 @@ pub fn is_exact_kimi_code_route(kind: ProviderKind, base_url: &str) -> bool {
is_exact_https_route(base_url, "api.kimi.com", "coding/v1")
}
/// Whether a configured Ollama route is exactly the hosted OpenAI-compatible
/// endpoint.
///
/// Local Ollama remains keyless. Neighboring paths, HTTP downgrades, and
/// lookalike hosts remain custom routes so they cannot inherit an Ollama Cloud
/// credential or durable secret-store slot.
#[must_use]
pub fn is_exact_ollama_cloud_route(kind: ProviderKind, base_url: &str) -> bool {
matches!(kind, ProviderKind::Ollama | ProviderKind::OllamaCloud)
&& is_exact_https_route(base_url, "ollama.com", "v1")
}
/// In-memory compatibility classifier for the released route-sensitive shape.
///
/// Only the old `ollama` identity at the exact hosted endpoint migrates. This
/// deliberately rejects neighboring paths, HTTP downgrades, and lookalike
/// hosts so no local/custom route can consume Ollama Cloud credentials.
#[must_use]
pub fn migrates_legacy_ollama_cloud_route(kind: ProviderKind, base_url: &str) -> bool {
kind == ProviderKind::Ollama && is_exact_ollama_cloud_route(kind, base_url)
}
/// Whether a configured route is exactly Moonshot's direct API endpoint.
///
/// Direct K3 owns a different reasoning-control dialect from the Kimi Code
@@ -540,6 +581,15 @@ pub fn is_exact_minimax_anthropic_route(kind: ProviderKind, base_url: &str) -> b
/// endpoint. It performs no discovery, credential lookup, or network I/O.
#[must_use]
pub fn credential_help_for_route(kind: ProviderKind, base_url: &str) -> CredentialHelp {
if is_exact_ollama_cloud_route(kind, base_url) {
return CredentialHelp {
acquisition: CredentialAcquisition::ApiKey,
credential_url: Some(OLLAMA_CLOUD_API_KEY_URL),
docs_url: Some("https://docs.ollama.com/api/authentication"),
guidance: "Ollama Cloud requires an API key. Create one in Ollama account settings, then save it for the ollama-cloud provider, set OLLAMA_CLOUD_API_KEY for Pi compatibility, or set Ollama's official OLLAMA_API_KEY.",
};
}
if is_exact_kimi_code_route(kind, base_url) {
return CredentialHelp {
acquisition: CredentialAcquisition::ApiKey,
@@ -916,6 +966,17 @@ provider!(
"ollama",
aliases: ["ollama-local"]
);
provider!(
OllamaCloud,
OllamaCloud,
"ollama-cloud",
"Ollama Cloud",
DEFAULT_OLLAMA_CLOUD_BASE_URL,
DEFAULT_OLLAMA_CLOUD_MODEL,
["OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY"],
"ollama_cloud",
aliases: ["ollama_cloud"]
);
provider!(
Huggingface,
Huggingface,
@@ -1326,6 +1387,17 @@ provider!(
"telecomjs",
aliases: ["telecom-js", "telecom_js", "telecomjs-cn", "tokenhub"]
);
provider!(
Edenai,
Edenai,
"edenai",
"Eden AI",
DEFAULT_EDENAI_BASE_URL,
DEFAULT_EDENAI_MODEL,
["EDENAI_API_KEY"],
"edenai",
aliases: ["eden-ai", "eden_ai"]
);
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
///
@@ -1590,6 +1662,7 @@ static MOONSHOT: Moonshot = Moonshot;
static SGLANG: Sglang = Sglang;
static VLLM: Vllm = Vllm;
static OLLAMA: Ollama = Ollama;
static OLLAMA_CLOUD: OllamaCloud = OllamaCloud;
static HUGGINGFACE: Huggingface = Huggingface;
static TOGETHER: Together = Together;
static QIANFAN: Qianfan = Qianfan;
@@ -1610,6 +1683,7 @@ static XAI: Xai = Xai;
static MISTRAL: Mistral = Mistral;
static ANTIGRAVITY: Antigravity = Antigravity;
static TELECOMJS: Telecomjs = Telecomjs;
static EDENAI: Edenai = Edenai;
static MODELSTUDIO_TOKEN_PLAN: ModelstudioTokenPlan = ModelstudioTokenPlan;
static MODELSTUDIO_TOKEN_PLAN_ANTHROPIC: ModelstudioTokenPlanAnthropic =
ModelstudioTokenPlanAnthropic;
@@ -1618,7 +1692,7 @@ static MODELSTUDIO_CODING_PLAN_ANTHROPIC: ModelstudioCodingPlanAnthropic =
ModelstudioCodingPlanAnthropic;
static CUSTOM: Custom = Custom;
static PROVIDER_REGISTRY: [&dyn Provider; 45] = [
static PROVIDER_REGISTRY: [&dyn Provider; 47] = [
&DEEPSEEK,
&DEEPSEEK_ANTHROPIC,
&NVIDIA_NIM,
@@ -1638,6 +1712,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 45] = [
&SGLANG,
&VLLM,
&OLLAMA,
&OLLAMA_CLOUD,
&HUGGINGFACE,
&TOGETHER,
&QIANFAN,
@@ -1657,6 +1732,7 @@ static PROVIDER_REGISTRY: [&dyn Provider; 45] = [
&XAI,
&MISTRAL,
&TELECOMJS,
&EDENAI,
&MODELSTUDIO_TOKEN_PLAN,
&MODELSTUDIO_TOKEN_PLAN_ANTHROPIC,
&MODELSTUDIO_CODING_PLAN,
@@ -1826,6 +1902,53 @@ mod tests {
}
}
#[test]
fn ollama_cloud_route_is_exact_and_requires_its_own_key() {
for base_url in [
OLLAMA_CLOUD_BASE_URL,
"https://ollama.com/v1/",
" HTTPS://OLLAMA.COM/v1/ ",
] {
for provider in [ProviderKind::Ollama, ProviderKind::OllamaCloud] {
assert!(is_exact_ollama_cloud_route(provider, base_url));
let help = credential_help_for_route(provider, base_url);
assert_eq!(help.acquisition, CredentialAcquisition::ApiKey);
assert_eq!(help.credential_url, Some(OLLAMA_CLOUD_API_KEY_URL));
assert_eq!(
help.docs_url,
Some("https://docs.ollama.com/api/authentication")
);
assert!(help.guidance.contains("OLLAMA_CLOUD_API_KEY"));
assert!(help.guidance.contains("OLLAMA_API_KEY"));
}
}
for base_url in [
"http://ollama.com/v1",
"https://ollama.com",
"https://ollama.com/api",
"https://ollama.com/v1/preview",
"https://ollama.com.evil.example/v1",
"https://api.ollama.com/v1",
"https://ollama.com/v1?tenant=other",
] {
assert!(!is_exact_ollama_cloud_route(ProviderKind::Ollama, base_url));
assert!(!is_exact_ollama_cloud_route(
ProviderKind::OllamaCloud,
base_url
));
}
assert!(!is_exact_ollama_cloud_route(
ProviderKind::Openai,
OLLAMA_CLOUD_BASE_URL
));
let local = credential_help_for_route(ProviderKind::Ollama, DEFAULT_OLLAMA_BASE_URL);
assert_eq!(local.acquisition, CredentialAcquisition::LocalOptional);
assert_eq!(local.credential_url, None);
assert!(local.guidance.contains("keyless by default"));
}
#[test]
fn direct_moonshot_route_matching_is_exact() {
assert!(is_exact_moonshot_platform_route(
+12 -4
View File
@@ -113,13 +113,18 @@ pub(crate) const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash
pub(crate) const DEFAULT_VLLM_BASE_URL: &str = "http://localhost:8000/v1";
pub(crate) const DEFAULT_OLLAMA_MODEL: &str = "deepseek-v4-flash";
pub(crate) const DEFAULT_OLLAMA_BASE_URL: &str = "http://localhost:11434/v1";
pub(crate) const DEFAULT_OLLAMA_CLOUD_MODEL: &str = "gpt-oss:120b";
pub(crate) const DEFAULT_OLLAMA_CLOUD_BASE_URL: &str = "https://ollama.com/v1";
// Z.ai (GLM Coding Plan) defaults
pub(crate) const DEFAULT_ZAI_MODEL: &str = "GLM-5.2";
// GLM-5.3 is a live peer of the default, never the default. Capability/limit
// Z.ai (GLM Coding Plan) defaults. GLM-5.3 is live on the Z.ai Coding Plan
// (2026-08-13) and is the default for new Z.ai routes. Capability/limit
// metadata still inherits from glm-5.2 until Z.ai publishes distinct 5.3
// numbers. See models_dev.bundled.json `_meta.pending_release_metadata`.
// numbers; no USD price is claimed. See models_dev.bundled.json
// `_meta.pending_release_metadata`. Explicit GLM-5.2 selections keep their
// own id: only the default moved.
pub(crate) const DEFAULT_ZAI_MODEL: &str = ZAI_GLM_5_3_MODEL;
pub(crate) const ZAI_GLM_5_3_MODEL: &str = "GLM-5.3";
pub(crate) const ZAI_GLM_5_2_MODEL: &str = "GLM-5.2";
pub(crate) const ZAI_GLM_5_1_MODEL: &str = "GLM-5.1";
pub(crate) const ZAI_GLM_5_TURBO_MODEL: &str = "GLM-5-Turbo";
pub(crate) const DEFAULT_ZAI_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4";
@@ -178,6 +183,9 @@ pub(crate) const DEFAULT_MISTRAL_BASE_URL: &str = "https://api.mistral.ai/v1";
// TelecomJS (Jiangsu Telecom TokenHub) defaults
pub(crate) const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub(crate) const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
// Eden AI (OpenAI-compatible AI gateway) defaults
pub(crate) const DEFAULT_EDENAI_MODEL: &str = "deepseek/deepseek-v4-pro";
pub(crate) const DEFAULT_EDENAI_BASE_URL: &str = "https://api.edenai.run/v3";
// Alibaba Cloud Model Studio (DashScope) defaults
// Token Plan (Personal / Team): shared endpoint, OpenAI + Anthropic dialects
pub(crate) const DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL: &str = "qwen3.8-max";
+11 -1
View File
@@ -61,6 +61,8 @@ pub enum ProviderKind {
Sglang,
Vllm,
Ollama,
#[serde(alias = "ollama_cloud")]
OllamaCloud,
#[serde(alias = "hugging-face", alias = "hugging_face", alias = "hf")]
Huggingface,
#[serde(alias = "together-ai", alias = "together_ai", alias = "togetherai")]
@@ -207,6 +209,12 @@ pub enum ProviderKind {
alias = "aistudio"
)]
Google,
/// Eden AI — OpenAI-compatible AI gateway (aggregator).
///
/// Serves a broad catalog of upstream models under `provider/model`
/// namespaced wire ids over the OpenAI Chat Completions protocol.
#[serde(alias = "eden-ai", alias = "eden_ai", alias = "edenai")]
Edenai,
/// User-defined OpenAI-compatible endpoint (#1519).
///
/// A single dynamic identity for arbitrary `[providers.<name>]
@@ -224,7 +232,7 @@ impl ProviderKind {
/// stay on the enum for serde and `provider_for_kind`, but they are not
/// first-class catalog rows. Plan is `mode` / base_url; dialect is
/// `wire = openai|anthropic` on the primary provider config.
pub const ALL: [Self; 40] = [
pub const ALL: [Self; 42] = [
Self::Deepseek,
Self::NvidiaNim,
Self::Openai,
@@ -243,6 +251,7 @@ impl ProviderKind {
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::OllamaCloud,
Self::Huggingface,
Self::Together,
Self::Qianfan,
@@ -264,6 +273,7 @@ impl ProviderKind {
Self::ModelstudioTokenPlan,
Self::Google,
Self::Antigravity,
Self::Edenai,
Self::Custom,
];
+1 -1
View File
@@ -530,7 +530,7 @@ fn resolver_auto_falls_back_to_descriptor_default_without_catalog_default() {
assert!(out.logical_model().is_auto());
assert_eq!(
out.wire_model_id().as_str(),
"GLM-5.2",
"GLM-5.3",
"no catalog default → descriptor built-in default wins"
);
assert_eq!(
+377 -21
View File
@@ -1143,6 +1143,9 @@ struct EnvGuard {
sglang_base_url: Option<OsString>,
vllm_api_key: Option<OsString>,
vllm_base_url: Option<OsString>,
ollama_cloud_api_key: Option<OsString>,
ollama_cloud_base_url: Option<OsString>,
ollama_cloud_model: Option<OsString>,
ollama_api_key: Option<OsString>,
ollama_base_url: Option<OsString>,
huggingface_api_key: Option<OsString>,
@@ -1163,6 +1166,9 @@ struct EnvGuard {
telecomjs_api_key: Option<OsString>,
telecomjs_base_url: Option<OsString>,
telecomjs_model: Option<OsString>,
edenai_api_key: Option<OsString>,
edenai_base_url: Option<OsString>,
edenai_model: Option<OsString>,
opencode_go_api_key: Option<OsString>,
opencode_go_base_url: Option<OsString>,
opencode_go_model: Option<OsString>,
@@ -1202,6 +1208,9 @@ impl EnvGuard {
telecomjs_api_key: env::var_os("TELECOMJS_API_KEY"),
telecomjs_base_url: env::var_os("TELECOMJS_BASE_URL"),
telecomjs_model: env::var_os("TELECOMJS_MODEL"),
edenai_api_key: env::var_os("EDENAI_API_KEY"),
edenai_base_url: env::var_os("EDENAI_BASE_URL"),
edenai_model: env::var_os("EDENAI_MODEL"),
opencode_go_api_key: env::var_os("OPENCODE_GO_API_KEY"),
opencode_go_base_url: env::var_os("OPENCODE_GO_BASE_URL"),
opencode_go_model: env::var_os("OPENCODE_GO_MODEL"),
@@ -1302,6 +1311,9 @@ impl EnvGuard {
sglang_base_url: env::var_os("SGLANG_BASE_URL"),
vllm_api_key: env::var_os("VLLM_API_KEY"),
vllm_base_url: env::var_os("VLLM_BASE_URL"),
ollama_cloud_api_key: env::var_os("OLLAMA_CLOUD_API_KEY"),
ollama_cloud_base_url: env::var_os("OLLAMA_CLOUD_BASE_URL"),
ollama_cloud_model: env::var_os("OLLAMA_CLOUD_MODEL"),
ollama_api_key: env::var_os("OLLAMA_API_KEY"),
ollama_base_url: env::var_os("OLLAMA_BASE_URL"),
huggingface_api_key: env::var_os("HUGGINGFACE_API_KEY"),
@@ -1334,6 +1346,9 @@ impl EnvGuard {
env::remove_var("TELECOMJS_API_KEY");
env::remove_var("TELECOMJS_BASE_URL");
env::remove_var("TELECOMJS_MODEL");
env::remove_var("EDENAI_API_KEY");
env::remove_var("EDENAI_BASE_URL");
env::remove_var("EDENAI_MODEL");
env::remove_var("OPENCODE_GO_API_KEY");
env::remove_var("OPENCODE_GO_BASE_URL");
env::remove_var("OPENCODE_GO_MODEL");
@@ -1434,6 +1449,9 @@ impl EnvGuard {
env::remove_var("SGLANG_BASE_URL");
env::remove_var("VLLM_API_KEY");
env::remove_var("VLLM_BASE_URL");
env::remove_var("OLLAMA_CLOUD_API_KEY");
env::remove_var("OLLAMA_CLOUD_BASE_URL");
env::remove_var("OLLAMA_CLOUD_MODEL");
env::remove_var("OLLAMA_API_KEY");
env::remove_var("OLLAMA_BASE_URL");
env::remove_var("HUGGINGFACE_API_KEY");
@@ -1489,6 +1507,9 @@ impl Drop for EnvGuard {
Self::restore_var("TELECOMJS_API_KEY", self.telecomjs_api_key.take());
Self::restore_var("TELECOMJS_BASE_URL", self.telecomjs_base_url.take());
Self::restore_var("TELECOMJS_MODEL", self.telecomjs_model.take());
Self::restore_var("EDENAI_API_KEY", self.edenai_api_key.take());
Self::restore_var("EDENAI_BASE_URL", self.edenai_base_url.take());
Self::restore_var("EDENAI_MODEL", self.edenai_model.take());
Self::restore_var("OPENCODE_GO_API_KEY", self.opencode_go_api_key.take());
Self::restore_var("OPENCODE_GO_BASE_URL", self.opencode_go_base_url.take());
Self::restore_var("OPENCODE_GO_MODEL", self.opencode_go_model.take());
@@ -1604,6 +1625,9 @@ impl Drop for EnvGuard {
Self::restore_var("SGLANG_BASE_URL", self.sglang_base_url.take());
Self::restore_var("VLLM_API_KEY", self.vllm_api_key.take());
Self::restore_var("VLLM_BASE_URL", self.vllm_base_url.take());
Self::restore_var("OLLAMA_CLOUD_API_KEY", self.ollama_cloud_api_key.take());
Self::restore_var("OLLAMA_CLOUD_BASE_URL", self.ollama_cloud_base_url.take());
Self::restore_var("OLLAMA_CLOUD_MODEL", self.ollama_cloud_model.take());
Self::restore_var("OLLAMA_API_KEY", self.ollama_api_key.take());
Self::restore_var("OLLAMA_BASE_URL", self.ollama_base_url.take());
Self::restore_var("HUGGINGFACE_API_KEY", self.huggingface_api_key.take());
@@ -1618,29 +1642,54 @@ impl Drop for EnvGuard {
struct RecordingSecretsStore {
gets: Mutex<Vec<String>>,
sets: Mutex<Vec<String>>,
deletes: Mutex<Vec<String>>,
value: Option<String>,
values: std::collections::HashMap<String, String>,
}
impl RecordingSecretsStore {
fn with_value(value: &str) -> Self {
Self {
gets: Mutex::new(Vec::new()),
sets: Mutex::new(Vec::new()),
deletes: Mutex::new(Vec::new()),
value: Some(value.to_string()),
values: std::collections::HashMap::new(),
}
}
fn with_entries(entries: &[(&str, &str)]) -> Self {
Self {
gets: Mutex::new(Vec::new()),
sets: Mutex::new(Vec::new()),
deletes: Mutex::new(Vec::new()),
value: None,
values: entries
.iter()
.map(|(key, value)| ((*key).to_string(), (*value).to_string()))
.collect(),
}
}
fn empty() -> Self {
Self::with_entries(&[])
}
}
impl codewhale_secrets::KeyringStore for RecordingSecretsStore {
fn get(&self, key: &str) -> Result<Option<String>, codewhale_secrets::SecretsError> {
self.gets.lock().unwrap().push(key.to_string());
Ok(self.value.clone())
Ok(self.values.get(key).cloned().or_else(|| self.value.clone()))
}
fn set(&self, _key: &str, _value: &str) -> Result<(), codewhale_secrets::SecretsError> {
fn set(&self, key: &str, _value: &str) -> Result<(), codewhale_secrets::SecretsError> {
self.sets.lock().unwrap().push(key.to_string());
Ok(())
}
fn delete(&self, _key: &str) -> Result<(), codewhale_secrets::SecretsError> {
fn delete(&self, key: &str) -> Result<(), codewhale_secrets::SecretsError> {
self.deletes.lock().unwrap().push(key.to_string());
Ok(())
}
@@ -4093,6 +4142,12 @@ fn provider_kind_parses_openrouter_and_novita_aliases() {
ProviderKind::parse("ollama-local"),
Some(ProviderKind::Ollama)
);
for alias in ["ollama-cloud", "ollama_cloud"] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::OllamaCloud));
let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("ollama cloud alias");
assert_eq!(parsed.provider, ProviderKind::OllamaCloud);
}
assert_eq!(
ProviderKind::parse("wanjie-ark"),
Some(ProviderKind::WanjieArk)
@@ -4710,6 +4765,66 @@ model = "glm-5.2"
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
}
#[test]
fn edenai_resolves_named_chat_gateway_and_environment_overrides() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
for alias in ["edenai", "eden-ai", "eden_ai"] {
assert_eq!(ProviderKind::parse(alias), Some(ProviderKind::Edenai));
let parsed: ConfigToml =
toml::from_str(&format!("provider = \"{alias}\"")).expect("Eden AI alias");
assert_eq!(parsed.provider, ProviderKind::Edenai);
}
let metadata = provider::resolve_provider("eden-ai").expect("Eden AI metadata");
assert_eq!(metadata.id(), "edenai");
assert_eq!(metadata.display_name(), "Eden AI");
assert_eq!(metadata.provider_config_key(), "edenai");
assert_eq!(metadata.default_base_url(), DEFAULT_EDENAI_BASE_URL);
assert_eq!(metadata.default_model(), DEFAULT_EDENAI_MODEL);
assert_eq!(metadata.env_vars(), &["EDENAI_API_KEY"]);
assert_eq!(
metadata.wire_policy(),
provider::WirePolicy::Fixed(provider::WireFormat::ChatCompletions)
);
let config: ConfigToml = toml::from_str(
r#"
provider = "edenai"
[providers.edenai]
api_key = "eden-config-key"
model = "anthropic/claude-sonnet-4-5"
"#,
)
.expect("Eden AI provider table");
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Edenai);
assert_eq!(resolved.base_url, DEFAULT_EDENAI_BASE_URL);
assert_eq!(resolved.model, "anthropic/claude-sonnet-4-5");
assert_eq!(resolved.api_key.as_deref(), Some("eden-config-key"));
assert_eq!(
resolved.api_key_source,
Some(RuntimeApiKeySource::ConfigFile)
);
unsafe {
std::env::set_var("EDENAI_API_KEY", "eden-env-key");
std::env::set_var("EDENAI_BASE_URL", "https://api.eu.edenai.run/v3");
std::env::set_var("EDENAI_MODEL", "deepseek/deepseek-v4-flash");
}
let env_config = ConfigToml {
provider: ProviderKind::Edenai,
..ConfigToml::default()
};
let resolved = env_config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.base_url, "https://api.eu.edenai.run/v3");
assert_eq!(resolved.model, "deepseek/deepseek-v4-flash");
assert_eq!(resolved.api_key.as_deref(), Some("eden-env-key"));
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Env));
}
#[test]
fn opencode_zen_configures_model_aware_provider_with_catalog_proof() {
let _lock = env_lock();
@@ -4811,9 +4926,9 @@ fn meta_model_api_scopes_both_documented_key_names_to_official_endpoint() {
fn provider_metadata_registry_covers_every_provider_kind_once() {
let providers = provider::all_providers();
// Full registry keeps legacy dialect/plan kinds for provider_for_kind.
assert_eq!(providers.len(), 45);
assert_eq!(providers.len(), 47);
// Catalog surface is one identity per vendor (no dual-wire / plan rows).
assert_eq!(ProviderKind::ALL.len(), 40);
assert_eq!(ProviderKind::ALL.len(), 42);
assert!(ProviderKind::ALL.len() < providers.len());
let mut ids = std::collections::BTreeSet::new();
@@ -5122,25 +5237,29 @@ fn xiaomi_mimo_aliases_resolve_to_canonical_models() {
#[test]
fn zai_aliases_resolve_to_canonical_models() {
// GLM-5.2 is the default; the glm-5.1 alias must still resolve to 5.1
// GLM-5.3 is the default; the glm-5.1 alias must still resolve to 5.1
// (not to the default), and GLM-5-Turbo resolves to its own id.
assert_eq!(
normalize_model_for_provider(ProviderKind::Zai, "glm-5.1"),
ZAI_GLM_5_1_MODEL
);
assert_eq!(
normalize_model_for_provider(ProviderKind::Zai, "glm-5-2"),
DEFAULT_ZAI_MODEL
);
assert_eq!(DEFAULT_ZAI_MODEL, "GLM-5.2");
// GLM-5.3 is a peer, not the default: its aliases must land on its own id
// and must never fold into DEFAULT_ZAI_MODEL.
assert_eq!(DEFAULT_ZAI_MODEL, "GLM-5.3");
assert_eq!(DEFAULT_ZAI_MODEL, ZAI_GLM_5_3_MODEL);
for alias in ["glm-5.3", "glm-5-3", "zai-glm-5.3", "zai-glm-5-3"] {
assert_eq!(
normalize_model_for_provider(ProviderKind::Zai, alias),
ZAI_GLM_5_3_MODEL,
"{alias} must canonicalize to GLM-5.3"
);
}
// GLM-5.2 is a peer, no longer the default: an explicit 5.2 selection
// must keep its own id and must never fold into DEFAULT_ZAI_MODEL.
for alias in ["glm-5.2", "glm-5-2", "zai-glm-5.2", "zai-glm-5-2"] {
assert_eq!(
normalize_model_for_provider(ProviderKind::Zai, alias),
ZAI_GLM_5_2_MODEL,
"{alias} must canonicalize to GLM-5.2"
);
assert_ne!(
normalize_model_for_provider(ProviderKind::Zai, alias),
DEFAULT_ZAI_MODEL,
@@ -5186,10 +5305,10 @@ fn zhipu_aliases_fold_into_zai_provider() {
);
assert_eq!(provider.model.as_deref(), Some("glm-5-2"));
// GLM aliases canonicalize under the Zai umbrella.
// GLM aliases canonicalize under the Zai umbrella, to their own ids.
assert_eq!(
normalize_model_for_provider(ProviderKind::Zai, "glm-5-2"),
DEFAULT_ZAI_MODEL
ZAI_GLM_5_2_MODEL
);
}
@@ -5896,6 +6015,238 @@ fn ollama_provider_defaults_to_local_endpoint_and_small_model() {
assert_eq!(resolved.api_key, None);
}
#[test]
fn ollama_cloud_endpoint_is_official_but_neighboring_routes_are_custom() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
assert!(provider_base_url_is_official(
ProviderKind::Ollama,
DEFAULT_OLLAMA_BASE_URL
));
for base_url in [
provider::OLLAMA_CLOUD_BASE_URL,
"https://ollama.com/v1/",
" HTTPS://OLLAMA.COM/v1/ ",
] {
for provider in [ProviderKind::Ollama, ProviderKind::OllamaCloud] {
assert!(provider_base_url_is_official(provider, base_url));
assert!(!provider_preserves_custom_base_url_model(
provider, base_url
));
}
}
for base_url in [
"http://ollama.com/v1",
"https://ollama.com/api",
"https://ollama.com/v1/preview",
"https://ollama.com.evil.example/v1",
"https://ollama-gateway.example/v1",
] {
for provider in [ProviderKind::Ollama, ProviderKind::OllamaCloud] {
assert!(!provider_base_url_is_official(provider, base_url));
assert!(provider_preserves_custom_base_url_model(provider, base_url));
}
}
}
#[test]
fn explicit_ollama_cloud_defaults_to_hosted_route_and_is_not_keyless() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let config = ConfigToml {
provider: ProviderKind::OllamaCloud,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::OllamaCloud);
assert_eq!(resolved.base_url, DEFAULT_OLLAMA_CLOUD_BASE_URL);
assert_eq!(resolved.model, DEFAULT_OLLAMA_CLOUD_MODEL);
assert_eq!(resolved.api_key, None);
}
#[test]
fn ollama_cloud_preserves_provider_authoritative_model_ids() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let mut configured = ConfigToml {
provider: ProviderKind::OllamaCloud,
..ConfigToml::default()
};
configured.providers.ollama_cloud.model = Some("vendor/model:tag".to_string());
let resolved = configured.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.model, "vendor/model:tag");
let persisted_root = ConfigToml {
provider: ProviderKind::OllamaCloud,
default_text_model: Some("deepseek-v4-flash:0731".to_string()),
..ConfigToml::default()
};
let resolved = persisted_root.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.model, "deepseek-v4-flash:0731");
}
#[test]
fn ollama_cloud_env_prefers_pi_compatible_name_then_official_name() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe {
env::set_var("DEEPSEEK_PROVIDER", "ollama-cloud");
env::set_var("OLLAMA_CLOUD_API_KEY", "pi-compatible-key");
env::set_var("OLLAMA_API_KEY", "official-fallback-key");
}
let preferred = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(preferred.provider, ProviderKind::OllamaCloud);
assert_eq!(preferred.api_key.as_deref(), Some("pi-compatible-key"));
assert_eq!(preferred.api_key_source, Some(RuntimeApiKeySource::Env));
// Safety: same serialized test restores both values through EnvGuard.
unsafe { env::remove_var("OLLAMA_CLOUD_API_KEY") };
let fallback = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(fallback.api_key.as_deref(), Some("official-fallback-key"));
assert_eq!(fallback.api_key_source, Some(RuntimeApiKeySource::Env));
}
#[test]
fn local_ollama_never_consumes_the_cloud_specific_environment_key() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
unsafe { env::set_var("OLLAMA_CLOUD_API_KEY", "must-not-reach-local-ollama") };
let config = ConfigToml {
provider: ProviderKind::Ollama,
..ConfigToml::default()
};
let resolved = config.resolve_runtime_options(&CliRuntimeOverrides::default());
assert_eq!(resolved.provider, ProviderKind::Ollama);
assert_eq!(resolved.base_url, DEFAULT_OLLAMA_BASE_URL);
assert_eq!(resolved.api_key, None);
}
#[test]
fn exact_legacy_ollama_cloud_tuple_migrates_in_memory_without_writes() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let store = Arc::new(RecordingSecretsStore::with_entries(&[(
"ollama",
"legacy-cloud-key",
)]));
let secrets = Secrets::new(store.clone());
let mut config = ConfigToml {
provider: ProviderKind::Ollama,
..ConfigToml::default()
};
config.providers.ollama.base_url = Some(provider::OLLAMA_CLOUD_BASE_URL.to_string());
config.providers.ollama.model = Some("legacy-cloud-model".to_string());
let before = toml::to_string(&config).expect("serialize pre-migration config");
let resolved =
config.resolve_runtime_options_with_secrets(&CliRuntimeOverrides::default(), &secrets);
assert_eq!(resolved.provider, ProviderKind::OllamaCloud);
assert_eq!(resolved.base_url, provider::OLLAMA_CLOUD_BASE_URL);
assert_eq!(resolved.model, "legacy-cloud-model");
assert_eq!(resolved.api_key.as_deref(), Some("legacy-cloud-key"));
assert_eq!(resolved.api_key_source, Some(RuntimeApiKeySource::Keyring));
assert_eq!(
store.gets.lock().unwrap().as_slice(),
["ollama-cloud", "ollama"]
);
assert!(store.sets.lock().unwrap().is_empty());
assert!(store.deletes.lock().unwrap().is_empty());
assert_eq!(
toml::to_string(&config).expect("serialize post-migration config"),
before,
"runtime migration must not rewrite the parsed config"
);
}
#[test]
fn explicit_ollama_cloud_uses_only_its_new_secret_slot() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let store = Arc::new(RecordingSecretsStore::with_entries(&[
("ollama-cloud", "cloud-key"),
("ollama", "must-not-be-consumed"),
]));
let secrets = Secrets::new(store.clone());
let mut config = ConfigToml {
provider: ProviderKind::OllamaCloud,
..ConfigToml::default()
};
config.providers.ollama.base_url = Some(provider::OLLAMA_CLOUD_BASE_URL.to_string());
let resolved =
config.resolve_runtime_options_with_secrets(&CliRuntimeOverrides::default(), &secrets);
assert_eq!(resolved.provider, ProviderKind::OllamaCloud);
assert_eq!(resolved.api_key.as_deref(), Some("cloud-key"));
assert_eq!(store.gets.lock().unwrap().as_slice(), ["ollama-cloud"]);
assert!(store.sets.lock().unwrap().is_empty());
assert!(store.deletes.lock().unwrap().is_empty());
}
#[test]
fn explicit_ollama_cloud_never_falls_back_to_local_secret_slot() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
let store = Arc::new(RecordingSecretsStore::with_entries(&[(
"ollama",
"must-not-be-consumed",
)]));
let secrets = Secrets::new(store.clone());
let config = ConfigToml {
provider: ProviderKind::OllamaCloud,
..ConfigToml::default()
};
let resolved =
config.resolve_runtime_options_with_secrets(&CliRuntimeOverrides::default(), &secrets);
assert_eq!(resolved.api_key, None);
assert_eq!(store.gets.lock().unwrap().as_slice(), ["ollama-cloud"]);
}
#[test]
fn neighboring_legacy_ollama_routes_do_not_migrate_or_probe_cloud_secrets() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
for base_url in [
"http://ollama.com/v1",
"https://ollama.com/api",
"https://ollama.com/v1/preview",
"https://ollama.com.evil.example/v1",
"https://ollama-gateway.example/v1",
] {
let store = Arc::new(RecordingSecretsStore::with_entries(&[
("ollama-cloud", "cloud-key"),
("ollama", "legacy-key"),
]));
let secrets = Secrets::new(store.clone());
let mut config = ConfigToml {
provider: ProviderKind::Ollama,
..ConfigToml::default()
};
config.providers.ollama.base_url = Some(base_url.to_string());
let resolved =
config.resolve_runtime_options_with_secrets(&CliRuntimeOverrides::default(), &secrets);
assert_eq!(resolved.provider, ProviderKind::Ollama, "{base_url}");
assert_eq!(resolved.api_key, None, "{base_url}");
assert!(store.gets.lock().unwrap().is_empty(), "{base_url}");
}
}
#[test]
fn self_hosted_providers_do_not_probe_secret_store_by_default() {
let _lock = env_lock();
@@ -6242,7 +6593,7 @@ fn ollama_provider_preserves_model_tags() {
}
#[test]
fn ollama_remote_env_url_does_not_inherit_ambient_optional_key() {
fn ollama_custom_remote_does_not_inherit_ambient_or_saved_official_key() {
let _lock = env_lock();
let _env = EnvGuard::without_deepseek_runtime_overrides();
// Safety: test-only environment mutation guarded by a module mutex.
@@ -6252,12 +6603,20 @@ fn ollama_remote_env_url_does_not_inherit_ambient_optional_key() {
env::set_var("OLLAMA_API_KEY", "ollama-env-key");
}
let resolved = ConfigToml::default().resolve_runtime_options(&CliRuntimeOverrides::default());
let store = Arc::new(RecordingSecretsStore::with_value("ollama-saved-key"));
let secrets = Secrets::new(store.clone());
let resolved = ConfigToml::default()
.resolve_runtime_options_with_secrets(&CliRuntimeOverrides::default(), &secrets);
assert_eq!(resolved.provider, ProviderKind::Ollama);
assert_eq!(resolved.base_url, "http://ollama.example/v1");
assert_eq!(resolved.api_key, None);
assert_eq!(resolved.api_key_source, None);
assert!(
store.gets.lock().unwrap().is_empty(),
"a custom Ollama endpoint must not read the official ollama secret slot"
);
}
#[test]
@@ -6931,10 +7290,7 @@ fn sentinel_config_values_fall_through_without_becoming_runtime_keys() {
assert_eq!(resolved.api_key_source, None);
assert!(custom_store.gets.lock().unwrap().is_empty());
let empty_store = Arc::new(RecordingSecretsStore {
gets: Mutex::new(Vec::new()),
value: None,
});
let empty_store = Arc::new(RecordingSecretsStore::empty());
let empty_secrets = Secrets::new(empty_store);
let mut xiaomi = ConfigToml {
provider: ProviderKind::XiaomiMimo,
+1
View File
@@ -21,6 +21,7 @@ codewhale-mcp = { path = "../mcp", version = "0.9.8" }
codewhale-protocol = { path = "../protocol", version = "0.9.8" }
codewhale-state = { path = "../state", version = "0.9.8" }
codewhale-tools = { path = "../tools", version = "0.9.8" }
regex = "1.11"
serde_json = { workspace = true, features = ["preserve_order"] }
tokio = { workspace = true, features = ["time"] }
tracing.workspace = true
+1
View File
@@ -4,6 +4,7 @@ pub mod ids;
pub mod journal;
pub mod request;
pub mod session;
pub mod tool_parser;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};
+64
View File
@@ -0,0 +1,64 @@
//! The user-facing approval posture (`Ask` / `Auto-Review` / `Full Access` /
//! `Never`). Lives beside `AskForApproval` so policy code and the TUI share
//! one definition; the TUI adds only presentation on top.
/// Determines when tool executions require user approval
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ApprovalMode {
/// Automatically review risky tool calls before deciding whether to ask.
Auto,
/// Bypass approvals entirely (YOLO mode / --yolo flag).
Bypass,
/// Suggest approval for non-safe tools (non-YOLO modes)
#[default]
Suggest,
/// Never execute tools requiring approval
Never,
}
impl ApprovalMode {
/// Shift+Tab permission cycle order (#0.8.68 M2).
pub const PERMISSION_CYCLE: [Self; 3] = [Self::Suggest, Self::Auto, Self::Bypass];
pub fn label(self) -> &'static str {
match self {
ApprovalMode::Auto => "AUTO",
ApprovalMode::Bypass => "BYPASS",
ApprovalMode::Suggest => "SUGGEST",
ApprovalMode::Never => "NEVER",
}
}
pub fn from_config_value(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"auto" | "auto-review" | "auto_review" => Some(ApprovalMode::Auto),
"bypass" | "yolo" | "dontask" | "dont_ask" | "bypass-permissions"
| "bypasspermissions" | "full-access" | "full_access" | "full" => {
Some(ApprovalMode::Bypass)
}
"suggest" | "suggested" | "on-request" | "untrusted" | "ask" => {
Some(ApprovalMode::Suggest)
}
"never" | "deny" | "denied" => Some(ApprovalMode::Never),
_ => None,
}
}
#[must_use]
pub fn cycle_permission_next(self) -> Self {
let Some(index) = Self::PERMISSION_CYCLE.iter().position(|mode| *mode == self) else {
return Self::Suggest;
};
Self::PERMISSION_CYCLE[(index + 1) % Self::PERMISSION_CYCLE.len()]
}
#[must_use]
pub fn permission_chip_label(self) -> &'static str {
match self {
Self::Suggest => "Ask",
Self::Auto => "Auto-Review",
Self::Bypass => "Full Access",
Self::Never => "Never",
}
}
}
+3
View File
@@ -1,6 +1,9 @@
pub mod approval_mode;
pub mod bash_arity;
pub mod shell_expand;
pub use approval_mode::ApprovalMode;
use std::collections::HashSet;
use anyhow::Result;
+9 -7
View File
@@ -98,15 +98,15 @@ impl InstallMethod {
/// The exact shell command that updates this install.
///
/// Homebrew still points at the legacy `deepseek-tui` formula: no
/// `codewhale` formula is published yet (see `docs/INSTALL.md`), and
/// naming one that does not exist would hand the user a command that
/// fails.
/// Homebrew's primary formula is `codewhale`. Existing Cellar paths
/// under the legacy `deepseek-tui` name still detect as Homebrew; those
/// installs can keep using `brew upgrade deepseek-tui` during the
/// overlap window, but new notices name the Codewhale formula.
#[must_use]
pub fn update_command(self) -> &'static str {
match self {
Self::Npm => "npm install -g codewhale@latest",
Self::Homebrew => "brew upgrade deepseek-tui",
Self::Homebrew => "brew upgrade codewhale",
Self::Cargo => "cargo install codewhale-cli --locked --force",
Self::Binary => "codewhale update",
}
@@ -167,6 +167,9 @@ mod tests {
#[test]
fn homebrew_install_is_detected_from_cellar_on_both_prefixes() {
for exe in [
"/opt/homebrew/Cellar/codewhale/0.9.8/bin/codewhale",
"/usr/local/Cellar/codewhale/0.9.8/bin/codewhale",
"/home/linuxbrew/.linuxbrew/Cellar/codewhale/0.9.8/bin/codewhale",
"/opt/homebrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
"/usr/local/Cellar/deepseek-tui/0.9.4/bin/codewhale",
"/home/linuxbrew/.linuxbrew/Cellar/deepseek-tui/0.9.4/bin/codewhale",
@@ -177,10 +180,9 @@ mod tests {
"{exe} should read as Homebrew"
);
}
// The formula is still the legacy name; see `docs/INSTALL.md`.
assert_eq!(
InstallMethod::Homebrew.update_command(),
"brew upgrade deepseek-tui"
"brew upgrade codewhale"
);
assert!(!InstallMethod::Homebrew.supports_self_update());
}
+1
View File
@@ -5,6 +5,7 @@ use serde::Deserialize;
pub mod check;
pub mod install;
pub mod tls;
pub use check::{SuppressionReason, UpdateCheckCache, suppression_reason};
pub use install::{InstallMethod, current_install_method};
+30
View File
@@ -0,0 +1,30 @@
//! Process-wide TLS bootstrap plus the platform HTTP client constructors that
//! depend on it. Every reqwest client Codewhale builds goes through here so
//! the rustls crypto provider is installed exactly once, before the first
//! client, on every path (TUI, CLI, tests).
/// Install the rustls `ring` crypto provider if no provider is installed yet.
/// Idempotent; a second call is a no-op.
pub fn ensure_rustls_crypto_provider() {
let _ = rustls::crypto::ring::default_provider().install_default();
}
/// A ready platform HTTP client (provider installed, platform verifier).
pub fn reqwest_client() -> reqwest::Client {
ensure_rustls_crypto_provider();
reqwest_client_builder()
.build()
.expect("build platform HTTP client")
}
/// The platform HTTP client builder, with the crypto provider installed.
pub fn reqwest_client_builder() -> reqwest::ClientBuilder {
ensure_rustls_crypto_provider();
crate::platform_http_client_builder()
}
/// The blocking platform HTTP client builder, with the crypto provider installed.
pub fn reqwest_blocking_client_builder() -> reqwest::blocking::ClientBuilder {
ensure_rustls_crypto_provider();
crate::platform_blocking_http_client_builder()
}
+50
View File
@@ -1155,6 +1155,7 @@ impl Secrets {
/// | `sglang` | `SGLANG_API_KEY` |
/// | `vllm` | `VLLM_API_KEY` |
/// | `ollama` | `OLLAMA_API_KEY` |
/// | `ollama-cloud` | `OLLAMA_CLOUD_API_KEY`, `OLLAMA_API_KEY` |
/// | `openai` | `OPENAI_API_KEY` |
/// | `atlascloud` / `atlas` | `ATLASCLOUD_API_KEY` |
/// | `volcengine` / `ark` | `VOLCENGINE_API_KEY`, `VOLCENGINE_ARK_API_KEY`, `ARK_API_KEY` |
@@ -1162,6 +1163,7 @@ impl Secrets {
/// | `meta` / `muse-spark` | `META_MODEL_API_KEY`, `MODEL_API_KEY` |
/// | `xai` / `grok` | `XAI_API_KEY` |
/// | `telecomjs` / `tokenhub` | `TELECOMJS_API_KEY` |
/// | `edenai` / `eden-ai` | `EDENAI_API_KEY` |
///
/// Returns `None` if the provider is not recognised or none of its
/// candidate environment variables are set to a non-empty value.
@@ -1190,6 +1192,7 @@ pub fn env_for(name: &str) -> Option<String> {
"sglang" | "sg-lang" => &["SGLANG_API_KEY"],
"vllm" | "v-llm" => &["VLLM_API_KEY"],
"ollama" | "ollama-local" => &["OLLAMA_API_KEY"],
"ollama-cloud" | "ollama_cloud" => &["OLLAMA_CLOUD_API_KEY", "OLLAMA_API_KEY"],
"openai" => &["OPENAI_API_KEY"],
"anthropic" | "claude" => &["ANTHROPIC_API_KEY"],
"atlascloud" | "atlas-cloud" | "atlas_cloud" | "atlas" => &["ATLASCLOUD_API_KEY"],
@@ -1217,6 +1220,7 @@ pub fn env_for(name: &str) -> Option<String> {
"telecomjs" | "telecom-js" | "telecom_js" | "telecomjs-cn" | "tokenhub" => {
&["TELECOMJS_API_KEY"]
}
"edenai" | "eden-ai" | "eden_ai" => &["EDENAI_API_KEY"],
// One Alibaba Cloud Model Studio account authenticates every plan /
// dialect variant; all four names share one env convention.
"modelstudio-token-plan"
@@ -1274,6 +1278,7 @@ mod tests {
"SGLANG_API_KEY",
"VLLM_API_KEY",
"OLLAMA_API_KEY",
"OLLAMA_CLOUD_API_KEY",
"OPENAI_API_KEY",
"ATLASCLOUD_API_KEY",
"WANJIE_ARK_API_KEY",
@@ -1292,6 +1297,7 @@ mod tests {
"MODEL_API_KEY",
"XAI_API_KEY",
"TELECOMJS_API_KEY",
"EDENAI_API_KEY",
"MODELSTUDIO_API_KEY",
"DASHSCOPE_API_KEY",
SECRET_BACKEND_ENV,
@@ -1848,6 +1854,19 @@ mod tests {
clear_known_envs();
}
#[test]
fn edenai_env_aliases_resolve() {
let _guard = env_lock();
clear_known_envs();
unsafe { std::env::set_var("EDENAI_API_KEY", "eden-key") };
for alias in ["edenai", "eden-ai", "eden_ai"] {
assert_eq!(env_for(alias).as_deref(), Some("eden-key"), "{alias}");
}
clear_known_envs();
}
#[test]
fn opencode_go_env_aliases_resolve() {
let _guard = env_lock();
@@ -2103,6 +2122,37 @@ mod tests {
unsafe { std::env::remove_var("OLLAMA_API_KEY") };
}
#[test]
fn ollama_cloud_env_prefers_pi_name_then_official_name() {
let _lock = env_lock();
clear_known_envs();
// Safety: env mutation guarded by env_lock().
unsafe {
std::env::set_var("OLLAMA_CLOUD_API_KEY", "cloud-specific-key");
std::env::set_var("OLLAMA_API_KEY", "official-fallback-key");
}
assert_eq!(
env_for("ollama-cloud").as_deref(),
Some("cloud-specific-key")
);
assert_eq!(
env_for("ollama_cloud").as_deref(),
Some("cloud-specific-key")
);
// The local identity stays on its original, keyless-provider env
// contract and never consumes the cloud-specific compatibility name.
assert_eq!(env_for("ollama").as_deref(), Some("official-fallback-key"));
// Safety: env mutation guarded by env_lock().
unsafe { std::env::remove_var("OLLAMA_CLOUD_API_KEY") };
assert_eq!(
env_for("ollama-cloud").as_deref(),
Some("official-fallback-key")
);
clear_known_envs();
}
#[cfg(unix)]
#[test]
fn file_store_round_trips_with_secure_perms() {
+32 -75
View File
@@ -1,85 +1,42 @@
# crates/tui — agent guidance
# TUI agent guidance
Scope: the TUI, the runtime engine embedded in it, and everything a user sees.
Read the repo-root `AGENTS.md` first. Current flakes and known debt are in
the `codewhale-ops` repo, not here.
Scope: the terminal UI, its embedded runtime engine, and user-visible behavior.
Read the repository guidance first.
## The shell grammar (do not regress it)
## UI contracts
The default shell is the underwater system (`src/tui/underwater.rs`, `ocean.rs`,
`widgets/`, `views/`). Its contract:
- **One owner per fact.** Route/mode/permission/context live in the header;
Tasks/To-do in the top strip; receipts and the single live row in the
transcript; phase/cost/detail keys in the footer. Never restate a fact in a
second place.
- **One live row.** Settled receipts are still; only the active row and the
footer phase mark move. Decorative motion exists only in empty idle water and
stops the instant the user types or anything needs attention.
- **Phase is typed.** `ShellPhase::from_app` derives idle/typing/working/
waiting/approval/done/failed from real app state. Never invent state in a
renderer; never compare English strings to detect state — use the enums.
- **Treatment is typed.** `OceanTreatment` (ombre/flat/classic) parses once from
settings. Every treatment keeps ambient life; appearance and motion
(`low_motion`, `fancy_animations`) are independent axes.
- **Footer notices go through the toast system** (`push_status_toast` /
`active_status_toast`), never the legacy `status_message` sink: toasts carry
level + TTL, errors hold sticky, acknowledgements expire.
- **Compact tiers shed chrome, not content.** At small sizes a room drops
titles/captions/spacers before the object the user opened it to manipulate,
and bodies budget from the footer's *wrapped* height (`wrapped_footer_lines` /
`action_footer_lines`).
- **Rows are objects.** Anything selectable has a hitbox recorded at render
time, keyboard + mouse parity, and visible focus. Destructive controls arm
before they fire.
## Localization
Every user-visible string goes through `tr(locale, MessageId::…)` — no hardcoded
English in render paths. Glyphs (`▸ · ▾ ─`), key names (`Enter`, `Alt+?`), and
commands (`/fleet setup`) are composed in code, never embedded in translations.
Adding a string is a four-part change: see `locales/AGENTS.md`.
- One owner per fact: route/mode/permission/context in the header; work in the
top strip; receipts and the active row in the transcript; phase/cost/detail
controls in the footer.
- Derive state from typed enums such as `ShellPhase` and `OceanTreatment`.
Renderers must not infer state from English strings or invent lifecycle state.
- Keep settled output still. Motion is semantic, bounded, and fully disabled by
reduced-motion settings.
- Route notices through the toast system, with typed level and lifetime; do not
add new writes to the legacy `status_message` sink.
- Compact layouts remove chrome before content. Selectable rows need recorded
hitboxes, visible focus, keyboard/mouse parity, and confirmation for
destructive actions.
- User-visible prose uses `tr(locale, MessageId::...)`. Commands, key names, and
glyphs are composed in code. Follow `locales/AGENTS.md` for string changes.
## Verification
```sh
cargo test -p codewhale-tui --lib --locked # library unit suite
cargo test -p codewhale-tui --tests --locked # every crates/tui/tests/ target
cargo test -p codewhale-tui --lib --locked
cargo test -p codewhale-tui --tests --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
```
Narrower reruns of the slow acceptance targets, once `--tests` has told you
which one moved:
```sh
cargo test -p codewhale-tui --test pty qa_pty --locked
cargo test -p codewhale-tui --test pty release_runtime_qa --locked
cargo test -p codewhale-tui --test pty terminal_matrix_qa --locked
```
**`--lib` and `--tests` are disjoint target sets.** `crates/tui/tests/` holds
two dozen process-level acceptance targets that a `--lib` run never compiles,
let alone executes, so a green `cargo test -p codewhale-tui --bin codewhale-tui`
says nothing about them. `adaptive_evidence_acceptance` sat red across two
releases for exactly that reason: every routine command anyone ran was a unit-only
run, and only `cargo test --workspace` reached it. Run both, or run the
workspace gate.
Run clippy with `--all-targets`: `--bin` alone skips test targets and lets lints
reach CI.
Real-terminal QA gotchas, learned the hard way:
- The local tmux **server** may carry `NO_COLOR=1` and `TERM=dumb` from old VHS
runs — launch panes with `env -u NO_COLOR` or all color QA silently lies. tmux
also force-enables the low-motion overlay; prove full motion with
`TMUX`/`TMUX_PANE` removed.
- Scripted PTY input: one Enter on the slash menu both accepts the highlighted
match and runs it. A scripted second Enter lands *inside* whatever modal just
opened. Send one key, wait, capture.
- Judge motion from repeated captures diffed over time, never single
screenshots. Layout gates: 40x12, 60x16, 80x24, 100x32, 140x40.
- `CODEWHALE_TUI_DEBUG=1` writes per-frame diff sizes to
`~/.codewhale/logs/tui-render.log`. Streaming should be tens of cells per
frame; a multi-thousand-cell frame is only acceptable on a genuine layout
transition.
`--lib` and `--tests` are disjoint; use both or the workspace gate.
`scripts/dev-test.sh <area|path> [filter]` prints and runs the fastest
targeted invocation for a source path (for example
`scripts/dev-test.sh crates/tui/src/elapsed.rs`). It uses `cargo nextest
run` when nextest is installed (`CODEWHALE_DEV_NEXTEST=0` forces libtest)
and applies `scripts/dev-cache.sh` so a new worktree gets an isolated
Cargo build-dir. For PTY
failures, rerun the exact test before changing behavior. Script one input at a
time and capture after the UI settles. Validate representative layouts at
40x12, 60x16, 80x24, 100x32, and 140x40; judge motion from repeated frames, not
a single screenshot. Remove inherited `NO_COLOR`, `TERM=dumb`, and tmux motion
overrides when they would invalidate the observation.
+239 -14
View File
@@ -7,19 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.9.8] - 2026-08-16
Codewhale v0.9.8 ships the remaining assigned finish. Remaining web
settings polish moves to v0.9.9. Prefab third-party templates that have
a published OpenAI-compatible host ship here (#5350).
### Fixed
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter is
gone so expanding a pane rematerializes layout the same way shrinking does.
- `sudo` (and `su`/setuid helpers) work again for wheel-group administrators
who want Codewhale to be able to escalate: the Linux startup hardening's
irreversible `PR_SET_NO_NEW_PRIVS` flag — inherited by every child process —
is now skippable with `CODEWHALE_NO_NEW_PRIVS=0` (#5413). The flag stays on
by default; the no-ptrace and no-core-dump measures are never skipped.
## [0.9.8] - 2026-08-14
Remaining web settings polish and prefab third-party templates move to
v0.9.9.
- Abort-class process deaths no longer poison the terminal (#5424). A
stack overflow, allocation failure, or double panic skips the panic hook
and every cleanup guard, which is how a v0.9.7 user's mid-turn exit left
mouse capture leaking SGR sequences into their shell. An
async-signal-safe handler now restores the terminal modes and appends a
one-line cause marker to `~/.codewhale/crashes/last-fatal-signal.log`
before re-raising, keeping the honest 128+signal wait status. A SIGKILL
(OOM killer) remains uninterceptable by design.
### Changed
- Prompt-cache prefix is pinned for the session. The tool loop no longer
recomposes the system prompt from disk on every model step, so an agent
writing a file no longer busts the provider KV prefix cache mid-turn. The
system prompt and tool catalog are re-composed only on a declared header
change (`/model`, mode, goal, session resume), which re-pins under a logged
reason; an undeclared change is reported as drift and the original pin is
kept instead of silently becoming the new baseline. Workspace, AGENTS.md,
skills, memory, and goal drift now reaches the model as one bounded
`<context_update>` user message at the next user turn — a history append,
not a header rewrite. `/cache stats` shows the pin reason, the last-miss
reason, the undeclared-drift count, and the context-update count. See
[docs/CACHE.md](docs/CACHE.md).
- Plugin compatibility is now per-component. A reviewed, trusted, enabled
bundle that mixes Skills or MCP with unsupported commands, agents, hooks,
LSP, native, filesystem-roots, or lifecycle-mutation declarations keeps the
@@ -54,8 +79,11 @@ v0.9.9.
thread** cannot paint over the session fact chips. Chips wrap instead
of sliding under the rail.
- Z.ai `GLM-5.3` is a live Coding Plan picker option (`/model` after
`/provider zai`, or `model = "GLM-5.3"`). `GLM-5.2` stays the default.
- Z.ai `GLM-5.3` is live on the Coding Plan and is now the default direct
Z.ai model: `DEFAULT_ZAI_MODEL` resolves to `GLM-5.3` in both
`codewhale-tui` and `codewhale-config`, and it is the first `/model` row
after `/provider zai`. Explicit `GLM-5.2` selections (`model = "GLM-5.2"`
and its `glm-5.2` aliases) keep their own id — only the default moved.
Limits and reasoning options still inherit from `GLM-5.2` until Z.ai
publishes distinct 5.3 numbers. No USD price is claimed. A live call
can still 429 with entitlement code 1311 on accounts that are not
@@ -72,6 +100,116 @@ v0.9.9.
the request. A clean output-limit stop continues the turn instead of
killing it (#5373).
- Ollama Cloud is a first-class hosted provider (`/provider ollama-cloud`)
on the official OpenAI-compatible `https://ollama.com/v1` route. Local
Ollama stays keyless. The exact released `ollama` + Cloud URL tuple keeps
a bounded compatibility path across saved sessions, Fleet, and nested
subagents; neighboring remotes stay custom and fail closed against
inherited official credentials.
- Homebrew ships a `codewhale` formula. `brew tap Hmbown/deepseek-tui &&
brew install codewhale` is the install path; `brew upgrade codewhale`
updates it. The legacy `deepseek-tui` formula remains a deprecated alias
for one overlap release.
- Terminal tab/window titles now carry the existing saved session name before
the live state (`Codewhale`, `reasoning…`, `using tool…`, `done`), so parallel
sessions are identifiable at a glance without a second title setting.
`/title <name>` is a discoverable alias for `/rename`; both update the one
session name shown in the picker, composer, and terminal tab. Control,
bidi, and zero-width format characters are stripped from the saved name
itself, so `/title`, `/rename`, the picker, the Runtime API,
`codewhale sessions`, and the OSC 0 tab title all carry the same
escape-free text (#5419, Sh1Zuku).
- Eden AI is a named OpenAI-compatible Chat Completions provider (`edenai`,
aliases `eden-ai` / `eden_ai`) with `EDENAI_API_KEY`, global and EU base-URL
overrides, a live provider-scoped model catalog, and
`deepseek/deepseek-v4-pro` as the verified default. Generic reasoning fields
stay omitted because Eden AI routes multiple upstream model families
(#5422, Kai Nacke).
- Children (sub-agents and Fleet workers) inherit the session's permission
posture faithfully: Auto-Review's deterministic floor and model guardian
decide a worker's held calls (fail closed when unavailable, never a
prompt); under Ask a held call is raised in the parent's approval UI and
the worker waits visibly; Full Access still fails closed on the safety
floor. Each prompt-less decision is a one-line note in that worker's
transcript (focus mode) and an audit-log record.
- Worker role defaults keep what the role does not intend to withhold:
every built-in role keeps network reads; `planner` may run read-only
shell probes; `custom` inherits the parent's write/network/shell posture
and is narrowed only by its explicit tool list or the spawning call.
Read-only roles (`scout`, `reviewer`, `planner`, `verifier`,
`consultant`) still never write the workspace. The focused worker's
header states its effective posture from the runtime snapshot.
- `/workflow status`, `/workflow cancel [run_id]`, `/workflow settings`, and
`/workflow help` are answered by Codewhale itself from the run journal and
live run state — no model turn — and `/workflow run <path>` launches a
checked-in workflow as-is. `/config workflow` and `/config goal` explain
the effective tables. The workflow tool now honors the session `[workflow]`
table (`automatic`, `auto_start_read_only`, `require_approval_for_writes`,
limits) instead of product defaults.
- Goal mode enters as readily as DeepSeek Harness: the agent may create the
session goal when a direct request describes a verifiable multi-turn end
state, and Codewhale shows a one-line `Goal set` receipt with how to pause
or clear it. Bare `/goal` shows plain progress (and how to continue when no
turn is running), prints usage on an empty session instead of asking the
model, and `/goal help|status` are reserved words.
- Whale Teams in the terminal: the six Signal Cut whale identities (Scout,
Patch, Harbor, Echo, Keel, Lantern) appear as species badges on `/fleet`
roster rows and worker rows, with an identity portrait in the roster detail
pane and a six-state word (Resting, Thinking, Working, Waiting for you,
Blocked, Offline) derived only from the child's real runtime status. Colors
come from the theme tokens, every glyph has an ASCII fallback, and the
working wake animates only under full motion. See
`docs/design/WHALE_TEAMS_TUI.md`.
- A session metrics strip on the phase row (`4 turns · 108 steps │ LLM
11m46s · Tool call 1m52s │ TTFT avg 1.5s · 120 tok/s │ Cache hit 99% │
Input 9.3M`), on by default as the `session_metrics` footer item
(`/statusline`, `[tui].status_items`). Every value comes from engine
receipts — turn starts, per-model-call usage with stream time,
time-to-first-token and whole-call time, tool start/complete edges, and
provider-reported cache and input tokens. Cells without evidence are
omitted, never estimated. `/status` prints the untrimmed line; the phase
row sheds its lowest-value groups to fit the columns it actually has.
- Auto-Review decisions nobody was prompted for are now visible in the
transcript as one-line notes: model-guardian allow/deny verdicts with
their risk tier and stated reason, guardian failures (denied, fail
closed), deterministic policy blocks, and holds Auto-Review denied
without pausing. The audit log keeps the full record. `/permissions`
ends with the active posture, what it decides on its own versus never,
and the audit-log path. The footer's `Esc to interrupt` hint is
localized. See `docs/design/AUTO_MODE_PARITY.md` for the Claude Code /
Kimi Code parity ledger and follow-ups.
- `codewhale integrations dsh status|plan|connect|update|launch|disable|enable|remove`
connects an existing official DeepSeek Harness (`dsh` 0.1.0-rc.6, verified)
through Codewhale using only its documented seams: a `--patch` overlay that
pins the exact Codewhale provider/model/endpoint identity (native
`deepseek-official` route, or a hand-declared `openai-completions` route
named `codewhale-<provider>` for OpenAI-compatible providers), the
Codewhale permission posture exported as `DSH_PERMISSION_MODE`, and an
append-only receipt. Codewhale writes only under
`$CODEWHALE_HOME/integrations/dsh/`, never copies API keys or edits DSH
files, never broadens permissions (`--allow-full-access` only mirrors an
existing Codewhale full-access posture), and reports not-installed /
offline / incompatible / detected / connected / stale-config /
stale-version / disabled honestly. Anthropic Messages and OpenAI Responses
routes are refused as not carriable. The documented DSH plugin path is an
explicit opt-in: `install-bundle` materializes a Codewhale bundle package
(`codewhale-dsh-bundle`, MIT notice retained) and installs it with
`dsh plugin --profile codewhale add <path>` into a dedicated `codewhale`
profile (pnpm required, reported truthfully when missing; `web`/`headless`
untouched), so `dsh --profile codewhale` alone carries the identity;
`update` regenerates the bundle patch and `remove-bundle` reverses it,
leaving the DSH-owned profile directory in place. `/setup tools` and `codewhale doctor`
show the read-only detection state; `doctor` also lists the DSH read-only
credential consent alongside Codex and Grok. The optional `--skin` export
writes a Codewhale token stylesheet generated from the TUI palette
(Blue Stage dark/light, ombre water column, mode/permission/state colors,
reduced-motion fallbacks); DSH exposes no custom-theme API, so the sheet is
labeled an unsupported overlay and is never injected. See
`docs/INTEGRATIONS_DSH.md`.
### Fixed
- Selecting the `google` provider kind resolved to the `antigravity`
@@ -106,11 +244,77 @@ v0.9.9.
- Google Gemini is its own backend (`/provider google`) on the official
OpenAI-compatible route with thought-signature capture/replay and
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) joins
as a separate credential-plane provider: consent-gated read-only import
of the official CLI's login with `ANTIGRAVITY_API_KEY`/`AGY_ADC_AUTH`
precedence; requests fail closed until the cloud-code wire protocol is
implemented.
fail-closed replay for thinking models. Antigravity (`agy` 1.1.13) is
a separate provider: consent-gated read-only import of the official
CLI's login, then a text-only cloud-code stream
(`/v1internal:streamGenerateContent`). Tools, images, and unknown SSE
shapes fail closed. Gemini 3.7 Flash is not advertised until a live
turn succeeds on this wire. The website 44-count still excludes
Antigravity.
- DeepSeek Flash SSE on macOS no longer turns mid-character HTTP/2
flushes into U+FFFD replacement characters (#5374). Invalid UTF-8
fails the line instead of using lossy decode.
- `[workshop] read_result_max_bytes` and `tool_result_max_bytes` raise
the model-visible read/tool-result floor; they never lower the
compile-time defaults and cap at 2MiB (#5367).
- Fireworks and OpenCode Zen DeepSeek V4 Flash/Pro keep a bundled
family rate when the live control plane is down, so session cost is
not stuck on `unverified_live_pricing` (#5241). `kimi-k3` stays
unpriced until a published rate exists.
- Provider setup ships a SenseNova OpenAI-compatible preset (`S`) on
the published `https://token.sensenova.cn/v1` host (#5350). OpenCode
Zen/Go stay first-class rows. Agnes has no published URL, so it has
no preset.
- Privileged release workflows no longer restore rust-cache, sccache,
or npm caches after checking out a caller-supplied SHA (CodeQL
cache-poisoning #88#106). Catalog drift no longer prints raw
bundled/upstream blobs (#107).
- Cancelling a turn now cancels its foreground child agents with it.
- Empty compaction no longer wipes conversation history.
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter
is gone so expanding a pane rematerializes layout the same way shrinking
does.
- The agent tool schema rejects empty calls.
- The local web client keeps recovered stream gaps closed, user questions
answerable, manual bootstrap access intact, and streamed prose quiet for
assistive tech.
- Website zh-Hans copy now says 宪章, matching the TUI pack (#5397,
Lstarsky0).
- Public website provider facts include Google Gemini and Ollama Cloud
(44 runnable routes). Antigravity stays credential-plane-only.
Harvested from #5398 (Lstarsky0) with that correction.
- The website models page carries a truthful read-only settings preview
built from repository facts; it never implies the site can change local
configuration (#5370, #5411, mvanhorn).
- The canonical `ultra` reasoning effort now maps to each provider's
maximum tier alongside the legacy `ultracode` alias, instead of being
silently dropped (#5303, #5409, buiducnhat).
- Session titles truncate by character count, not byte offset, so
multi-byte titles (CJK, emoji) cut at the intended width and word
boundary instead of past the limit (#5415).
- Wide terminals and tmux panes fill the full available width again for the
transcript and composer (#5322). The brief v0.9 session-shell side gutter is
gone so expanding a pane rematerializes layout the same way shrinking does.
- The background verifier test drives the current libtest executable
instead of the rustup `rustc` shim, so the TUI suite no longer depends
on `$HOME` or holds the process-wide test environment lock across an
async wait (#5056, #5423, Isabel Wu).
### Removed
@@ -139,6 +343,27 @@ v0.9.9.
- Site layout uses one container, the ticker no longer implies false
provider readiness, and install links stay in the active locale.
### Contributors
- EvanProgramming (@EvanProgramming) — webhook client panic fallback
(#5381); session-index JSONL mutex (#5382).
- Lstarsky0 (@Lstarsky0) — session peek hides internal runtime events
(#5376); thinking-ladder test re-pin (#5378); provider-count follow-ups
(#5383/#5384); macOS agy fixture canonicalization (#5392); zh-Hans 宪章
terminology (#5397); regenerated website facts harvested and corrected
from #5398.
- Matt Van Horn (@mvanhorn) — read-only models settings preview on the
website (#5411, fixes #5370).
- Nhat Bui (@buiducnhat) — canonical `ultra` reasoning effort mapped across
provider effort tables (#5409); session titles truncated by character
count, not byte offset (#5415).
- Sh1Zuku (@SparkofSpike) — `/title` and the session name in the terminal
tab/window title, plus the mid-turn title deadlock fix (#5419).
- Kai Nacke (@redstar) — Eden AI provider registration, aliases,
`EDENAI_API_KEY`, and the global/EU endpoints (#5422).
- Isabel Wu (@wuisabel-gif) — background verifier test isolated from
rustup and `$HOME` (#5423, slice of #5056).
## [0.9.7] - 2026-08-12
Codewhale v0.9.7 keeps the catalog ordinary. Grok 4.6 lands as a normal catalog
+2 -1
View File
@@ -59,7 +59,7 @@ oauth2 = "5"
# ColorCompatBackend answers `get_cursor_position()` from tracked state
# (#2640's recommended workaround), so the pin is no longer needed and lru
# can resolve to >= 0.18.2 (RUSTSEC-2026-0253).
ratatui = { version = "=0.30.0", features = ["unstable-rendered-line-info"] }
ratatui = { version = "=0.30.2", features = ["unstable-rendered-line-info"] }
ratatui-core = "0.1"
regex = "1.11"
reqwest = { workspace = true, features = ["blocking", "stream", "form", "http2"] }
@@ -111,6 +111,7 @@ codewhale-build-support = { path = "../build-support", version = "0.9.8" }
[dev-dependencies]
cucumber = "0.23.0"
jsonschema.workspace = true
wiremock = "0.6"
tiny_http = "0.12"
pretty_assertions = "1.4"
+14 -44
View File
@@ -1,49 +1,19 @@
# crates/tui/locales — agent guidance
# Locale agent guidance
UI packs. `en.json` is the reference. Which packs are **complete** (held to
exact raw key parity with English) and which are intentionally partial is
defined by the tests, not by this file — read them rather than a list that goes
stale on the next locale PR.
`en.json` is the reference pack. Tests define which other packs require exact
parity; read those tests instead of maintaining a list here.
## Adding or changing a string
For every new string:
1. Add the `MessageId` variant, the `ALL_MESSAGE_IDS` entry, and the `en.json`
key — all three, or `message_id_list_english_pack_stay_in_exact_sync` fails.
2. Translate into every complete pack, or
`shipped_complete_packs_have_raw_key_parity_with_english` fails. Do not "fix"
that test by copying English into a pack — the silent English fallback is
invisible at runtime, so the gate is the only thing between users and
untranslated UI.
3. If you change an **existing English value**, retranslate it everywhere. Value
drift is invisible to the key gates; say what you changed in the commit body.
1. Add the `MessageId` variant, `ALL_MESSAGE_IDS` entry, and English key.
2. Translate every complete pack. Do not satisfy parity by copying English.
3. If an English value changes, update its translations as well.
## Translation conventions
Keep `{named}` placeholders literal. Commands, key names, URLs, product terms,
and glyphs follow the conventions enforced by localization tests; ordinary
prose should be natural and compact. Preserve intentional edge whitespace.
- `{named}` placeholders stay literal; call sites substitute with `.replace()`.
- Product terms stay English per pack convention: Fleet, Plan / Act / Operate,
Ask / Auto-Review / Full Access. Plain words ("read only", phase words)
translate naturally and must stay short — footers and row controls render them
in tight budgets.
- Key names, commands, and glyphs are never in translations; they are composed
in code.
- Preserve intentional leading/trailing spaces (pane titles, `Rule `, the
slash-menu hint).
- Script rules: ru/uk prose is Cyrillic only (uk uses і/ї/є/ґ, never ы/э/ъ); hi
prose is Devanagari. Latin appears only in product terms, commands, key names,
placeholders, and URLs. Script-purity fixtures in `localization.rs` enforce
this for high-visibility strings.
## Adding a locale
Pack JSON with full parity, `Locale` variant + tag/display/parse arms in
`localization.rs`, onboarding picker entry (`language.rs` — a test forces every
shipped locale to be offered), the typed `UiLocale` schema in `config_ui.rs`,
setup-wizard match arms, and locale display arms in the config/change commands.
The `/config` hint and invalid-locale error derive from `Locale::shipped()`
automatically; the schema agreement test keeps `UiLocale` aligned with that
registry. Picker hotkeys run `1..=9` then `a`, `b`, … so more than nine locales
stay single-keystroke selectable.
Translated READMEs (repo root) are separate from these packs but follow the same
discipline: `scripts/check-readme-translations.py` fails when English changes
without the translations being refreshed and restamped.
Adding a locale also requires its `Locale` registry/display/parse entries,
onboarding picker entry, `UiLocale` schema value, and setup/config match arms.
Registry agreement tests are the source of truth. Translated root READMEs are a
separate surface guarded by `scripts/check-readme-translations.py`.
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Escriu una tasca o usa /.",
"ComposerOperatePlaceholder": "Descriu lobjectiu — Codewhale continua treballant fins que estigui enllestit",
"ComposerDispatchFailedRestored": "El missatge no s'ha enviat ({error}); s'ha restaurat al redactor.",
"DispatchFailedQueued": "L'enviament ha fallat ({error}); s'han mantingut {count} seguiments en cua.",
"DispatchFailedInitial": "No s'ha pogut enviar el prompt inicial: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Proveïdor actiu",
"ConfigLabelBaseUrlDeepseek": "URL de l'API del proveïdor (ruta DeepSeek)",
"ConfigLabelProviderUrl": "URL de l'API del proveïdor",
"ConfigHintProviderUrl": "Endpoint actual del proveïdor; Xiaomi: pla de tokens | pagament per ús | URL personalitzat",
"ConfigLabelModel": "Model actiu del proveïdor",
"ConfigLabelFastModel": "Model ràpid (derivat)",
"ConfigLabelDefaultModel": "Model alternatiu heretat (només rutes DeepSeek)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "per torn · heurística local · sense petició al router",
"ModelPickerAutoLastRoute": "última {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} via {source} · Ctrl+O: detalls de la ruta",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code no pot enviar aquest torn de manera segura perquè aquesta connexió encara no admet instruccions del sistema. No sha enviat res; tria un altre proveïdor.",
"HelpTitle": "Ajuda",
"HelpFilterPlaceholder": "Escriu per filtrar",
"HelpFilterPrefix": "Filtre: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Carrega una sessió des d'un fitxer",
"CmdLogoutDescription": "Esborra la clau d'API i torna a la configuració",
"CmdMcpDescription": "Obre o gestiona servidors MCP",
"McpRecommendedUnknownId": "ID MCP recomanat desconegut. Executa {recommendations_command} per revisar la llista seleccionada.",
"McpRecommendationsSafety": "Veure aquesta llista no afegeix ni activa res. Un afegit explícit només escriu la configuració; revisa-la abans que {restart_command} connecti el servidor.",
"McpRecommendationGithub": "• github — punt final MCP remot oficial de GitHub\n punt final: {endpoint}\n lautenticació va a part: usa {login_command} només si el servidor anuncia OAuth;\n si no, configura fora de lhistorial un PAT amb privilegis mínims. Els permisos\n concedits poden escriure o suprimir dades del repositori; comença en només lectura si és possible.\n afegeix explícitament: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial de Chrome DevTools mitjançant un paquet npm fixat\n paquet: {package} ({launcher})\n pot inspeccionar/controlar Chrome i llegir pàgines autenticades. Tanca les pestanyes\n sensibles i verifica el paquet abans dafegir-lo; {restart_command} pot baixar-lo i executar-lo.\n afegeix explícitament: {add_command}",
"PluginKimiUsage": "Ús:\n {list_command}\n {approve_command}\nLa llista és de només lectura. Laprovació copia un connector canònic gestionat per Kimi mitjançant linstal·lador revisat; roman desactivat i no fiable.",
"PluginKimiManagedRootHeading": "Connectors gestionats per Kimi a {root}:",
"PluginKimiNoneFound": "No shan trobat connectors gestionats vàlids.",
"PluginKimiLicenseUnspecified": "sense especificar",
"PluginKimiApplicable": "aplicable en aquest SO",
"PluginKimiNotApplicable": "no aplicable en aquest SO",
"PluginKimiCandidateSummary": "{name} {version} — llicència={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " camí: {path}\n hash de contingut: {content_hash}\n hash de capacitats: {capability_hash}\n aprova: {approve_command}",
"PluginKimiRejectedHeading": "Entrades rebutjades (no importables):",
"PluginKimiInspectionFooter": "Aquesta inspecció no ha copiat, marcat com a fiable, activat ni executat res. No shan comprovat aplicacions, dimonis, binaris, extensions del navegador, credencials ni permisos del SO externs de Kimi.",
"PluginKimiCandidateMissing": "No hi ha cap connector canònic vàlid gestionat per Kimi anomenat `{name}`. Torna a executar {list_command}.",
"PluginKimiCandidateChanged": "El connector gestionat per Kimi `{name}` ha canviat des de la revisió. Sesperava el hash {expected} i ara és {actual}. No sha copiat res; torna a executar {list_command}.",
"PluginKimiHomeMissing": "No es pot trobar la carpeta personal per importar des de Kimi.",
"PluginKimiRootInspectFailed": "No es pot inspeccionar larrel de connectors Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "Larrel de connectors Kimi {root} ha de ser una carpeta real, no un enllaç ni un punt danàlisi.",
"PluginKimiRootCanonicalizeFailed": "No es pot canonitzar larrel de connectors Kimi {root}: {error}",
"PluginKimiRootListFailed": "No es pot llistar larrel de connectors Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "No es pot llegir una entrada de connector Kimi: {error}",
"PluginKimiEntryLimit": "Larrel de connectors Kimi conté {count} entrades; el màxim revisat per anàlisi és {max}.",
"PluginKimiEntryInspectFailed": "{path}: no es pot inspeccionar: {error}",
"PluginKimiEntryLinksRefused": "{path}: es rebutgen els enllaços i punts danàlisi",
"PluginKimiEntryOutsideRoot": "{path}: el camí canònic {canonical_path} no és fill directe de larrel gestionada",
"PluginKimiEntryCanonicalizeFailed": "{path}: no es pot canonitzar: {error}",
"PluginKimiManifestUnreadable": "{path}: no hi ha cap {manifest} llegible: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} ha de ser un fitxer normal real",
"PluginKimiManifestInvalid": "{path}: manifest no vàlid: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: el nom de la carpeta ha de coincidir exactament amb el nom `{name}` del manifest",
"PluginKimiHashUnavailable": "no disponible",
"PluginKimiRollbackDestinationMissing": "Linstal·lador no ha informat del camí de destinació.",
"PluginKimiMismatchRemoved": "El connector `{name}` copiat no coincideix amb el contingut aprovat (sesperava {expected}, sha trobat {actual}). Sha eliminat la còpia inesperada; revisa-ho i torna-ho a provar.",
"PluginKimiMismatchRollbackFailed": "Error: el connector `{name}` copiat no coincideix amb el contingut aprovat (sesperava {expected}, sha trobat {actual}) i leliminació automàtica ha fallat: {error}. Roman desactivat i no fiable; inspecciona {path} abans de continuar.",
"PluginKimiUserPluginDirectory": "la carpeta de connectors de lusuari",
"PluginKimiMarketplaceZipUnsupported": "Linstal·lador revisat de Codewhale no admet paquets ZIP de Kimi; instal·la des duna carpeta local o importa un connector gestionat per Kimi dorigen.",
"PluginKimiMarketplaceRemoteUnsupported": "Les fonts remotes de Kimi han dacabar en .tar.gz o .tgz per instal·lar-les amb Codewhale; es reconeix .zip, però no sadmet.",
"PluginKimiMarketplaceGzipTarball": "URL darxiu tar gzip",
"CmdPluginDescription": "Inspecciona i gestiona paquets de plugins confiables; les eines executables heretades es mantenen separades",
"CmdPluginBundleUsage": "Ús: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "No s'ha trobat cap paquet de plugins de Codewhale.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carrega, migra ni sincronitza codi font local amb el Work allotjat. Utilitza {command} per començar des de la punta de la branca disponible a GitHub o CNB. Els commits no enviats, els fitxers modificats o ignorats, els secrets i l'estat de la sessió es mantenen en local.",
"CmdRemoteEnvBrowserLabel": "Work allotjat de Codewhale",
"CmdRenameDescription": "Reanomena la sessió actual",
"CmdTitleDescription": "Defineix el nom de la sessió i el títol de la pestanya/finestra del terminal",
"CmdRestoreDescription": "Reverteix l'espai de treball a una instantània anterior pre/post-torn. Sense argument, llista les instantànies recents.",
"CmdRetryDescription": "Reintenta l'última petició",
"CmdReviewDescription": "Executa una revisió de codi estructurada sobre un fitxer, diff o PR",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Eines/MCP encara requereix acció; enregistrat per a l'informe de configuració (no bloqueja la primera execució).",
"SetupToolsMcpPreviewTitle": "Eines / MCP: incorporació segura",
"SetupToolsMcpOnRampText": "Eines, MCP, Skills i Plugins — Incorporació segura\n\n/setup només llegeix l'inventari local. Mai inicia servidors MCP, instal·la skills, executa plugins ni executa ordres no confiades.\n\nInventari actual:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Directori d'eines: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (adaptadors compartits): {hotbar_result}\n\nCamins (carpeta personal ocultada):\n- Configuració MCP: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nArrencada segura (executa-la tu mateix en un terminal normal o ordre del TUI):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills · /skills install <spec>\n- Plugins: /plugin · codewhale setup --plugins\n- Directori d'eines: codewhale setup --tools\n\nLes accions amb efectes secundaris sempre requereixen confirmació explícita. Les ordres de plugins es mantenen separades de les ordres de barra; la font de plugins Hotbar queda ajornada fins que arribin les portes d'aprovació.\n\nConsulta docs/MCP.md i docs/skills/README.md per al que encara requereix configuració externa manual.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — connectat a través de Codewhale, mai un segon planificador:\n- Estat: {dsh_result}\n- Detecció només de lectura; connectar/planificar/iniciar/eliminar: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale només escriu a $CODEWHALE_HOME/integrations/dsh; mai copia claus d'API ni edita fitxers de DSH.",
"HotbarActionModeOperateName": "Mode Operate",
"HotbarActionModeOperateDescription": "Posa la teva Fleet a treballar en paral·lel.",
"HomeOperateModeTip": "Operate — posa la teva Fleet a treballar en paral·lel",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet a punt",
"EmptyStateHelpConnector": "o",
"EmptyStateHelpHint": "— mira-ho tot",
"SessionsSurfaceTitle": "sessions",
"SessionsPaneTitle": " sessions (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "No s'han pogut verificar les identitats de perfil existents ({error}); arregla el fitxer anomenat abans de desar.",
"FleetProfileIdConflict": "L'id de perfil `{id}` ja l'usa {path}; torna a redactar amb un rol diferent o elimina primer el fitxer antic.",
"FleetProfileProviderUnconfigured": "El perfil fixa el proveïdor `{provider}`, que no té credencials configurades ({env}); configura'l a /provider abans de desar.",
"FleetDestStepTitle": "On s'ha de desar aquest perfil?",
"FleetDestStepSubtitle": "No s'escriu res fins que ho confirmis a l'últim pas.",
"FleetDestProjectLabel": "Aquest projecte",
"FleetDestPersonalLabel": "Personal",
"FleetDestProjectSummary": "Només aquest projecte",
"FleetDestPersonalSummary": "Disponible a tots els projectes",
"FleetDestProjectDescription": "Es desa dins d'aquest projecte ({workspace}). Només s'aplica aquí i té prioritat sobre un perfil Personal amb el mateix ID.",
"FleetDestPersonalDescription": "Es desa al teu directori de Codewhale. S'aplica a tots els projectes, excepte on un projecte tingui el seu propi perfil amb el mateix ID, que hi té prioritat.",
"FleetDestPathLine": "Fitxer: {path}",
"FleetDestUnavailable": "No disponible: {reason}",
"FleetDestReasonNoProjectConfig": "els perfils de projecte estan desactivats en aquesta sessió (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "la carpeta de l'espai de treball {path} no existeix o no és un directori",
"FleetDestReasonHomeUnavailable": "no s'ha pogut resoldre el teu directori de Codewhale ({error})",
"FleetDestWillReplace": "Substituirà el fitxer existent {path}",
"FleetDestOverridesProject": "Aquest projecte ja té un perfil '{id}', que hi té prioritat; aquest perfil Personal s'aplica als altres projectes.",
"FleetDestOverridesPersonal": "Té prioritat sobre el teu perfil Personal '{id}' dins d'aquest projecte.",
"FleetDestOverridesBuiltIn": "Substitueix el rol {origin} '{id}' de la plantilla.",
"FleetSavesToChip": "Es desa a: {scope} · {path}",
"FleetSavesToUndecided": "Es desa a: tria-ho al pas 3 — Aquest projecte o Personal",
"FleetActionSaveProject": "Desa en aquest projecte",
"FleetActionSavePersonal": "Desa com a perfil Personal",
"FleetActionReplaceProject": "Substitueix en aquest projecte",
"FleetActionReplacePersonal": "Substitueix el perfil Personal",
"FleetActionConfirmReplace": "Prem Enter una altra vegada per substituir {file}",
"FleetActionChangeDestination": "Canvia la destinació",
"FleetActionBack": "Enrere",
"FleetReviewSavesTo": "Es desa a",
"FleetModelRowBlockedNotice": "No seleccionable: {reason}. Configura-ho a /provider o tria una altra fila.",
"FleetDestProjectDisabledSave": "Els perfils de projecte estan desactivats en aquesta sessió (--no-project-config); no s'ha desat res. Tria Personal o reinicia sense l'opció.",
"WorkflowStatusWaiting": "esperant",
"WorkflowDebrief": "resum: {done}/{total} resolts · {failed} fallits · {cancelled} cancel·lats · {elapsed}",
"WorkflowTranscriptDetails": "transcripció: JSON complet de l'execució disponible als detalls de l'eina ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Execució de lautomatització {id} afegida a la cua: {status} (tasca {task})",
"AutomationDeletePreview": "La supressió encara no està confirmada. No sha suprimit res.\nAutomatització: {id} ({name})\nExecucions registrades: {run_count}\nPer suprimir la definició i lhistorial dexecucions, executa:\n{command}",
"AutomationDeleteConfirmationStale": "La confirmació de supressió ja no coincideix amb lautomatització {id}; no sha suprimit res. Revisa lestat actual amb {command}.",
"AutomationDeleted": "Sha suprimit lautomatització {id} ({name}). Execucions registrades suprimides: {run_count}."
"AutomationDeleted": "Sha suprimit lautomatització {id} ({name}). Execucions registrades suprimides: {run_count}.",
"WhaleStateResting": "Descansant",
"WhaleStateThinking": "Pensant",
"WhaleStateWorking": "Treballant",
"WhaleStateWaiting": "Esperant-te",
"WhaleStateBlocked": "Bloquejada",
"WhaleStateOffline": "Fora de línia",
"WhaleAnimalScout": "zífid",
"WhaleAnimalPatch": "marsopa comuna",
"WhaleAnimalHarbor": "iubarta",
"WhaleAnimalEcho": "cap d'olla",
"WhaleAnimalKeel": "catxalot",
"WhaleAnimalLantern": "orca",
"WhaleAnimalPlain": "balena",
"WhaleJobScout": "recerca",
"WhaleJobPatch": "programació",
"WhaleJobHarbor": "coordinació",
"WhaleJobEcho": "comunicacions",
"WhaleJobKeel": "operacions",
"WhaleJobLantern": "revisió",
"WhaleJobPlain": "feina general",
"SessionMetricsTurn": "torn",
"SessionMetricsTurns": "torns",
"SessionMetricsStep": "pas",
"SessionMetricsSteps": "passos",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Eines",
"SessionMetricsTtft": "TTFT mitj.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache",
"SessionMetricsInput": "Entrada",
"SessionMetricsStatusLine": "Mètriques de la sessió: {metrics}",
"AutoReviewReceiptGuardianAllowed": "L'Auto-Review ha permès '{tool}' (risc {risk}, guardià del model): {reason}",
"AutoReviewReceiptGuardianDenied": "L'Auto-Review ha denegat '{tool}' (risc {risk}, guardià del model): {reason}",
"AutoReviewReceiptGuardianUnavailable": "L'Auto-Review no ha pogut revisar '{tool}' ({reason}); denegat, fail closed",
"AutoReviewReceiptDeterministicBlocked": "L'Auto-Review ha bloquejat '{tool}' (política determinista): {reason}",
"AutoReviewReceiptHeld": "L'Auto-Review ha retingut '{tool}' sense aturar-se; denegat (cal una persona — canvia a Ask)",
"FooterHintEscInterrupt": "Esc per interrompre",
"PermissionsPostureHeader": "Postura de permisos actual: {posture}",
"PermissionsPostureAsk": "Ask: les crides a tool que canvien autoritat, cost, abast o resultat obren un avís; les crides de només lectura provadament segures s'executen sense. Les regles ask de dalt sempre forcen un avís.",
"PermissionsPostureAuto": "Auto-Review: mai obre un avís. Una política determinista permet crides provadament segures i bloqueja de manera estricta feina de publicació o destructiva en segon pla; les crides que no pot provar segures van a un guardià del model d'una sola passada, que permet o denega amb un motiu declarat (el risc alt o crític mai s'executa automàticament; una revisió fallida denega, fail closed). Les retencions que requereixen una persona es deneguen, no s'amaguen. Cada decisió d'aquestes s'escriu a la transcripció com a nota i al registre d'auditoria.",
"PermissionsPostureBypass": "Full Access: les crides a tool ordinàries s'executen sense avisos. Les retencions no evitables de seguretat, llei del repositori i política gestionada fan fail closed com a bloquejos estrictes en lloc de preguntar.",
"PermissionsPostureNever": "never: només s'executen les tools considerades segures/de només lectura; tota la resta es bloqueja sense avís.",
"PermissionsReceiptsNote": "Les decisions preses sense avís (veredictes del guardià de l'Auto-Review, bloquejos i retencions) apareixen com a notes a la transcripció i al registre d'auditoria a {audit_path}. Full Access es tria deliberadament amb Shift+Tab o /config, mai per una regla.",
"AgentFocusOpened": "Enfocat en {agent}. Els teus missatges ara van a aquest worker; Esc torna a la conversa principal.",
"AgentFocusClosed": "De tornada a la conversa principal.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Missatge per a {agent} · Esc torna al principal",
"AgentFocusNoTranscript": "Encara no hi ha transcripció de {agent}. Els missatges apareixen aquí a mesura que el worker els intercanvia.",
"AgentFocusOmitted": "Els missatges anteriors ({count}) s'han omès de la transcripció en memòria.",
"AgentFocusFollowUpDelivered": "A la cua per a {agent}: llegirà el missatge a la propera ronda.",
"AgentFocusFollowUpQueued": "A la cua per a {agent}",
"AgentFocusFollowUpContinued": "{agent} ja havia acabat; ha continuat en un fork nou ({target}). Aquesta vista ara segueix el fork.",
"AgentFocusFollowUpFailed": "No s'ha pogut lliurar a {agent}: {reason}",
"FooterHintForAgents": "agents",
"FooterHintToManage": "gestionar",
"AgentRailQueuedCount": "{count} a la cua",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "escriu",
"AgentFocusPostureReadOnly": "només lectura",
"AgentFocusPostureNetwork": "xarxa",
"AgentFocusPostureNoNetwork": "sense xarxa",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell de només lectura",
"AgentFocusPostureShellNone": "sense shell",
"GoalReceiptSet": "Objectiu establert: \"{objective}\" · /goal mostra el progrés · /goal pause o /goal clear l'atura",
"GoalStatusIdleHint": "no s'està executant — envia un missatge o /goal resume per continuar"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Aufgabe schreiben oder / verwenden.",
"ComposerOperatePlaceholder": "Beschreibe das Ziel — Codewhale arbeitet weiter, bis es erreicht ist",
"ComposerDispatchFailedRestored": "Nachricht nicht gesendet ({error}); im Composer wiederhergestellt.",
"DispatchFailedQueued": "Senden fehlgeschlagen ({error}); {count} wartende Folgenachricht(en) behalten.",
"DispatchFailedInitial": "Erster Prompt konnte nicht gesendet werden: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Aktiver Provider",
"ConfigLabelBaseUrlDeepseek": "Provider-API-URL (DeepSeek-Route)",
"ConfigLabelProviderUrl": "Provider-API-URL",
"ConfigHintProviderUrl": "Aktueller Provider-Endpunkt; Xiaomi: Token-Paket | nutzungsbasierte Abrechnung | benutzerdefinierte URL",
"ConfigLabelModel": "Aktives Provider-Modell",
"ConfigLabelFastModel": "Schnelles Modell (abgeleitet)",
"ConfigLabelDefaultModel": "Legacy-Fallback-Modell (nur DeepSeek-Routen)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "pro Zug · lokale Heuristik · keine Router-Anfrage",
"ModelPickerAutoLastRoute": "zuletzt {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} via {source} · Ctrl+O: Routendetails",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code kann diesen Turn nicht sicher senden, da diese Verbindung noch keine Systemanweisungen unterstützt. Nichts wurde gesendet; wähle einen anderen Anbieter.",
"HelpTitle": "Hilfe",
"HelpFilterPlaceholder": "Zum Filtern tippen",
"HelpFilterPrefix": "Filter: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Sitzung aus Datei laden",
"CmdLogoutDescription": "API-Schlüssel löschen und zum Setup zurückkehren",
"CmdMcpDescription": "MCP-Server öffnen oder verwalten",
"McpRecommendedUnknownId": "Unbekannte empfohlene MCP-ID. Mit {recommendations_command} kann die kuratierte Liste geprüft werden.",
"McpRecommendationsSafety": "Diese Liste fügt nichts hinzu und aktiviert nichts. Explizites Hinzufügen schreibt nur die Konfiguration; prüfe sie, bevor {restart_command} den Server verbindet.",
"McpRecommendationGithub": "• github — offizieller Remote-MCP-Endpunkt von GitHub\n Endpunkt: {endpoint}\n Authentifizierung erfolgt getrennt: {login_command} nur verwenden, wenn der Server OAuth anbietet;\n andernfalls außerhalb des Befehlsverlaufs ein PAT mit minimalen Rechten konfigurieren. Erteilte\n Rechte können Repository-Daten schreiben oder löschen; möglichst schreibgeschützt beginnen.\n explizit hinzufügen: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — offizielles Chrome-DevTools-MCP über ein fest versioniertes npm-Paket\n Paket: {package} ({launcher})\n es kann Chrome untersuchen/steuern und authentifizierte Seiten lesen. Vertrauliche Tabs\n schließen und das Paket vor dem Hinzufügen prüfen; {restart_command} kann es laden und ausführen.\n explizit hinzufügen: {add_command}",
"PluginKimiUsage": "Verwendung:\n {list_command}\n {approve_command}\nDie Liste ist schreibgeschützt. Die Freigabe kopiert ein kanonisches, von Kimi verwaltetes Plugin über den geprüften Installer; es bleibt deaktiviert und nicht vertrauenswürdig.",
"PluginKimiManagedRootHeading": "Von Kimi verwaltete Plugins unter {root}:",
"PluginKimiNoneFound": "Keine gültigen verwalteten Plugins gefunden.",
"PluginKimiLicenseUnspecified": "nicht angegeben",
"PluginKimiApplicable": "für dieses Betriebssystem geeignet",
"PluginKimiNotApplicable": "für dieses Betriebssystem nicht geeignet",
"PluginKimiCandidateSummary": "{name} {version} — Lizenz={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " Pfad: {path}\n Inhalts-Hash: {content_hash}\n Berechtigungs-Hash: {capability_hash}\n freigeben: {approve_command}",
"PluginKimiRejectedHeading": "Abgelehnte Einträge (nicht importierbar):",
"PluginKimiInspectionFooter": "Diese Prüfung hat nichts kopiert, als vertrauenswürdig markiert, aktiviert oder ausgeführt. Externe Kimi-Apps, Daemons, Binärdateien, Browser-Erweiterungen, Anmeldedaten und Betriebssystemrechte wurden nicht geprüft.",
"PluginKimiCandidateMissing": "Kein gültiges kanonisches, von Kimi verwaltetes Plugin namens `{name}`. {list_command} erneut ausführen.",
"PluginKimiCandidateChanged": "Das von Kimi verwaltete Plugin `{name}` wurde seit der Prüfung geändert. Erwarteter Inhalts-Hash: {expected}, aktuell: {actual}. Nichts wurde kopiert; {list_command} erneut ausführen.",
"PluginKimiHomeMissing": "Das Benutzerverzeichnis für den Kimi-Import wurde nicht gefunden.",
"PluginKimiRootInspectFailed": "Kimi-Plugin-Stamm {root} kann nicht geprüft werden: {error}",
"PluginKimiRootMustBeDirectory": "Der Kimi-Plugin-Stamm {root} muss ein echtes Verzeichnis sein, kein Link oder Reparsepunkt.",
"PluginKimiRootCanonicalizeFailed": "Kimi-Plugin-Stamm {root} kann nicht kanonisiert werden: {error}",
"PluginKimiRootListFailed": "Kimi-Plugin-Stamm {root} kann nicht aufgelistet werden: {error}",
"PluginKimiEntryReadFailed": "Ein Kimi-Plugin-Eintrag kann nicht gelesen werden: {error}",
"PluginKimiEntryLimit": "Der Kimi-Plugin-Stamm enthält {count} Einträge; pro Prüfung werden höchstens {max} geprüft.",
"PluginKimiEntryInspectFailed": "{path}: Prüfung nicht möglich: {error}",
"PluginKimiEntryLinksRefused": "{path}: Links und Reparsepunkte werden abgelehnt",
"PluginKimiEntryOutsideRoot": "{path}: Der kanonische Pfad {canonical_path} ist kein direktes Kind des verwalteten Stamms",
"PluginKimiEntryCanonicalizeFailed": "{path}: Kanonisierung nicht möglich: {error}",
"PluginKimiManifestUnreadable": "{path}: kein lesbares {manifest}: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} muss eine echte reguläre Datei sein",
"PluginKimiManifestInvalid": "{path}: ungültiges Manifest: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: Der Verzeichnisname muss exakt dem Manifestnamen `{name}` entsprechen",
"PluginKimiHashUnavailable": "nicht verfügbar",
"PluginKimiRollbackDestinationMissing": "Der Installer hat den Zielpfad nicht gemeldet.",
"PluginKimiMismatchRemoved": "Plugin `{name}` entsprach nach dem Kopieren nicht dem freigegebenen Inhalt (erwartet {expected}, gefunden {actual}). Die unerwartete Kopie wurde entfernt; prüfen und erneut versuchen.",
"PluginKimiMismatchRollbackFailed": "Fehler: Plugin `{name}` entsprach nach dem Kopieren nicht dem freigegebenen Inhalt (erwartet {expected}, gefunden {actual}); das automatische Entfernen schlug fehl: {error}. Es bleibt deaktiviert und nicht vertrauenswürdig; vor weiteren Schritten {path} prüfen.",
"PluginKimiUserPluginDirectory": "das Benutzer-Plugin-Verzeichnis",
"PluginKimiMarketplaceZipUnsupported": "Der geprüfte Codewhale-Installer unterstützt keine Kimi-ZIP-Pakete; aus einem lokalen Verzeichnis installieren oder ein übergeordnetes, von Kimi verwaltetes Plugin importieren.",
"PluginKimiMarketplaceRemoteUnsupported": "Kimi-Remotequellen müssen für die Codewhale-Installation auf .tar.gz oder .tgz enden; .zip wird erkannt, aber nicht unterstützt.",
"PluginKimiMarketplaceGzipTarball": "gzip-Tarball-URL",
"CmdPluginDescription": "Vertrauenswürdige Plugin-Bundles ansehen und verwalten; ausführbare Legacy-Tools bleiben getrennt",
"CmdPluginBundleUsage": "Verwendung: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Keine Codewhale-Plugin-Bundles gefunden.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale lädt lokalen Quellcode nicht in gehostetes Work hoch und migriert oder synchronisiert ihn nicht dorthin. Verwende {command}, um von der bei GitHub oder CNB verfügbaren Branch-Spitze zu starten. Nicht gepushte Commits, geänderte oder ignorierte Dateien, Geheimnisse und Sitzungsstatus bleiben lokal.",
"CmdRemoteEnvBrowserLabel": "Gehostetes Work von Codewhale",
"CmdRenameDescription": "Aktuelle Sitzung umbenennen",
"CmdTitleDescription": "Sitzungsnamen und Titel des Terminal-Tabs/Fensters festlegen",
"CmdRestoreDescription": "Workspace auf einen früheren Pre-/Post-Turn-Snapshot zurücksetzen. Ohne Argument werden die letzten Snapshots aufgelistet.",
"CmdRetryDescription": "Letzte Anfrage wiederholen",
"CmdReviewDescription": "Strukturiertes Code-Review für eine Datei, einen Diff oder PR ausführen",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP erfordert weiterhin Aktion; für den Setup-Bericht gespeichert (blockiert den ersten Lauf nicht).",
"SetupToolsMcpPreviewTitle": "Tools / MCP — sichere Einstiege",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills & Plugins — Sichere Einstiege\n\n/setup liest nur das lokale Inventar. Es startet nie MCP-Server, installiert keine Skills, führt keine Plugins aus und führt keine unvertrauenswürdigen Befehle aus.\n\nAktuelles Inventar:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools-Verzeichnis: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (geteilte Adapter): {hotbar_result}\n\nPfade (Home geschwärzt):\n- MCP-Config: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nSicherer Bootstrap (selbst in einem normalen Terminal oder TUI-Befehl ausführen):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills · /skills install <spec>\n- Plugins: /plugin · codewhale setup --plugins\n- Tools-Verzeichnis: codewhale setup --tools\n\nAktionen mit Nebenwirkungen erfordern immer eine explizite Bestätigung. Plugin-Befehle bleiben von Slash-Befehlen getrennt; die Hotbar-Plugin-Quelle bleibt zurückgestellt, bis Freigabe-Gates landen.\n\nSiehe docs/MCP.md und docs/skills/README.md für alles, was weiterhin manuelle externe Einrichtung braucht.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — über Codewhale verbunden, nie ein zweiter Scheduler:\n- Zustand: {dsh_result}\n- Nur-Lese-Erkennung; verbinden/planen/starten/entfernen: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale schreibt nur nach $CODEWHALE_HOME/integrations/dsh; es kopiert nie API-Schlüssel und ändert keine DSH-Dateien.",
"HotbarActionModeOperateName": "Operate-Modus",
"HotbarActionModeOperateDescription": "Ihre Fleet parallel arbeiten lassen.",
"HomeOperateModeTip": "Operate — Ihre Fleet parallel arbeiten lassen",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet bereit",
"EmptyStateHelpConnector": "oder",
"EmptyStateHelpHint": "— alles anzeigen",
"SessionsSurfaceTitle": "Sitzungen",
"SessionsPaneTitle": " Sitzungen (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "Bestehende Profil-Identitäten konnten nicht verifiziert werden ({error}); die benannte Datei vor dem Speichern korrigieren.",
"FleetProfileIdConflict": "Profil-ID `{id}` wird bereits von {path} genutzt; mit einer anderen Rolle neu entwerfen oder zuerst die alte Datei entfernen.",
"FleetProfileProviderUnconfigured": "Profil pinnt Provider `{provider}`, für den keine Zugangsdaten konfiguriert sind ({env}); vor dem Speichern in /provider einrichten.",
"FleetDestStepTitle": "Wo soll dieses Profil gespeichert werden?",
"FleetDestStepSubtitle": "Es wird nichts geschrieben, bis du im letzten Schritt bestätigst.",
"FleetDestProjectLabel": "Dieses Projekt",
"FleetDestPersonalLabel": "Persönlich",
"FleetDestProjectSummary": "Nur dieses Projekt",
"FleetDestPersonalSummary": "In jedem Projekt verfügbar",
"FleetDestProjectDescription": "Wird in diesem Projekt ({workspace}) gespeichert. Gilt nur hier und hat Vorrang vor einem persönlichen Profil mit derselben ID.",
"FleetDestPersonalDescription": "Wird in deinem Codewhale-Home gespeichert. Gilt in jedem Projekt außer dort, wo ein Projekt ein eigenes Profil mit derselben ID hat; das hat dort Vorrang.",
"FleetDestPathLine": "Datei: {path}",
"FleetDestUnavailable": "Nicht verfügbar: {reason}",
"FleetDestReasonNoProjectConfig": "Projektprofile sind in dieser Sitzung deaktiviert (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "der Arbeitsbereichsordner {path} existiert nicht oder ist kein Verzeichnis",
"FleetDestReasonHomeUnavailable": "dein Codewhale-Home konnte nicht aufgelöst werden ({error})",
"FleetDestWillReplace": "Ersetzt die vorhandene Datei {path}",
"FleetDestOverridesProject": "Dieses Projekt hat bereits ein Profil '{id}', das hier Vorrang hat; dieses persönliche Profil gilt in anderen Projekten.",
"FleetDestOverridesPersonal": "Hat in diesem Projekt Vorrang vor deinem persönlichen Profil '{id}'.",
"FleetDestOverridesBuiltIn": "Ersetzt die Rolle {origin} '{id}' in der Aufstellung.",
"FleetSavesToChip": "Speichert nach: {scope} · {path}",
"FleetSavesToUndecided": "Speichert nach: in Schritt 3 wählen Dieses Projekt oder Persönlich",
"FleetActionSaveProject": "In diesem Projekt speichern",
"FleetActionSavePersonal": "Als persönliches Profil speichern",
"FleetActionReplaceProject": "In diesem Projekt ersetzen",
"FleetActionReplacePersonal": "Persönliches Profil ersetzen",
"FleetActionConfirmReplace": "Drücke erneut Enter, um {file} zu ersetzen",
"FleetActionChangeDestination": "Speicherort ändern",
"FleetActionBack": "Zurück",
"FleetReviewSavesTo": "Speichert nach",
"FleetModelRowBlockedNotice": "Nicht wählbar: {reason}. Richte es unter /provider ein oder wähle eine andere Zeile.",
"FleetDestProjectDisabledSave": "Projektprofile sind in dieser Sitzung deaktiviert (--no-project-config); es wurde nichts gespeichert. Wähle Persönlich oder starte ohne das Flag neu.",
"WorkflowStatusWaiting": "wartet",
"WorkflowDebrief": "Debrief: {done}/{total} erledigt · {failed} fehlgeschlagen · {cancelled} abgebrochen · {elapsed}",
"WorkflowTranscriptDetails": "Transkript: vollständiges Lauf-JSON in den Tool-Details verfügbar ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Ausführung der Automatisierung {id} eingereiht: {status} (Aufgabe {task})",
"AutomationDeletePreview": "Das Löschen ist noch nicht bestätigt. Nichts wurde gelöscht.\nAutomatisierung: {id} ({name})\nAufgezeichnete Ausführungen: {run_count}\nZum Löschen der Definition und des Ausführungsverlaufs ausführen:\n{command}",
"AutomationDeleteConfirmationStale": "Die Löschbestätigung passt nicht mehr zur Automatisierung {id}; nichts wurde gelöscht. Den aktuellen Stand mit {command} prüfen.",
"AutomationDeleted": "Automatisierung {id} ({name}) gelöscht. Gelöschte aufgezeichnete Ausführungen: {run_count}."
"AutomationDeleted": "Automatisierung {id} ({name}) gelöscht. Gelöschte aufgezeichnete Ausführungen: {run_count}.",
"WhaleStateResting": "Ruht",
"WhaleStateThinking": "Denkt nach",
"WhaleStateWorking": "Arbeitet",
"WhaleStateWaiting": "Wartet auf dich",
"WhaleStateBlocked": "Blockiert",
"WhaleStateOffline": "Offline",
"WhaleAnimalScout": "Schnabelwal",
"WhaleAnimalPatch": "Schweinswal",
"WhaleAnimalHarbor": "Buckelwal",
"WhaleAnimalEcho": "Grindwal",
"WhaleAnimalKeel": "Pottwal",
"WhaleAnimalLantern": "Orca",
"WhaleAnimalPlain": "Wal",
"WhaleJobScout": "Recherche",
"WhaleJobPatch": "Programmierung",
"WhaleJobHarbor": "Koordination",
"WhaleJobEcho": "Kommunikation",
"WhaleJobKeel": "Betrieb",
"WhaleJobLantern": "Review",
"WhaleJobPlain": "allgemeine Arbeit",
"SessionMetricsTurn": "Runde",
"SessionMetricsTurns": "Runden",
"SessionMetricsStep": "Schritt",
"SessionMetricsSteps": "Schritte",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Toolaufruf",
"SessionMetricsTtft": "TTFT Ø",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache-Hit",
"SessionMetricsInput": "Eingabe",
"SessionMetricsStatusLine": "Sitzungsmetriken: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review hat '{tool}' erlaubt (Risiko {risk}, Modell-Guardian): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review hat '{tool}' abgelehnt (Risiko {risk}, Modell-Guardian): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review konnte '{tool}' nicht prüfen ({reason}); abgelehnt, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review hat '{tool}' blockiert (deterministische Richtlinie): {reason}",
"AutoReviewReceiptHeld": "Auto-Review hat '{tool}' ohne Pause zurückgehalten; abgelehnt (braucht eine Person — zu Ask wechseln)",
"FooterHintEscInterrupt": "Esc zum Unterbrechen",
"PermissionsPostureHeader": "Aktuelle Berechtigungshaltung: {posture}",
"PermissionsPostureAsk": "Ask: Tool-Aufrufe, die Befugnis, Kosten, Umfang oder Ergebnis ändern, öffnen eine Abfrage; nachweislich sichere Nur-Lese-Aufrufe laufen ohne. Die ask-Regeln oben erzwingen immer eine Abfrage.",
"PermissionsPostureAuto": "Auto-Review: öffnet nie eine Abfrage. Eine deterministische Richtlinie erlaubt nachweislich sichere Aufrufe und blockiert veröffentlichungsartige oder destruktive Hintergrundarbeit hart; Aufrufe, die sie nicht als sicher beweisen kann, gehen an einen einmaligen Modell-Guardian, der mit Begründung erlaubt oder ablehnt (hohes oder kritisches Risiko läuft nie automatisch; eine fehlgeschlagene Prüfung lehnt ab, fail closed). Zurückhaltungen, die eine Person erfordern, werden abgelehnt, nicht versteckt. Jede solche Entscheidung wird als Notiz ins Transkript und ins Audit-Log geschrieben.",
"PermissionsPostureBypass": "Full Access: gewöhnliche Tool-Aufrufe laufen ohne Abfragen. Nicht umgehbare Zurückhaltungen aus Sicherheit, Repository-Recht und verwalteter Richtlinie schlagen als harte Blockaden fail closed fehl, statt zu fragen.",
"PermissionsPostureNever": "never: nur als sicher/nur-lesend eingestufte Tools laufen; alles andere wird ohne Abfrage blockiert.",
"PermissionsReceiptsNote": "Entscheidungen ohne Abfrage (Auto-Review-Guardian-Urteile, Blockaden und Zurückhaltungen) erscheinen als Notizen im Transkript und im Audit-Log unter {audit_path}. Full Access wird bewusst mit Shift+Tab oder /config gewählt, nie durch eine Regel.",
"AgentFocusOpened": "Fokus auf {agent}. Deine Nachrichten gehen jetzt an diesen Worker; Esc kehrt zur Hauptunterhaltung zurück.",
"AgentFocusClosed": "Zurück in der Hauptunterhaltung.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Nachricht an {agent} · Esc zurück zur Hauptunterhaltung",
"AgentFocusNoTranscript": "Für {agent} liegt noch kein Transkript vor. Nachrichten erscheinen hier, sobald der Worker sie austauscht.",
"AgentFocusOmitted": "Frühere Nachrichten ({count}) fehlen im Transkript im Arbeitsspeicher.",
"AgentFocusFollowUpDelivered": "Für {agent} eingereiht: er liest die Nachricht in seiner nächsten Runde.",
"AgentFocusFollowUpQueued": "Für {agent} eingereiht",
"AgentFocusFollowUpContinued": "{agent} war bereits fertig; auf einem neuen Fork ({target}) fortgesetzt. Diese Ansicht folgt jetzt dem Fork.",
"AgentFocusFollowUpFailed": "Zustellung an {agent} nicht möglich: {reason}",
"FooterHintForAgents": "Agenten",
"FooterHintToManage": "verwalten",
"AgentRailQueuedCount": "{count} eingereiht",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "schreibt",
"AgentFocusPostureReadOnly": "nur lesen",
"AgentFocusPostureNetwork": "Netzwerk",
"AgentFocusPostureNoNetwork": "kein Netzwerk",
"AgentFocusPostureShellFull": "Shell",
"AgentFocusPostureShellReadOnly": "Nur-Lese-Shell",
"AgentFocusPostureShellNone": "keine Shell",
"GoalReceiptSet": "Ziel gesetzt: \"{objective}\" · /goal zeigt den Fortschritt · /goal pause oder /goal clear beendet es",
"GoalStatusIdleHint": "läuft gerade nicht — Nachricht senden oder /goal resume zum Fortsetzen"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Write a task or use /.",
"ComposerOperatePlaceholder": "Describe the goal — Codewhale keeps working until it's done",
"ComposerDispatchFailedRestored": "Message not sent ({error}); restored to composer.",
"DispatchFailedQueued": "Dispatch failed ({error}); kept {count} queued follow-up(s).",
"DispatchFailedInitial": "Initial prompt could not be sent: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Active provider",
"ConfigLabelBaseUrlDeepseek": "Provider API URL (DeepSeek route)",
"ConfigLabelProviderUrl": "Provider API URL",
"ConfigHintProviderUrl": "Current provider endpoint; Xiaomi: token plan | pay as you go | custom URL",
"ConfigLabelModel": "Active provider model",
"ConfigLabelFastModel": "Fast model (derived)",
"ConfigLabelDefaultModel": "Legacy fallback model (DeepSeek routes only)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "per turn · local heuristic · no router request",
"ModelPickerAutoLastRoute": "last {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} via {source} · Ctrl+O: route details",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code cannot safely send this turn because this wire does not support system instructions yet. Nothing was sent; choose another provider.",
"HelpTitle": "Help",
"HelpFilterPlaceholder": "Type to filter",
"HelpFilterPrefix": "Filter: ",
@@ -252,7 +255,46 @@
"CmdLoadDescription": "Load session from file",
"CmdLogoutDescription": "Clear API key and return to setup",
"CmdMcpDescription": "Open or manage MCP servers",
"McpRecommendedUnknownId": "Unknown recommended MCP ID. Run {recommendations_command} to inspect the curated list.",
"McpRecommendationsSafety": "Viewing this list adds or enables nothing. An explicit add writes config only; review it before {restart_command} connects the server.",
"McpRecommendationGithub": "• github — GitHub's official remote MCP endpoint\n endpoint: {endpoint}\n auth is separate: use {login_command} only when the server advertises OAuth;\n otherwise configure a least-privilege PAT outside command history. Granted\n scopes may write or delete repository data, so start read-only where possible.\n add explicitly: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — official Chrome DevTools MCP via pinned npm package\n package: {package} ({launcher})\n it can inspect/control Chrome and read authenticated pages. Close sensitive\n tabs and verify the package before adding; {restart_command} may download and run it.\n add explicitly: {add_command}",
"CmdPluginDescription": "Inspect and manage trusted plugin bundles; legacy executable tools stay separate",
"PluginKimiUsage": "Usage:\n {list_command}\n {approve_command}\nListing is read-only. Approval copies one canonical Kimi-managed plugin through the reviewed installer; it remains disabled and untrusted.",
"PluginKimiManagedRootHeading": "Kimi-managed plugins at {root}:",
"PluginKimiNoneFound": "No valid managed plugins found.",
"PluginKimiLicenseUnspecified": "unspecified",
"PluginKimiApplicable": "applicable on this OS",
"PluginKimiNotApplicable": "not applicable on this OS",
"PluginKimiCandidateSummary": "{name} {version} — license={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " path: {path}\n content hash: {content_hash}\n capability hash: {capability_hash}\n approve: {approve_command}",
"PluginKimiRejectedHeading": "Rejected entries (not importable):",
"PluginKimiInspectionFooter": "This inspection did not copy, trust, enable, or execute anything. External Kimi apps, daemons, binaries, browser extensions, credentials, and OS permissions were not checked.",
"PluginKimiCandidateMissing": "No valid canonical Kimi-managed plugin named `{name}`. Run {list_command} again.",
"PluginKimiCandidateChanged": "Kimi-managed plugin `{name}` changed since review. Expected content hash {expected}, now {actual}. Nothing was copied; run {list_command} again.",
"PluginKimiHomeMissing": "Cannot locate the user home directory for Kimi import.",
"PluginKimiRootInspectFailed": "Cannot inspect Kimi managed plugin root {root}: {error}",
"PluginKimiRootMustBeDirectory": "Kimi managed plugin root {root} must be a real directory, not a link or reparse point.",
"PluginKimiRootCanonicalizeFailed": "Cannot canonicalize Kimi managed plugin root {root}: {error}",
"PluginKimiRootListFailed": "Cannot list Kimi managed plugin root {root}: {error}",
"PluginKimiEntryReadFailed": "Cannot read a Kimi managed plugin entry: {error}",
"PluginKimiEntryLimit": "Kimi managed plugin root contains {count} entries; the maximum reviewed in one scan is {max}.",
"PluginKimiEntryInspectFailed": "{path}: cannot inspect: {error}",
"PluginKimiEntryLinksRefused": "{path}: links and reparse points are refused",
"PluginKimiEntryOutsideRoot": "{path}: canonical path {canonical_path} is not an immediate child of the managed root",
"PluginKimiEntryCanonicalizeFailed": "{path}: cannot canonicalize: {error}",
"PluginKimiManifestUnreadable": "{path}: no readable {manifest}: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} must be a real regular file",
"PluginKimiManifestInvalid": "{path}: invalid manifest: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: directory name must exactly match manifest name `{name}`",
"PluginKimiHashUnavailable": "unavailable",
"PluginKimiRollbackDestinationMissing": "The installer did not report the destination path.",
"PluginKimiMismatchRemoved": "Plugin `{name}` did not match the approved content after copying (expected {expected}, found {actual}). The unexpected copy was removed; review and retry.",
"PluginKimiMismatchRollbackFailed": "Error: Plugin `{name}` did not match the approved content after copying (expected {expected}, found {actual}), and automatic removal failed: {error}. It remains disabled and untrusted; inspect {path} before taking further action.",
"PluginKimiUserPluginDirectory": "the user plugin directory",
"PluginKimiMarketplaceZipUnsupported": "Kimi ZIP bundles are not supported by Codewhale's reviewed installer; install from a local directory or import an upstream Kimi-managed plugin.",
"PluginKimiMarketplaceRemoteUnsupported": "Kimi remote sources must end in .tar.gz or .tgz for Codewhale installation; .zip is recognized but not supported.",
"PluginKimiMarketplaceGzipTarball": "gzip tarball URL",
"CmdPluginBundleUsage": "Usage: /plugin [list|show <name>|validate [name]|export <name> <dir>|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "No Codewhale plugin bundles discovered.",
"CmdPluginBundleListHeader": "Plugin bundles ({count}):",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale does not upload, migrate, or sync local source into hosted Work. Use {command} to start from the branch tip available at GitHub or CNB. Unpushed commits, dirty or ignored files, secrets, and session state stay local.",
"CmdRemoteEnvBrowserLabel": "Codewhale hosted Work",
"CmdRenameDescription": "Rename the current session",
"CmdTitleDescription": "Name the current session and its terminal tab/window",
"CmdRestoreDescription": "Roll back the workspace to a prior pre/post-turn snapshot. With no arg, lists recent snapshots.",
"CmdRetryDescription": "Retry the last request",
"CmdReviewDescription": "Run a structured code review on a file, diff, or PR",
@@ -992,6 +1035,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP still needs action; recorded for setup report (does not block first-run).",
"SetupToolsMcpPreviewTitle": "Tools / MCP safe on-ramps",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills & Plugins — Safe On-Ramps\n\n/setup only reads local inventory. It never starts MCP servers, installs skills, runs plugins, or executes untrusted commands.\n\nCurrent inventory:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools dir: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (shared adapters): {hotbar_result}\n\nPaths (redacted home):\n- MCP config: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nSafe bootstrap (run yourself in a normal terminal or TUI command):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills · /skills install <spec>\n- Plugins: /plugin · codewhale setup --plugins\n- Tools dir: codewhale setup --tools\n\nSide-effectful actions always require explicit confirmation. Plugin commands stay distinct from slash commands; the Hotbar plugin source remains deferred until approval gates land.\n\nSee docs/MCP.md and docs/skills/README.md for what still needs manual external setup.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — connected through Codewhale, never a second scheduler:\n- State: {dsh_result}\n- Read-only detection; connect/plan/launch/remove: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale writes only $CODEWHALE_HOME/integrations/dsh; it never copies API keys or edits DSH files.",
"HotbarActionModeOperateName": "Operate mode",
"HotbarActionModeOperateDescription": "Put your Fleet to work in parallel.",
"HomeOperateModeTip": "Operate — put your Fleet to work in parallel",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet ready",
"EmptyStateHelpConnector": "or",
"EmptyStateHelpHint": "— see everything",
"SessionsSurfaceTitle": "sessions",
"SessionsPaneTitle": " sessions (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "Could not verify existing profile identities ({error}); fix the named file before saving.",
"FleetProfileIdConflict": "Profile id `{id}` is already used by {path}; redraft with a different role or remove the old file first.",
"FleetProfileProviderUnconfigured": "Profile pins provider `{provider}`, which has no configured credentials ({env}); set it up in /provider before saving.",
"FleetDestStepTitle": "Where should this profile live?",
"FleetDestStepSubtitle": "Nothing is written until you confirm on the last step.",
"FleetDestProjectLabel": "This project",
"FleetDestPersonalLabel": "Personal",
"FleetDestProjectSummary": "Only this project",
"FleetDestPersonalSummary": "Available in every project",
"FleetDestProjectDescription": "Saved inside this project ({workspace}). It applies here only, and it takes precedence over a Personal profile with the same ID.",
"FleetDestPersonalDescription": "Saved in your Codewhale home. It applies in every project — except where a project has its own profile with the same ID, which takes precedence there.",
"FleetDestPathLine": "File: {path}",
"FleetDestUnavailable": "Not available: {reason}",
"FleetDestReasonNoProjectConfig": "project profiles are disabled for this session (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "the workspace folder {path} does not exist or is not a directory",
"FleetDestReasonHomeUnavailable": "your Codewhale home could not be resolved ({error})",
"FleetDestWillReplace": "Will replace the existing file {path}",
"FleetDestOverridesProject": "This project already has a '{id}' profile, which takes precedence here; this Personal profile applies in other projects.",
"FleetDestOverridesPersonal": "Takes precedence over your Personal '{id}' profile inside this project.",
"FleetDestOverridesBuiltIn": "Replaces the {origin} '{id}' role in the roster.",
"FleetSavesToChip": "Saves to: {scope} · {path}",
"FleetSavesToUndecided": "Saves to: choose in step 3 — This project or Personal",
"FleetActionSaveProject": "Save to this project",
"FleetActionSavePersonal": "Save as Personal profile",
"FleetActionReplaceProject": "Replace in this project",
"FleetActionReplacePersonal": "Replace Personal profile",
"FleetActionConfirmReplace": "Press Enter again to replace {file}",
"FleetActionChangeDestination": "Change destination",
"FleetActionBack": "Back",
"FleetReviewSavesTo": "Saves to",
"FleetModelRowBlockedNotice": "Not selectable: {reason}. Configure it in /provider or pick another row.",
"FleetDestProjectDisabledSave": "Project profiles are disabled for this session (--no-project-config); nothing was saved. Choose Personal or restart without the flag.",
"WorkflowStatusWaiting": "waiting",
"WorkflowDebrief": "debrief: {done}/{total} settled · {failed} failed · {cancelled} cancelled · {elapsed}",
"WorkflowTranscriptDetails": "transcript: full run JSON available in tool details ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Automation {id} run enqueued: {status} (task {task})",
"AutomationDeletePreview": "Deletion is not armed. Nothing was deleted.\nAutomation: {id} ({name})\nRecorded runs: {run_count}\nTo delete the definition and run history, run:\n{command}",
"AutomationDeleteConfirmationStale": "Deletion confirmation no longer matches automation {id}; nothing was deleted. Review the current state with {command}.",
"AutomationDeleted": "Deleted automation {id} ({name}). Recorded runs deleted: {run_count}."
"AutomationDeleted": "Deleted automation {id} ({name}). Recorded runs deleted: {run_count}.",
"WhaleStateResting": "Resting",
"WhaleStateThinking": "Thinking",
"WhaleStateWorking": "Working",
"WhaleStateWaiting": "Waiting for you",
"WhaleStateBlocked": "Blocked",
"WhaleStateOffline": "Offline",
"WhaleAnimalScout": "beaked whale",
"WhaleAnimalPatch": "harbor porpoise",
"WhaleAnimalHarbor": "humpback whale",
"WhaleAnimalEcho": "pilot whale",
"WhaleAnimalKeel": "sperm whale",
"WhaleAnimalLantern": "orca",
"WhaleAnimalPlain": "whale",
"WhaleJobScout": "research",
"WhaleJobPatch": "coding",
"WhaleJobHarbor": "coordination",
"WhaleJobEcho": "communications",
"WhaleJobKeel": "operations",
"WhaleJobLantern": "review",
"WhaleJobPlain": "general work",
"SessionMetricsTurn": "turn",
"SessionMetricsTurns": "turns",
"SessionMetricsStep": "step",
"SessionMetricsSteps": "steps",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Tool call",
"SessionMetricsTtft": "TTFT avg",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache hit",
"SessionMetricsInput": "Input",
"SessionMetricsStatusLine": "Session metrics: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review allowed '{tool}' ({risk} risk, model guardian): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review denied '{tool}' ({risk} risk, model guardian): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review could not review '{tool}' ({reason}); denied, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review blocked '{tool}' (deterministic policy): {reason}",
"AutoReviewReceiptHeld": "Auto-Review held '{tool}' without pausing; denied (needs a person — switch to Ask)",
"FooterHintEscInterrupt": "Esc to interrupt",
"PermissionsPostureHeader": "Permission posture now: {posture}",
"PermissionsPostureAsk": "Ask: tool calls that change authority, cost, scope, or outcome open a prompt; proven-safe read-only calls run without one. Ask rules above always force a prompt.",
"PermissionsPostureAuto": "Auto-Review: never opens a prompt. A deterministic policy allows proven-safe calls and hard-blocks publish-like or destructive background work; calls it cannot prove safe go to a one-shot model guardian that allows or denies with a stated reason (high or critical risk never auto-runs; a failed review denies, fail closed). Holds that require a person are denied, not hidden. Each such decision is written to the transcript as a note and to the audit log.",
"PermissionsPostureBypass": "Full Access: ordinary tool calls run without prompts. Non-bypassable safety, repository-law, and managed-policy holds fail closed as hard blocks instead of prompting.",
"PermissionsPostureNever": "never: only tools considered safe/read-only run; everything else is blocked without a prompt.",
"PermissionsReceiptsNote": "Decisions made without a prompt (Auto-Review guardian verdicts, blocks, and holds) appear as transcript notes and in the audit log at {audit_path}. Full Access is chosen deliberately with Shift+Tab or /config, never by a rule.",
"AgentFocusOpened": "Focused on {agent}. Your messages now go to this worker; Esc returns to the main conversation.",
"AgentFocusClosed": "Back to the main conversation.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Message {agent} · Esc returns to main",
"AgentFocusNoTranscript": "No transcript captured yet for {agent}. Messages appear here as the worker exchanges them.",
"AgentFocusOmitted": "Earlier messages ({count}) are omitted from the in-memory transcript.",
"AgentFocusFollowUpDelivered": "Queued for {agent}: it reads the message at its next round.",
"AgentFocusFollowUpQueued": "Queued for {agent}",
"AgentFocusFollowUpContinued": "{agent} had finished; continued on a new fork ({target}). This view now follows the fork.",
"AgentFocusFollowUpFailed": "Could not deliver to {agent}: {reason}",
"FooterHintForAgents": "for agents",
"FooterHintToManage": "to manage",
"AgentRailQueuedCount": "{count} queued",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "writes",
"AgentFocusPostureReadOnly": "read-only",
"AgentFocusPostureNetwork": "network",
"AgentFocusPostureNoNetwork": "no network",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "read-only shell",
"AgentFocusPostureShellNone": "no shell",
"GoalReceiptSet": "Goal set: \"{objective}\" · /goal shows progress · /goal pause or /goal clear stops it",
"GoalStatusIdleHint": "not running now — send a message or /goal resume to continue"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Escribe una tarea o usa /.",
"ComposerOperatePlaceholder": "Describe el objetivo — Codewhale seguirá trabajando hasta terminarlo",
"ComposerDispatchFailedRestored": "No se envió el mensaje ({error}); se restauró en el editor.",
"DispatchFailedQueued": "Error al enviar ({error}); se mantuvieron {count} mensajes de seguimiento en cola.",
"DispatchFailedInitial": "No se pudo enviar el prompt inicial: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Proveedor activo",
"ConfigLabelBaseUrlDeepseek": "URL de API del proveedor (ruta DeepSeek)",
"ConfigLabelProviderUrl": "URL de API del proveedor",
"ConfigHintProviderUrl": "Endpoint actual del proveedor; Xiaomi: plan de tokens | pago por uso | URL personalizada",
"ConfigLabelModel": "Modelo activo del proveedor",
"ConfigLabelFastModel": "Modelo rápido (derivado)",
"ConfigLabelDefaultModel": "Modelo alternativo heredado (solo rutas DeepSeek)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "por turno · heurística local · sin solicitud al enrutador",
"ModelPickerAutoLastRoute": "última {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} mediante {source} · Ctrl+O: detalles de la ruta",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code no puede enviar este turno de forma segura porque esta conexión aún no admite instrucciones del sistema. No se envió nada; elige otro proveedor.",
"HelpTitle": "Ayuda",
"HelpFilterPlaceholder": "Escribe para filtrar",
"HelpFilterPrefix": "Filtro: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "Cargar la sesión desde un archivo",
"CmdLogoutDescription": "Limpiar la clave de API y volver a la configuración",
"CmdMcpDescription": "Abrir o gestionar servidores MCP",
"McpRecommendedUnknownId": "ID de MCP recomendado desconocido. Ejecuta {recommendations_command} para revisar la lista seleccionada.",
"McpRecommendationsSafety": "Ver esta lista no agrega ni habilita nada. Agregar algo explícitamente solo escribe la configuración; revísala antes de que {restart_command} conecte el servidor.",
"McpRecommendationGithub": "• github — endpoint MCP remoto oficial de GitHub\n endpoint: {endpoint}\n la autenticación es aparte: usa {login_command} solo si el servidor anuncia OAuth;\n de lo contrario, configura un PAT con privilegios mínimos fuera del historial de comandos. Los\n permisos concedidos pueden escribir o borrar datos del repositorio; empieza en modo de solo lectura cuando sea posible.\n agregar explícitamente: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial de Chrome DevTools mediante un paquete npm fijado\n paquete: {package} ({launcher})\n puede inspeccionar o controlar Chrome y leer páginas autenticadas. Cierra las pestañas\n sensibles y verifica el paquete antes de agregarlo; {restart_command} puede descargarlo y ejecutarlo.\n agregar explícitamente: {add_command}",
"PluginKimiUsage": "Uso:\n {list_command}\n {approve_command}\nLa lista es de solo lectura. La aprobación copia un plugin canónico gestionado por Kimi mediante el instalador revisado; permanece deshabilitado y no confiable.",
"PluginKimiManagedRootHeading": "Plugins gestionados por Kimi en {root}:",
"PluginKimiNoneFound": "No se encontraron plugins gestionados válidos.",
"PluginKimiLicenseUnspecified": "sin especificar",
"PluginKimiApplicable": "compatible con este SO",
"PluginKimiNotApplicable": "no compatible con este SO",
"PluginKimiCandidateSummary": "{name} {version} — licencia={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " ruta: {path}\n hash de contenido: {content_hash}\n hash de capacidades: {capability_hash}\n aprobar: {approve_command}",
"PluginKimiRejectedHeading": "Entradas rechazadas (no importables):",
"PluginKimiInspectionFooter": "Esta inspección no copió, marcó como confiable, habilitó ni ejecutó nada. No se comprobaron apps, demonios, binarios, extensiones del navegador, credenciales ni permisos del SO externos de Kimi.",
"PluginKimiCandidateMissing": "No hay un plugin canónico válido gestionado por Kimi llamado `{name}`. Ejecuta {list_command} de nuevo.",
"PluginKimiCandidateChanged": "El plugin gestionado por Kimi `{name}` cambió desde la revisión. Se esperaba el hash {expected} y ahora es {actual}. No se copió nada; ejecuta {list_command} de nuevo.",
"PluginKimiHomeMissing": "No se puede localizar la carpeta personal del usuario para importar desde Kimi.",
"PluginKimiRootInspectFailed": "No se puede inspeccionar la raíz de plugins gestionados por Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "La raíz de plugins gestionados por Kimi {root} debe ser una carpeta real, no un enlace ni un punto de reanálisis.",
"PluginKimiRootCanonicalizeFailed": "No se puede canonizar la raíz de plugins gestionados por Kimi {root}: {error}",
"PluginKimiRootListFailed": "No se puede listar la raíz de plugins gestionados por Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "No se puede leer una entrada de plugin gestionado por Kimi: {error}",
"PluginKimiEntryLimit": "La raíz de plugins gestionados por Kimi contiene {count} entradas; el máximo revisado por análisis es {max}.",
"PluginKimiEntryInspectFailed": "{path}: no se puede inspeccionar: {error}",
"PluginKimiEntryLinksRefused": "{path}: se rechazan enlaces y puntos de reanálisis",
"PluginKimiEntryOutsideRoot": "{path}: la ruta canónica {canonical_path} no es hija directa de la raíz gestionada",
"PluginKimiEntryCanonicalizeFailed": "{path}: no se puede canonizar: {error}",
"PluginKimiManifestUnreadable": "{path}: no hay un {manifest} legible: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} debe ser un archivo normal real",
"PluginKimiManifestInvalid": "{path}: manifiesto no válido: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: el nombre de la carpeta debe coincidir exactamente con el nombre `{name}` del manifiesto",
"PluginKimiHashUnavailable": "no disponible",
"PluginKimiRollbackDestinationMissing": "El instalador no informó la ruta de destino.",
"PluginKimiMismatchRemoved": "El contenido copiado del plugin `{name}` no coincide con el aprobado (se esperaba {expected}, se encontró {actual}). Se eliminó la copia inesperada; revísalo e inténtalo de nuevo.",
"PluginKimiMismatchRollbackFailed": "Error: el contenido copiado del plugin `{name}` no coincide con el aprobado (se esperaba {expected}, se encontró {actual}) y falló la eliminación automática: {error}. Permanece deshabilitado y no confiable; inspecciona {path} antes de continuar.",
"PluginKimiUserPluginDirectory": "la carpeta de plugins del usuario",
"PluginKimiMarketplaceZipUnsupported": "El instalador revisado de Codewhale no admite paquetes ZIP de Kimi; instala desde una carpeta local o importa un plugin gestionado por Kimi ascendente.",
"PluginKimiMarketplaceRemoteUnsupported": "Las fuentes remotas de Kimi deben terminar en .tar.gz o .tgz para instalarlas con Codewhale; se reconoce .zip, pero no se admite.",
"PluginKimiMarketplaceGzipTarball": "URL de tarball gzip",
"CmdPluginDescription": "Inspeccionar y administrar paquetes de plugins confiables; las herramientas ejecutables heredadas permanecen separadas",
"CmdPluginBundleUsage": "Uso: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "No se encontraron paquetes de plugins de Codewhale.",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale no carga, migra ni sincroniza el código fuente local con el Work alojado. Usa {command} para iniciar desde la punta de la rama disponible en GitHub o CNB. Los commits sin enviar, los archivos modificados o ignorados, los secretos y el estado de la sesión permanecen locales.",
"CmdRemoteEnvBrowserLabel": "Work alojado de Codewhale",
"CmdRenameDescription": "Renombrar la sesión actual",
"CmdTitleDescription": "Nombrar la sesión actual y su pestaña/ventana de terminal",
"CmdRestoreDescription": "Revertir el workspace a un snapshot pre/post-turno anterior. Sin argumento, lista los snapshots recientes.",
"CmdRetryDescription": "Repetir la última solicitud",
"CmdReviewDescription": "Ejecutar una revisión de código estructurada en un archivo, diff o PR",
@@ -990,6 +1033,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP aún necesita acción; se registró en el informe (no bloquea el primer uso).",
"SetupToolsMcpPreviewTitle": "On-ramps seguros de Tools / MCP",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills y Plugins — On-ramps seguros\n\n/setup solo lee el inventario local. No inicia servidores MCP, no instala skills, no ejecuta plugins ni comandos no confiables.\n\nInventario actual:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (adaptadores compartidos): {hotbar_result}\n\nRutas:\n- MCP: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nBootstrap seguro (ejecútalo tú):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills\n- Plugins: /plugin · codewhale setup --plugins\n- Tools: codewhale setup --tools\n\nLas acciones con efectos secundarios siempre requieren confirmación explícita. Ver docs/MCP.md.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — conectado a través de Codewhale, nunca un segundo planificador:\n- Estado: {dsh_result}\n- Detección de solo lectura; conectar/planear/iniciar/quitar: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale solo escribe en $CODEWHALE_HOME/integrations/dsh; nunca copia claves de API ni edita archivos de DSH.",
"HotbarActionModeOperateName": "Modo Operate",
"HotbarActionModeOperateDescription": "Pon tu Fleet a trabajar en paralelo.",
"HomeOperateModeTip": "Operate — pon tu Fleet a trabajar en paralelo",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet lista",
"EmptyStateHelpConnector": "o",
"EmptyStateHelpHint": "— ver todo",
"SessionsSurfaceTitle": "sesiones",
"SessionsPaneTitle": " sesiones (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "No se pudieron verificar las identidades de perfiles existentes ({error}); corrige el archivo indicado antes de guardar.",
"FleetProfileIdConflict": "El id de perfil `{id}` ya está en uso por {path}; redacta de nuevo con otro rol o elimina primero el archivo antiguo.",
"FleetProfileProviderUnconfigured": "El perfil fija el proveedor `{provider}`, que no tiene credenciales configuradas ({env}); configúralo en /provider antes de guardar.",
"FleetDestStepTitle": "¿Dónde debe guardarse este perfil?",
"FleetDestStepSubtitle": "No se escribe nada hasta que confirmes en el último paso.",
"FleetDestProjectLabel": "Este proyecto",
"FleetDestPersonalLabel": "Personal",
"FleetDestProjectSummary": "Solo este proyecto",
"FleetDestPersonalSummary": "Disponible en todos los proyectos",
"FleetDestProjectDescription": "Se guarda dentro de este proyecto ({workspace}). Aplica solo aquí y tiene prioridad sobre un perfil Personal con el mismo ID.",
"FleetDestPersonalDescription": "Se guarda en tu directorio de Codewhale. Aplica en todos los proyectos, salvo donde un proyecto tenga su propio perfil con el mismo ID, que tiene prioridad allí.",
"FleetDestPathLine": "Archivo: {path}",
"FleetDestUnavailable": "No disponible: {reason}",
"FleetDestReasonNoProjectConfig": "los perfiles de proyecto están desactivados en esta sesión (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "la carpeta del espacio de trabajo {path} no existe o no es un directorio",
"FleetDestReasonHomeUnavailable": "no se pudo resolver tu directorio de Codewhale ({error})",
"FleetDestWillReplace": "Reemplazará el archivo existente {path}",
"FleetDestOverridesProject": "Este proyecto ya tiene un perfil '{id}', que tiene prioridad aquí; este perfil Personal aplica en los demás proyectos.",
"FleetDestOverridesPersonal": "Tiene prioridad sobre tu perfil Personal '{id}' dentro de este proyecto.",
"FleetDestOverridesBuiltIn": "Reemplaza el rol {origin} '{id}' de la plantilla.",
"FleetSavesToChip": "Se guarda en: {scope} · {path}",
"FleetSavesToUndecided": "Se guarda en: elige en el paso 3 — Este proyecto o Personal",
"FleetActionSaveProject": "Guardar en este proyecto",
"FleetActionSavePersonal": "Guardar como perfil Personal",
"FleetActionReplaceProject": "Reemplazar en este proyecto",
"FleetActionReplacePersonal": "Reemplazar perfil Personal",
"FleetActionConfirmReplace": "Presiona Enter otra vez para reemplazar {file}",
"FleetActionChangeDestination": "Cambiar destino",
"FleetActionBack": "Atrás",
"FleetReviewSavesTo": "Se guarda en",
"FleetModelRowBlockedNotice": "No seleccionable: {reason}. Configúralo en /provider o elige otra fila.",
"FleetDestProjectDisabledSave": "Los perfiles de proyecto están desactivados en esta sesión (--no-project-config); no se guardó nada. Elige Personal o reinicia sin la opción.",
"WorkflowStatusWaiting": "esperando",
"WorkflowDebrief": "resumen: {done}/{total} resueltos · {failed} fallidos · {cancelled} cancelados · {elapsed}",
"WorkflowTranscriptDetails": "transcripción: JSON completo disponible en los detalles de herramienta ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Ejecución de la automatización {id} puesta en cola: {status} (tarea {task})",
"AutomationDeletePreview": "La eliminación aún no está confirmada. No se eliminó nada.\nAutomatización: {id} ({name})\nEjecuciones registradas: {run_count}\nPara eliminar la definición y el historial de ejecuciones, ejecuta:\n{command}",
"AutomationDeleteConfirmationStale": "La confirmación de eliminación ya no coincide con la automatización {id}; no se eliminó nada. Revisa el estado actual con {command}.",
"AutomationDeleted": "Se eliminó la automatización {id} ({name}). Ejecuciones registradas eliminadas: {run_count}."
"AutomationDeleted": "Se eliminó la automatización {id} ({name}). Ejecuciones registradas eliminadas: {run_count}.",
"WhaleStateResting": "Descansando",
"WhaleStateThinking": "Pensando",
"WhaleStateWorking": "Trabajando",
"WhaleStateWaiting": "Esperándote",
"WhaleStateBlocked": "Bloqueada",
"WhaleStateOffline": "Sin conexión",
"WhaleAnimalScout": "zifio",
"WhaleAnimalPatch": "marsopa común",
"WhaleAnimalHarbor": "ballena jorobada",
"WhaleAnimalEcho": "calderón",
"WhaleAnimalKeel": "cachalote",
"WhaleAnimalLantern": "orca",
"WhaleAnimalPlain": "ballena",
"WhaleJobScout": "investigación",
"WhaleJobPatch": "programación",
"WhaleJobHarbor": "coordinación",
"WhaleJobEcho": "comunicaciones",
"WhaleJobKeel": "operaciones",
"WhaleJobLantern": "revisión",
"WhaleJobPlain": "trabajo general",
"SessionMetricsTurn": "turno",
"SessionMetricsTurns": "turnos",
"SessionMetricsStep": "paso",
"SessionMetricsSteps": "pasos",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Herram.",
"SessionMetricsTtft": "TTFT prom.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Caché",
"SessionMetricsInput": "Entrada",
"SessionMetricsStatusLine": "Métricas de la sesión: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review permitió '{tool}' (riesgo {risk}, guardián del modelo): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review denegó '{tool}' (riesgo {risk}, guardián del modelo): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review no pudo revisar '{tool}' ({reason}); denegado, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review bloqueó '{tool}' (política determinista): {reason}",
"AutoReviewReceiptHeld": "Auto-Review retuvo '{tool}' sin pausar; denegado (requiere una persona — cambia a Ask)",
"FooterHintEscInterrupt": "Esc para interrumpir",
"PermissionsPostureHeader": "Postura de permisos actual: {posture}",
"PermissionsPostureAsk": "Ask: las llamadas a tool que cambian autoridad, costo, alcance o resultado abren un aviso; las llamadas de solo lectura comprobadamente seguras corren sin él. Las reglas ask de arriba siempre fuerzan un aviso.",
"PermissionsPostureAuto": "Auto-Review: nunca abre un aviso. Una política determinista permite llamadas comprobadamente seguras y bloquea de forma estricta trabajo de publicación o destructivo en segundo plano; las llamadas que no puede probar seguras van a un guardián de modelo de una sola pasada, que permite o deniega con un motivo declarado (riesgo alto o crítico nunca se ejecuta automáticamente; una revisión fallida deniega, fail closed). Las retenciones que requieren una persona se deniegan, no se ocultan. Cada decisión así se escribe en la transcripción como nota y en el registro de auditoría.",
"PermissionsPostureBypass": "Full Access: las llamadas a tool comunes corren sin avisos. Las retenciones no eludibles de seguridad, ley del repositorio y política administrada hacen fail closed como bloqueos estrictos en lugar de preguntar.",
"PermissionsPostureNever": "never: solo corren las tools consideradas seguras/solo lectura; todo lo demás se bloquea sin aviso.",
"PermissionsReceiptsNote": "Las decisiones tomadas sin aviso (veredictos del guardián de Auto-Review, bloqueos y retenciones) aparecen como notas en la transcripción y en el registro de auditoría en {audit_path}. Full Access se elige deliberadamente con Shift+Tab o /config, nunca por una regla.",
"AgentFocusOpened": "Enfocado en {agent}. Tus mensajes ahora van a este worker; Esc vuelve a la conversación principal.",
"AgentFocusClosed": "De vuelta a la conversación principal.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Mensaje para {agent} · Esc vuelve al principal",
"AgentFocusNoTranscript": "Aún no hay transcripción de {agent}. Los mensajes aparecen aquí a medida que el worker los intercambia.",
"AgentFocusOmitted": "Los mensajes anteriores ({count}) se omitieron de la transcripción en memoria.",
"AgentFocusFollowUpDelivered": "En cola para {agent}: leerá el mensaje en su próxima ronda.",
"AgentFocusFollowUpQueued": "En cola para {agent}",
"AgentFocusFollowUpContinued": "{agent} ya había terminado; continuó en un nuevo fork ({target}). Esta vista ahora sigue el fork.",
"AgentFocusFollowUpFailed": "No se pudo entregar a {agent}: {reason}",
"FooterHintForAgents": "agentes",
"FooterHintToManage": "gestionar",
"AgentRailQueuedCount": "{count} en cola",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "escribe",
"AgentFocusPostureReadOnly": "solo lectura",
"AgentFocusPostureNetwork": "red",
"AgentFocusPostureNoNetwork": "sin red",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell de solo lectura",
"AgentFocusPostureShellNone": "sin shell",
"GoalReceiptSet": "Meta definida: \"{objective}\" · /goal muestra el progreso · /goal pause o /goal clear la detiene",
"GoalStatusIdleHint": "no está en ejecución — envía un mensaje o /goal resume para continuar"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Écrivez une tâche ou utilisez /.",
"ComposerOperatePlaceholder": "Décrivez lobjectif — Codewhale continue jusqu’à ce quil soit atteint",
"ComposerDispatchFailedRestored": "Message non envoyé ({error}) ; restauré dans le composer.",
"DispatchFailedQueued": "Échec de l'envoi ({error}) ; {count} suivi(s) en file conservé(s).",
"DispatchFailedInitial": "Le prompt initial n'a pas pu être envoyé : {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Fournisseur actif",
"ConfigLabelBaseUrlDeepseek": "URL de l'API du fournisseur (route DeepSeek)",
"ConfigLabelProviderUrl": "URL de l'API du fournisseur",
"ConfigHintProviderUrl": "Endpoint actuel du fournisseur ; Xiaomi : forfait de jetons | paiement à lusage | URL personnalisée",
"ConfigLabelModel": "Modèle actif du fournisseur",
"ConfigLabelFastModel": "Modèle rapide (dérivé)",
"ConfigLabelDefaultModel": "Modèle de secours hérité (routes DeepSeek uniquement)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "par tour · heuristique locale · pas de requête au routeur",
"ModelPickerAutoLastRoute": "dernière {provider} · {model}",
"AutoRouteSelectedToast": "Auto : {provider} / {model} via {source} · Ctrl+O : détails de la route",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code ne peut pas envoyer ce tour en toute sécurité, car cette connexion ne prend pas encore en charge les instructions système. Rien na été envoyé ; choisissez un autre fournisseur.",
"HelpTitle": "Aide",
"HelpFilterPlaceholder": "Taper pour filtrer",
"HelpFilterPrefix": "Filtre : ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Charger une session depuis un fichier",
"CmdLogoutDescription": "Effacer la clé API et revenir à la configuration",
"CmdMcpDescription": "Ouvrir ou gérer les serveurs MCP",
"McpRecommendedUnknownId": "ID MCP recommandé inconnu. Exécutez {recommendations_command} pour consulter la liste sélectionnée.",
"McpRecommendationsSafety": "Consulter cette liste najoute ni nactive rien. Un ajout explicite écrit seulement la configuration ; vérifiez-la avant que {restart_command} connecte le serveur.",
"McpRecommendationGithub": "• github — point de terminaison MCP distant officiel de GitHub\n point de terminaison : {endpoint}\n lauthentification est séparée : utilisez {login_command} uniquement si le serveur annonce OAuth ;\n sinon, configurez hors de lhistorique un PAT aux privilèges minimaux. Les autorisations\n accordées peuvent écrire ou supprimer des données du dépôt ; commencez en lecture seule si possible.\n ajouter explicitement : {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools officiel via un paquet npm à version fixe\n paquet : {package} ({launcher})\n il peut inspecter/contrôler Chrome et lire des pages authentifiées. Fermez les onglets\n sensibles et vérifiez le paquet avant lajout ; {restart_command} peut le télécharger et lexécuter.\n ajouter explicitement : {add_command}",
"PluginKimiUsage": "Utilisation :\n {list_command}\n {approve_command}\nLa liste est en lecture seule. Lapprobation copie un plugin canonique géré par Kimi via linstallateur vérifié ; il reste désactivé et non approuvé.",
"PluginKimiManagedRootHeading": "Plugins gérés par Kimi dans {root} :",
"PluginKimiNoneFound": "Aucun plugin géré valide trouvé.",
"PluginKimiLicenseUnspecified": "non précisée",
"PluginKimiApplicable": "compatible avec cet OS",
"PluginKimiNotApplicable": "incompatible avec cet OS",
"PluginKimiCandidateSummary": "{name} {version} — licence={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " chemin : {path}\n hachage du contenu : {content_hash}\n hachage des capacités : {capability_hash}\n approuver : {approve_command}",
"PluginKimiRejectedHeading": "Entrées rejetées (non importables) :",
"PluginKimiInspectionFooter": "Cette inspection na rien copié, approuvé, activé ni exécuté. Les applications, démons, binaires, extensions de navigateur, identifiants et autorisations système externes de Kimi nont pas été vérifiés.",
"PluginKimiCandidateMissing": "Aucun plugin canonique valide géré par Kimi nommé `{name}`. Réexécutez {list_command}.",
"PluginKimiCandidateChanged": "Le plugin géré par Kimi `{name}` a changé depuis la vérification. Hachage attendu : {expected}, actuel : {actual}. Rien na été copié ; réexécutez {list_command}.",
"PluginKimiHomeMissing": "Impossible de trouver le dossier personnel pour limport Kimi.",
"PluginKimiRootInspectFailed": "Impossible dinspecter la racine des plugins Kimi {root} : {error}",
"PluginKimiRootMustBeDirectory": "La racine des plugins Kimi {root} doit être un vrai dossier, pas un lien ni un point danalyse.",
"PluginKimiRootCanonicalizeFailed": "Impossible de canoniser la racine des plugins Kimi {root} : {error}",
"PluginKimiRootListFailed": "Impossible de lister la racine des plugins Kimi {root} : {error}",
"PluginKimiEntryReadFailed": "Impossible de lire une entrée de plugin Kimi : {error}",
"PluginKimiEntryLimit": "La racine des plugins Kimi contient {count} entrées ; le maximum vérifié par analyse est {max}.",
"PluginKimiEntryInspectFailed": "{path} : inspection impossible : {error}",
"PluginKimiEntryLinksRefused": "{path} : les liens et points danalyse sont refusés",
"PluginKimiEntryOutsideRoot": "{path} : le chemin canonique {canonical_path} nest pas un enfant direct de la racine gérée",
"PluginKimiEntryCanonicalizeFailed": "{path} : canonisation impossible : {error}",
"PluginKimiManifestUnreadable": "{path} : aucun {manifest} lisible : {error}",
"PluginKimiManifestMustBeFile": "{path} : {manifest} doit être un vrai fichier ordinaire",
"PluginKimiManifestInvalid": "{path} : manifeste invalide : {error}",
"PluginKimiDirectoryNameMismatch": "{path} : le nom du dossier doit correspondre exactement au nom `{name}` du manifeste",
"PluginKimiHashUnavailable": "indisponible",
"PluginKimiRollbackDestinationMissing": "Linstallateur na pas indiqué le chemin de destination.",
"PluginKimiMismatchRemoved": "Après copie, le plugin `{name}` ne correspondait pas au contenu approuvé (attendu {expected}, trouvé {actual}). La copie inattendue a été supprimée ; vérifiez puis réessayez.",
"PluginKimiMismatchRollbackFailed": "Erreur : après copie, le plugin `{name}` ne correspondait pas au contenu approuvé (attendu {expected}, trouvé {actual}) et la suppression automatique a échoué : {error}. Il reste désactivé et non approuvé ; inspectez {path} avant de continuer.",
"PluginKimiUserPluginDirectory": "le dossier de plugins de lutilisateur",
"PluginKimiMarketplaceZipUnsupported": "Linstallateur vérifié de Codewhale ne prend pas en charge les paquets ZIP Kimi ; installez depuis un dossier local ou importez un plugin géré par Kimi en amont.",
"PluginKimiMarketplaceRemoteUnsupported": "Les sources Kimi distantes doivent finir par .tar.gz ou .tgz pour linstallation Codewhale ; .zip est reconnu mais non pris en charge.",
"PluginKimiMarketplaceGzipTarball": "URL darchive tar gzip",
"CmdPluginDescription": "Inspecter et gérer les bundles de plugins de confiance ; les outils exécutables hérités restent à part",
"CmdPluginBundleUsage": "Usage : /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Aucun bundle de plugins Codewhale trouvé.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale ne téléverse, ne migre et ne synchronise pas le code source local dans le Work hébergé. Utilisez {command} pour démarrer depuis la pointe de la branche disponible sur GitHub ou CNB. Les commits non poussés, les fichiers modifiés ou ignorés, les secrets et l'état de la session restent locaux.",
"CmdRemoteEnvBrowserLabel": "Work hébergé de Codewhale",
"CmdRenameDescription": "Renommer la session en cours",
"CmdTitleDescription": "Nommer la session et son onglet ou sa fenêtre de terminal",
"CmdRestoreDescription": "Restaurer le workspace à un snapshot pré/post-tour antérieur. Sans argument, liste les snapshots récents.",
"CmdRetryDescription": "Réessayer la dernière requête",
"CmdReviewDescription": "Lancer une revue de code structurée sur un fichier, un diff ou une PR",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP nécessite encore une action ; enregistré pour le rapport de setup (ne bloque pas le premier lancement).",
"SetupToolsMcpPreviewTitle": "Tools / MCP — amorçages sûrs",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills & Plugins — Amorçages sûrs\n\n/setup ne fait que lire l'inventaire local. Il ne démarre jamais de serveurs MCP, n'installe pas de skills, n'exécute pas de plugins ni de commandes non fiables.\n\nInventaire actuel :\n- MCP : {mcp_result}\n- Skills : {skills_result}\n- Répertoire tools : {tools_result}\n- Plugins : {plugins_result}\n- Hotbar (adaptateurs partagés) : {hotbar_result}\n\nChemins (home masqué) :\n- Config MCP : {mcp_path}\n- Skills : {skills_path}\n- Plugins : {plugins_path}\n\nAmorçage sûr (à exécuter vous-même dans un terminal normal ou une commande TUI) :\n- MCP : /mcp · codewhale mcp init · codewhale doctor\n- Skills : /skills · codewhale setup --skills · /skills install <spec>\n- Plugins : /plugin · codewhale setup --plugins\n- Répertoire tools : codewhale setup --tools\n\nLes actions à effets de bord exigent toujours une confirmation explicite. Les commandes de plugins restent distinctes des commandes slash ; la source de plugins du Hotbar reste différée jusqu'à l'arrivée des gates d'approbation.\n\nVoir docs/MCP.md et docs/skills/README.md pour ce qui nécessite encore une configuration externe manuelle.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh) :",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — connecté via Codewhale, jamais un second ordonnanceur :\n- État : {dsh_result}\n- Détection en lecture seule ; connecter/planifier/lancer/retirer : codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale n'écrit que dans $CODEWHALE_HOME/integrations/dsh ; il ne copie jamais de clés d'API et ne modifie aucun fichier DSH.",
"HotbarActionModeOperateName": "Mode Operate",
"HotbarActionModeOperateDescription": "Faites travailler votre Fleet en parallèle.",
"HomeOperateModeTip": "Operate — faites travailler votre Fleet en parallèle",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet prête",
"EmptyStateHelpConnector": "ou",
"EmptyStateHelpHint": "— tout voir",
"SessionsSurfaceTitle": "sessions",
"SessionsPaneTitle": " sessions (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "Impossible de vérifier les identités des profils existants ({error}) ; corrigez le fichier indiqué avant d'enregistrer.",
"FleetProfileIdConflict": "L'id de profil `{id}` est déjà utilisé par {path} ; rédigez un nouveau brouillon avec un rôle différent ou supprimez d'abord l'ancien fichier.",
"FleetProfileProviderUnconfigured": "Le profil épingle le fournisseur `{provider}`, qui n'a pas d'identifiants configurés ({env}) ; configurez-le dans /provider avant d'enregistrer.",
"FleetDestStepTitle": "Où ce profil doit-il être enregistré ?",
"FleetDestStepSubtitle": "Rien n'est écrit tant que vous ne confirmez pas à la dernière étape.",
"FleetDestProjectLabel": "Ce projet",
"FleetDestPersonalLabel": "Personnel",
"FleetDestProjectSummary": "Ce projet uniquement",
"FleetDestPersonalSummary": "Disponible dans tous les projets",
"FleetDestProjectDescription": "Enregistré dans ce projet ({workspace}). Il s'applique ici seulement et prime sur un profil Personnel portant le même ID.",
"FleetDestPersonalDescription": "Enregistré dans votre dossier Codewhale. Il s'applique dans tous les projets, sauf là où un projet possède son propre profil avec le même ID, qui prime alors.",
"FleetDestPathLine": "Fichier : {path}",
"FleetDestUnavailable": "Indisponible : {reason}",
"FleetDestReasonNoProjectConfig": "les profils de projet sont désactivés pour cette session (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "le dossier d'espace de travail {path} n'existe pas ou n'est pas un répertoire",
"FleetDestReasonHomeUnavailable": "impossible de résoudre votre dossier Codewhale ({error})",
"FleetDestWillReplace": "Remplacera le fichier existant {path}",
"FleetDestOverridesProject": "Ce projet possède déjà un profil '{id}', qui prime ici ; ce profil Personnel s'applique dans les autres projets.",
"FleetDestOverridesPersonal": "Prime sur votre profil Personnel '{id}' dans ce projet.",
"FleetDestOverridesBuiltIn": "Remplace le rôle {origin} '{id}' de l'effectif.",
"FleetSavesToChip": "Enregistre dans : {scope} · {path}",
"FleetSavesToUndecided": "Enregistre dans : à choisir à l'étape 3 — Ce projet ou Personnel",
"FleetActionSaveProject": "Enregistrer dans ce projet",
"FleetActionSavePersonal": "Enregistrer comme profil Personnel",
"FleetActionReplaceProject": "Remplacer dans ce projet",
"FleetActionReplacePersonal": "Remplacer le profil Personnel",
"FleetActionConfirmReplace": "Appuyez à nouveau sur Entrée pour remplacer {file}",
"FleetActionChangeDestination": "Changer la destination",
"FleetActionBack": "Retour",
"FleetReviewSavesTo": "Enregistre dans",
"FleetModelRowBlockedNotice": "Non sélectionnable : {reason}. Configurez-le dans /provider ou choisissez une autre ligne.",
"FleetDestProjectDisabledSave": "Les profils de projet sont désactivés pour cette session (--no-project-config) ; rien n'a été enregistré. Choisissez Personnel ou relancez sans l'option.",
"WorkflowStatusWaiting": "en attente",
"WorkflowDebrief": "débrief : {done}/{total} réglés · {failed} échoués · {cancelled} annulés · {elapsed}",
"WorkflowTranscriptDetails": "transcription : JSON complet de l'exécution disponible dans les détails de l'outil ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Exécution de lautomatisation {id} mise en attente : {status} (tâche {task})",
"AutomationDeletePreview": "La suppression nest pas encore confirmée. Rien na été supprimé.\nAutomatisation : {id} ({name})\nExécutions enregistrées : {run_count}\nPour supprimer la définition et lhistorique des exécutions, lancez :\n{command}",
"AutomationDeleteConfirmationStale": "La confirmation de suppression ne correspond plus à lautomatisation {id} ; rien na été supprimé. Vérifiez l’état actuel avec {command}.",
"AutomationDeleted": "Automatisation {id} ({name}) supprimée. Exécutions enregistrées supprimées : {run_count}."
"AutomationDeleted": "Automatisation {id} ({name}) supprimée. Exécutions enregistrées supprimées : {run_count}.",
"WhaleStateResting": "Au repos",
"WhaleStateThinking": "Réfléchit",
"WhaleStateWorking": "Travaille",
"WhaleStateWaiting": "Vous attend",
"WhaleStateBlocked": "Bloquée",
"WhaleStateOffline": "Hors ligne",
"WhaleAnimalScout": "baleine à bec",
"WhaleAnimalPatch": "marsouin commun",
"WhaleAnimalHarbor": "baleine à bosse",
"WhaleAnimalEcho": "globicéphale",
"WhaleAnimalKeel": "cachalot",
"WhaleAnimalLantern": "orque",
"WhaleAnimalPlain": "baleine",
"WhaleJobScout": "recherche",
"WhaleJobPatch": "codage",
"WhaleJobHarbor": "coordination",
"WhaleJobEcho": "communications",
"WhaleJobKeel": "exploitation",
"WhaleJobLantern": "revue",
"WhaleJobPlain": "travail général",
"SessionMetricsTurn": "tour",
"SessionMetricsTurns": "tours",
"SessionMetricsStep": "étape",
"SessionMetricsSteps": "étapes",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Outils",
"SessionMetricsTtft": "TTFT moy.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache",
"SessionMetricsInput": "Entrée",
"SessionMetricsStatusLine": "Métriques de session : {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review a autorisé '{tool}' (risque {risk}, gardien du modèle) : {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review a refusé '{tool}' (risque {risk}, gardien du modèle) : {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review n'a pas pu examiner '{tool}' ({reason}) ; refusé, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review a bloqué '{tool}' (politique déterministe) : {reason}",
"AutoReviewReceiptHeld": "Auto-Review a retenu '{tool}' sans pause ; refusé (une personne est requise — passez en Ask)",
"FooterHintEscInterrupt": "Échap pour interrompre",
"PermissionsPostureHeader": "Posture de permission actuelle : {posture}",
"PermissionsPostureAsk": "Ask : les appels de tool qui modifient l'autorité, le coût, la portée ou le résultat ouvrent une invite ; les appels en lecture seule prouvés sûrs s'exécutent sans. Les règles ask ci-dessus forcent toujours une invite.",
"PermissionsPostureAuto": "Auto-Review : n'ouvre jamais d'invite. Une politique déterministe autorise les appels prouvés sûrs et bloque fermement les travaux de publication ou destructifs en arrière-plan ; les appels qu'elle ne peut prouver sûrs vont à un gardien de modèle en une passe, qui autorise ou refuse avec un motif (un risque élevé ou critique ne s'exécute jamais automatiquement ; un examen échoué refuse, fail closed). Les retenues exigeant une personne sont refusées, pas cachées. Chaque décision est écrite dans la transcription sous forme de note et dans le journal d'audit.",
"PermissionsPostureBypass": "Full Access : les appels de tool ordinaires s'exécutent sans invite. Les retenues non contournables de sécurité, de loi du dépôt et de politique gérée échouent fermées comme blocages durs au lieu de demander.",
"PermissionsPostureNever": "never : seuls les tools jugés sûrs/en lecture seule s'exécutent ; tout le reste est bloqué sans invite.",
"PermissionsReceiptsNote": "Les décisions prises sans invite (verdicts du gardien Auto-Review, blocages et retenues) apparaissent comme notes dans la transcription et dans le journal d'audit à {audit_path}. Full Access se choisit délibérément avec Shift+Tab ou /config, jamais par une règle.",
"AgentFocusOpened": "Focus sur {agent}. Vos messages vont maintenant à ce worker ; Échap revient à la conversation principale.",
"AgentFocusClosed": "Retour à la conversation principale.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Message pour {agent} · Échap revient au principal",
"AgentFocusNoTranscript": "Aucune transcription pour {agent} pour l'instant. Les messages apparaissent ici au fil des échanges du worker.",
"AgentFocusOmitted": "Les messages précédents ({count}) sont omis de la transcription en mémoire.",
"AgentFocusFollowUpDelivered": "En file pour {agent} : il lira le message à son prochain tour.",
"AgentFocusFollowUpQueued": "En file pour {agent}",
"AgentFocusFollowUpContinued": "{agent} avait terminé ; poursuivi sur un nouveau fork ({target}). Cette vue suit désormais le fork.",
"AgentFocusFollowUpFailed": "Impossible de transmettre à {agent} : {reason}",
"FooterHintForAgents": "agents",
"FooterHintToManage": "gérer",
"AgentRailQueuedCount": "{count} en file",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "écrit",
"AgentFocusPostureReadOnly": "lecture seule",
"AgentFocusPostureNetwork": "réseau",
"AgentFocusPostureNoNetwork": "sans réseau",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell en lecture seule",
"AgentFocusPostureShellNone": "sans shell",
"GoalReceiptSet": "Objectif défini : « {objective} » · /goal affiche la progression · /goal pause ou /goal clear l'arrête",
"GoalStatusIdleHint": "à l'arrêt pour l'instant — envoyez un message ou /goal resume pour continuer"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "कार्य लिखें या / का उपयोग करें।",
"ComposerOperatePlaceholder": "लक्ष्य बताएँ — Codewhale पूरा होने तक काम करता रहेगा",
"ComposerDispatchFailedRestored": "संदेश नहीं भेजा गया ({error}); कम्पोज़र में वापस रखा गया।",
"DispatchFailedQueued": "भेजना विफल ({error}); {count} कतारबद्ध फ़ॉलो-अप सुरक्षित रखे।",
"DispatchFailedInitial": "प्रारंभिक प्रॉम्प्ट नहीं भेजा जा सका: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "सक्रिय प्रोवाइडर",
"ConfigLabelBaseUrlDeepseek": "प्रोवाइडर API URL (DeepSeek रूट)",
"ConfigLabelProviderUrl": "प्रोवाइडर API URL",
"ConfigHintProviderUrl": "मौजूदा प्रोवाइडर एंडपॉइंट; Xiaomi: टोकन प्लान | उपयोग के अनुसार भुगतान | कस्टम URL",
"ConfigLabelModel": "सक्रिय प्रोवाइडर मॉडल",
"ConfigLabelFastModel": "तेज़ मॉडल (व्युत्पन्न)",
"ConfigLabelDefaultModel": "लीगेसी फ़ॉलबैक मॉडल (केवल DeepSeek रूट)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "प्रति टर्न · लोकल ह्यूरिस्टिक · कोई राउटर अनुरोध नहीं",
"ModelPickerAutoLastRoute": "अंतिम {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} ({source} द्वारा) · Ctrl+O: रूट विवरण",
"CloudCodeSystemPromptUnsupported": "यह कनेक्शन अभी सिस्टम निर्देश समर्थित नहीं करता, इसलिए Antigravity cloud-code इस टर्न को सुरक्षित रूप से नहीं भेज सकता। कुछ नहीं भेजा गया; कोई अन्य प्रदाता चुनें।",
"HelpTitle": "मदद",
"HelpFilterPlaceholder": "फ़िल्टर करने के लिए टाइप करें",
"HelpFilterPrefix": "फ़िल्टर: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "फ़ाइल से सत्र लोड करें",
"CmdLogoutDescription": "API कुंजी साफ़ करें और सेटअप पर लौटें",
"CmdMcpDescription": "MCP सर्वर खोलें या प्रबंधित करें",
"McpRecommendedUnknownId": "अज्ञात सुझाई गई MCP ID। चुनी हुई सूची देखने के लिए {recommendations_command} चलाएँ।",
"McpRecommendationsSafety": "इस सूची को देखने से कुछ भी जुड़ता या चालू नहीं होता। स्पष्ट रूप से जोड़ने पर केवल कॉन्फ़िगरेशन लिखा जाता है; {restart_command} से सर्वर जुड़ने से पहले इसकी जाँच करें।",
"McpRecommendationGithub": "• github — GitHub का आधिकारिक रिमोट MCP एंडपॉइंट\n एंडपॉइंट: {endpoint}\n प्रमाणीकरण अलग है: {login_command} केवल तभी चलाएँ जब सर्वर OAuth उपलब्ध बताए;\n अन्यथा कम-से-कम अधिकार वाला PAT कमांड इतिहास से बाहर कॉन्फ़िगर करें। दिए गए\n स्कोप रिपॉज़िटरी डेटा लिख या मिटा सकते हैं; जहाँ संभव हो केवल-पढ़ने से शुरू करें।\n स्पष्ट रूप से जोड़ें: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — निश्चित संस्करण वाले npm पैकेज से आधिकारिक Chrome DevTools MCP\n पैकेज: {package} ({launcher})\n यह Chrome की जाँच/नियंत्रण और प्रमाणित पेज पढ़ सकता है। संवेदनशील टैब बंद करें\n और जोड़ने से पहले पैकेज जाँचें; {restart_command} इसे डाउनलोड करके चला सकता है।\n स्पष्ट रूप से जोड़ें: {add_command}",
"PluginKimiUsage": "उपयोग:\n {list_command}\n {approve_command}\nसूची केवल-पढ़ने योग्य है। मंज़ूरी पर Kimi-प्रबंधित एक मानक प्लगइन समीक्षित इंस्टॉलर से कॉपी होता है; वह बंद और अविश्वसनीय रहता है।",
"PluginKimiManagedRootHeading": "{root} पर Kimi-प्रबंधित प्लगइन:",
"PluginKimiNoneFound": "कोई मान्य प्रबंधित प्लगइन नहीं मिला।",
"PluginKimiLicenseUnspecified": "निर्दिष्ट नहीं",
"PluginKimiApplicable": "इस OS पर लागू",
"PluginKimiNotApplicable": "इस OS पर लागू नहीं",
"PluginKimiCandidateSummary": "{name} {version} — लाइसेंस={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " पथ: {path}\n सामग्री हैश: {content_hash}\n क्षमता हैश: {capability_hash}\n मंज़ूर करें: {approve_command}",
"PluginKimiRejectedHeading": "अस्वीकृत प्रविष्टियाँ (आयात योग्य नहीं):",
"PluginKimiInspectionFooter": "इस जाँच ने कुछ भी कॉपी, विश्वसनीय, चालू या निष्पादित नहीं किया। बाहरी Kimi ऐप, डेमन, बाइनरी, ब्राउज़र एक्सटेंशन, क्रेडेंशियल और OS अनुमतियाँ भी नहीं जाँची गईं।",
"PluginKimiCandidateMissing": "`{name}` नाम का कोई मान्य मानक Kimi-प्रबंधित प्लगइन नहीं है। {list_command} फिर चलाएँ।",
"PluginKimiCandidateChanged": "Kimi-प्रबंधित प्लगइन `{name}` समीक्षा के बाद बदल गया। अपेक्षित हैश {expected}, अब {actual} है। कुछ कॉपी नहीं हुआ; {list_command} फिर चलाएँ।",
"PluginKimiHomeMissing": "Kimi आयात के लिए उपयोगकर्ता होम डायरेक्टरी नहीं मिली।",
"PluginKimiRootInspectFailed": "Kimi प्लगइन रूट {root} की जाँच नहीं हो सकी: {error}",
"PluginKimiRootMustBeDirectory": "Kimi प्लगइन रूट {root} एक वास्तविक डायरेक्टरी होनी चाहिए, लिंक या रीपार्स पॉइंट नहीं।",
"PluginKimiRootCanonicalizeFailed": "Kimi प्लगइन रूट {root} को मानकीकृत नहीं किया जा सका: {error}",
"PluginKimiRootListFailed": "Kimi प्लगइन रूट {root} की सूची नहीं बन सकी: {error}",
"PluginKimiEntryReadFailed": "Kimi प्लगइन प्रविष्टि पढ़ी नहीं जा सकी: {error}",
"PluginKimiEntryLimit": "Kimi प्लगइन रूट में {count} प्रविष्टियाँ हैं; एक स्कैन में अधिकतम {max} की समीक्षा होती है।",
"PluginKimiEntryInspectFailed": "{path}: जाँच नहीं हो सकी: {error}",
"PluginKimiEntryLinksRefused": "{path}: लिंक और रीपार्स पॉइंट अस्वीकार हैं",
"PluginKimiEntryOutsideRoot": "{path}: मानक पथ {canonical_path} प्रबंधित रूट का सीधा चाइल्ड नहीं है",
"PluginKimiEntryCanonicalizeFailed": "{path}: मानकीकृत नहीं किया जा सका: {error}",
"PluginKimiManifestUnreadable": "{path}: पढ़ने योग्य {manifest} नहीं है: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} एक वास्तविक सामान्य फ़ाइल होनी चाहिए",
"PluginKimiManifestInvalid": "{path}: अमान्य मैनिफ़ेस्ट: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: डायरेक्टरी नाम मैनिफ़ेस्ट नाम `{name}` से बिल्कुल मिलना चाहिए",
"PluginKimiHashUnavailable": "उपलब्ध नहीं",
"PluginKimiRollbackDestinationMissing": "इंस्टॉलर ने गंतव्य पथ नहीं बताया।",
"PluginKimiMismatchRemoved": "कॉपी किया गया प्लगइन `{name}` मंज़ूर सामग्री से नहीं मिला (अपेक्षित {expected}, मिला {actual})। अनपेक्षित कॉपी हटा दी गई; समीक्षा करके फिर प्रयास करें।",
"PluginKimiMismatchRollbackFailed": "त्रुटि: कॉपी किया गया प्लगइन `{name}` मंज़ूर सामग्री से नहीं मिला (अपेक्षित {expected}, मिला {actual}) और स्वतः हटाना विफल रहा: {error}। यह बंद और अविश्वसनीय है; आगे बढ़ने से पहले {path} जाँचें।",
"PluginKimiUserPluginDirectory": "उपयोगकर्ता प्लगइन डायरेक्टरी",
"PluginKimiMarketplaceZipUnsupported": "Codewhale का समीक्षित इंस्टॉलर Kimi ZIP बंडल समर्थित नहीं करता; स्थानीय डायरेक्टरी से इंस्टॉल करें या अपस्ट्रीम Kimi-प्रबंधित प्लगइन आयात करें।",
"PluginKimiMarketplaceRemoteUnsupported": "Codewhale इंस्टॉलेशन के लिए रिमोट Kimi स्रोत .tar.gz या .tgz पर समाप्त होना चाहिए; .zip पहचाना जाता है, पर समर्थित नहीं है।",
"PluginKimiMarketplaceGzipTarball": "gzip टारबॉल URL",
"CmdPluginDescription": "विश्वसनीय प्लगिन बंडल देखें और प्रबंधित करें; लीगेसी एक्ज़िक्यूटेबल टूल अलग रहते हैं",
"CmdPluginBundleUsage": "उपयोग: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "कोई Codewhale प्लगिन बंडल नहीं मिला।",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale लोकल सोर्स को होस्टेड Work में अपलोड, माइग्रेट या सिंक नहीं करता। GitHub या CNB पर उपलब्ध ब्रांच टिप से शुरू करने के लिए {command} का उपयोग करें। पुश न किए गए कमिट, बदली हुई या इग्नोर की गई फ़ाइलें, सीक्रेट और सत्र स्थिति लोकल ही रहती हैं।",
"CmdRemoteEnvBrowserLabel": "Codewhale होस्टेड Work",
"CmdRenameDescription": "वर्तमान सत्र का नाम बदलें",
"CmdTitleDescription": "वर्तमान सत्र और उसके टर्मिनल टैब/विंडो का नाम सेट करें",
"CmdRestoreDescription": "वर्कस्पेस को पूर्व pre/post-turn स्नैपशॉट पर वापस लाएँ। बिना आर्ग के हालिया स्नैपशॉट सूचीबद्ध करता है।",
"CmdRetryDescription": "अंतिम अनुरोध पुनः प्रयास करें",
"CmdReviewDescription": "फ़ाइल, diff या PR पर संरचित कोड समीक्षा चलाएँ",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP में अभी कार्रवाई बाक़ी है; सेटअप रिपोर्ट के लिए दर्ज (पहला रन नहीं रोकता)।",
"SetupToolsMcpPreviewTitle": "Tools / MCP सुरक्षित ऑन-रैंप",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills और Plugins — सुरक्षित ऑन-रैंप\n\n/setup केवल लोकल इन्वेंटरी पढ़ता है। यह कभी MCP सर्वर शुरू नहीं करता, स्किल इंस्टॉल नहीं करता, प्लगइन नहीं चलाता, या अविश्वसनीय कमांड निष्पादित नहीं करता।\n\nवर्तमान इन्वेंटरी:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools डायरेक्टरी: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (साझा एडॉप्टर): {hotbar_result}\n\nपथ (होम छिपा):\n- MCP कॉन्फ़िग: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nसुरक्षित बूटस्ट्रैप (सामान्य टर्मिनल या TUI कमांड में खुद चलाएँ):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills · /skills install <spec>\n- Plugins: /plugin · codewhale setup --plugins\n- Tools डायरेक्टरी: codewhale setup --tools\n\nदुष्प्रभावी कार्रवाइयों के लिए हमेशा स्पष्ट पुष्टि आवश्यक है। प्लगइन कमांड स्लैश कमांड से अलग रहते हैं; Hotbar प्लगइन स्रोत अनुमति गेट आने तक स्थगित रहता है।\n\nजो अभी मैन्युअल बाहरी सेटअप माँगता है, उसके लिए docs/MCP.md और docs/skills/README.md देखें।",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — Codewhale के माध्यम से जुड़ा, कभी दूसरा शेड्यूलर नहीं:\n- स्थिति: {dsh_result}\n- केवल-पढ़ने वाली पहचान; जोड़ें/योजना/चलाएँ/हटाएँ: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale केवल $CODEWHALE_HOME/integrations/dsh में लिखता है; API कुंजियाँ कभी कॉपी नहीं करता और DSH फ़ाइलें नहीं बदलता।",
"HotbarActionModeOperateName": "Operate मोड",
"HotbarActionModeOperateDescription": "अपनी Fleet को समानांतर काम पर लगाएँ।",
"HomeOperateModeTip": "Operate — अपनी Fleet को समानांतर काम पर लगाएँ",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet तैयार",
"EmptyStateHelpConnector": "या",
"EmptyStateHelpHint": "— सब कुछ देखें",
"SessionsSurfaceTitle": "सत्र",
"SessionsPaneTitle": " सत्र (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "मौजूदा प्रोफ़ाइल पहचान सत्यापित नहीं हो सकी ({error}); सहेजने से पहले नामित फ़ाइल ठीक करें।",
"FleetProfileIdConflict": "प्रोफ़ाइल id `{id}` पहले से {path} इस्तेमाल कर रहा है; दूसरी भूमिका से फिर मसौदा बनाएँ या पहले पुरानी फ़ाइल हटाएँ।",
"FleetProfileProviderUnconfigured": "प्रोफ़ाइल प्रोवाइडर `{provider}` पिन करती है, जिसके क्रेडेंशल कॉन्फ़िगर नहीं ({env}); सहेजने से पहले /provider में सेटअप करें।",
"FleetDestStepTitle": "यह प्रोफ़ाइल कहाँ सहेजी जाए?",
"FleetDestStepSubtitle": "अंतिम चरण में पुष्टि करने तक कुछ भी नहीं लिखा जाता।",
"FleetDestProjectLabel": "यह प्रोजेक्ट",
"FleetDestPersonalLabel": "व्यक्तिगत",
"FleetDestProjectSummary": "केवल यह प्रोजेक्ट",
"FleetDestPersonalSummary": "हर प्रोजेक्ट में उपलब्ध",
"FleetDestProjectDescription": "इस प्रोजेक्ट ({workspace}) के भीतर सहेजी जाती है। यह केवल यहाँ लागू होती है और समान ID वाली व्यक्तिगत प्रोफ़ाइल पर वरीयता पाती है।",
"FleetDestPersonalDescription": "आपके Codewhale होम में सहेजी जाती है। यह हर प्रोजेक्ट में लागू होती है — सिवाय वहाँ जहाँ किसी प्रोजेक्ट की अपनी समान ID वाली प्रोफ़ाइल हो, जो वहाँ वरीयता पाती है।",
"FleetDestPathLine": "फ़ाइल: {path}",
"FleetDestUnavailable": "उपलब्ध नहीं: {reason}",
"FleetDestReasonNoProjectConfig": "इस सत्र में प्रोजेक्ट प्रोफ़ाइलें अक्षम हैं (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "वर्कस्पेस फ़ोल्डर {path} मौजूद नहीं है या डायरेक्टरी नहीं है",
"FleetDestReasonHomeUnavailable": "आपका Codewhale होम निर्धारित नहीं किया जा सका ({error})",
"FleetDestWillReplace": "मौजूदा फ़ाइल {path} को बदल देगा",
"FleetDestOverridesProject": "इस प्रोजेक्ट में पहले से '{id}' प्रोफ़ाइल है, जो यहाँ वरीयता पाती है; यह व्यक्तिगत प्रोफ़ाइल अन्य प्रोजेक्टों में लागू होती है।",
"FleetDestOverridesPersonal": "इस प्रोजेक्ट के भीतर आपकी व्यक्तिगत '{id}' प्रोफ़ाइल पर वरीयता पाती है।",
"FleetDestOverridesBuiltIn": "रोस्टर में {origin} '{id}' भूमिका को बदल देती है।",
"FleetSavesToChip": "सहेजी जाएगी: {scope} · {path}",
"FleetSavesToUndecided": "सहेजी जाएगी: चरण 3 में चुनें — यह प्रोजेक्ट या व्यक्तिगत",
"FleetActionSaveProject": "इस प्रोजेक्ट में सहेजें",
"FleetActionSavePersonal": "व्यक्तिगत प्रोफ़ाइल के रूप में सहेजें",
"FleetActionReplaceProject": "इस प्रोजेक्ट में बदलें",
"FleetActionReplacePersonal": "व्यक्तिगत प्रोफ़ाइल बदलें",
"FleetActionConfirmReplace": "{file} को बदलने के लिए फिर से Enter दबाएँ",
"FleetActionChangeDestination": "गंतव्य बदलें",
"FleetActionBack": "वापस",
"FleetReviewSavesTo": "सहेजी जाएगी",
"FleetModelRowBlockedNotice": "चयन योग्य नहीं: {reason}। इसे /provider में कॉन्फ़िगर करें या दूसरी पंक्ति चुनें।",
"FleetDestProjectDisabledSave": "इस सत्र में प्रोजेक्ट प्रोफ़ाइलें अक्षम हैं (--no-project-config); कुछ भी सहेजा नहीं गया। व्यक्तिगत चुनें या फ़्लैग के बिना पुनः आरंभ करें।",
"WorkflowStatusWaiting": "प्रतीक्षारत",
"WorkflowDebrief": "डिब्रीफ़: {done}/{total} निपटे · {failed} विफल · {cancelled} रद्द · {elapsed}",
"WorkflowTranscriptDetails": "ट्रांसक्रिप्ट: पूरा रन JSON टूल विवरण में उपलब्ध ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "स्वचालन {id} का निष्पादन कतार में जोड़ा गया: {status} (कार्य {task})",
"AutomationDeletePreview": "हटाना अभी पक्का नहीं किया गया है। कुछ भी नहीं हटाया गया।\nस्वचालन: {id} ({name})\nदर्ज निष्पादन: {run_count}\nपरिभाषा और निष्पादन इतिहास हटाने के लिए यह चलाएँ:\n{command}",
"AutomationDeleteConfirmationStale": "हटाने की पुष्टि अब स्वचालन {id} की वर्तमान स्थिति से मेल नहीं खाती; कुछ भी नहीं हटाया गया। {command} से वर्तमान स्थिति फिर देखें।",
"AutomationDeleted": "स्वचालन {id} ({name}) हटा दिया गया। हटाए गए दर्ज निष्पादन: {run_count}।"
"AutomationDeleted": "स्वचालन {id} ({name}) हटा दिया गया। हटाए गए दर्ज निष्पादन: {run_count}।",
"WhaleStateResting": "विश्राम में",
"WhaleStateThinking": "सोच रहा है",
"WhaleStateWorking": "काम कर रहा है",
"WhaleStateWaiting": "आपकी प्रतीक्षा में",
"WhaleStateBlocked": "अवरुद्ध",
"WhaleStateOffline": "ऑफ़लाइन",
"WhaleAnimalScout": "चोंचदार व्हेल",
"WhaleAnimalPatch": "बंदरगाह पोरपॉइज़",
"WhaleAnimalHarbor": "हंपबैक व्हेल",
"WhaleAnimalEcho": "पायलट व्हेल",
"WhaleAnimalKeel": "स्पर्म व्हेल",
"WhaleAnimalLantern": "ओर्का",
"WhaleAnimalPlain": "व्हेल",
"WhaleJobScout": "शोध",
"WhaleJobPatch": "कोडिंग",
"WhaleJobHarbor": "समन्वय",
"WhaleJobEcho": "संचार",
"WhaleJobKeel": "संचालन",
"WhaleJobLantern": "समीक्षा",
"WhaleJobPlain": "सामान्य कार्य",
"SessionMetricsTurn": "टर्न",
"SessionMetricsTurns": "टर्न",
"SessionMetricsStep": "चरण",
"SessionMetricsSteps": "चरण",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "टूल कॉल",
"SessionMetricsTtft": "TTFT औसत",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "कैश हिट",
"SessionMetricsInput": "इनपुट",
"SessionMetricsStatusLine": "सत्र मेट्रिक्स: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review ने '{tool}' की अनुमति दी (जोखिम {risk}, मॉडल गार्जियन): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review ने '{tool}' को अस्वीकार किया (जोखिम {risk}, मॉडल गार्जियन): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review '{tool}' की समीक्षा नहीं कर सका ({reason}); अस्वीकृत, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review ने '{tool}' को अवरुद्ध किया (नियतात्मक नीति): {reason}",
"AutoReviewReceiptHeld": "Auto-Review ने बिना रुके '{tool}' को रोका; अस्वीकृत (व्यक्ति चाहिए — Ask पर जाएँ)",
"FooterHintEscInterrupt": "रोकने के लिए Esc",
"PermissionsPostureHeader": "वर्तमान अनुमति स्थिति: {posture}",
"PermissionsPostureAsk": "Ask: अधिकार, लागत, दायरा या परिणाम बदलने वाली tool कॉल प्रॉम्प्ट खोलती हैं; सिद्ध-सुरक्षित केवल-पढ़ने वाली कॉल बिना प्रॉम्प्ट चलती हैं। ऊपर के ask नियम हमेशा प्रॉम्प्ट लागू करते हैं।",
"PermissionsPostureAuto": "Auto-Review: कभी प्रॉम्प्ट नहीं खोलता। नियतात्मक नीति सिद्ध-सुरक्षित कॉल की अनुमति देती है और प्रकाशन-जैसे या विनाशकारी पृष्ठभूमि कार्य को कड़ाई से अवरुद्ध करती है; जिन कॉल को सुरक्षित सिद्ध नहीं किया जा सकता वे एक-बार के मॉडल गार्जियन के पास जाती हैं जो कारण बताकर अनुमति देता या अस्वीकार करता है (उच्च या गंभीर जोखिम कभी स्वतः नहीं चलता; असफल समीक्षा fail closed के तहत अस्वीकार करती है)। जिन रोकों के लिए व्यक्ति चाहिए वे छिपाई नहीं जातीं, अस्वीकार की जाती हैं। ऐसा हर निर्णय ट्रांसक्रिप्ट में नोट के रूप में और ऑडिट लॉग में लिखा जाता है।",
"PermissionsPostureBypass": "Full Access: सामान्य tool कॉल बिना प्रॉम्प्ट चलती हैं। जिन्हें दरकिनार नहीं किया जा सकता ऐसी सुरक्षा, रिपॉजिटरी-कानून और प्रबंधित-नीति की रोकें पूछने के बजाय कड़े अवरोध के रूप में fail closed होती हैं।",
"PermissionsPostureNever": "never: केवल सुरक्षित/केवल-पढ़ने वाली मानी गई tool चलती हैं; बाकी सब बिना प्रॉम्प्ट अवरुद्ध होता है।",
"PermissionsReceiptsNote": "बिना प्रॉम्प्ट लिए गए निर्णय (Auto-Review गार्जियन के फ़ैसले, अवरोध और रोकें) ट्रांसक्रिप्ट नोट के रूप में और {audit_path} पर ऑडिट लॉग में दिखते हैं। Full Access जानबूझकर Shift+Tab या /config से चुना जाता है, कभी किसी नियम से नहीं।",
"AgentFocusOpened": "{agent} पर फ़ोकस है। अब आपके संदेश इसी वर्कर को जाएँगे; Esc से मुख्य बातचीत पर लौटें।",
"AgentFocusClosed": "मुख्य बातचीत पर वापस।",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "{agent} को संदेश · Esc से मुख्य पर लौटें",
"AgentFocusNoTranscript": "{agent} का अभी कोई ट्रांसक्रिप्ट नहीं है। वर्कर के संदेश आदान-प्रदान करते ही यहाँ दिखेंगे।",
"AgentFocusOmitted": "पहले के संदेश ({count}) इन-मेमोरी ट्रांसक्रिप्ट से छोड़ दिए गए हैं।",
"AgentFocusFollowUpDelivered": "{agent} के लिए कतार में: यह अगले राउंड में संदेश पढ़ेगा।",
"AgentFocusFollowUpQueued": "{agent} के लिए कतार में",
"AgentFocusFollowUpContinued": "{agent} पूरा हो चुका था; नए फ़ोर्क ({target}) पर जारी रखा गया। यह दृश्य अब उस फ़ोर्क का अनुसरण करता है।",
"AgentFocusFollowUpFailed": "{agent} तक नहीं पहुँचाया जा सका: {reason}",
"FooterHintForAgents": "एजेंट",
"FooterHintToManage": "प्रबंधन",
"AgentRailQueuedCount": "{count} कतार में",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "लिख सकता है",
"AgentFocusPostureReadOnly": "केवल-पढ़ने",
"AgentFocusPostureNetwork": "नेटवर्क",
"AgentFocusPostureNoNetwork": "नेटवर्क नहीं",
"AgentFocusPostureShellFull": "शेल",
"AgentFocusPostureShellReadOnly": "केवल-पढ़ने शेल",
"AgentFocusPostureShellNone": "शेल नहीं",
"GoalReceiptSet": "लक्ष्य सेट: \"{objective}\" · /goal प्रगति दिखाता है · /goal pause या /goal clear इसे रोकता है",
"GoalStatusIdleHint": "अभी नहीं चल रहा — जारी रखने के लिए संदेश भेजें या /goal resume"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Tulis tugas atau gunakan /.",
"ComposerOperatePlaceholder": "Jelaskan tujuannya — Codewhale terus bekerja sampai selesai",
"ComposerDispatchFailedRestored": "Pesan tidak terkirim ({error}); dikembalikan ke komposer.",
"DispatchFailedQueued": "Pengiriman gagal ({error}); {count} tindak lanjut tetap dalam antrean.",
"DispatchFailedInitial": "Prompt awal tidak dapat dikirim: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Penyedia aktif",
"ConfigLabelBaseUrlDeepseek": "URL API penyedia (rute DeepSeek)",
"ConfigLabelProviderUrl": "URL API penyedia",
"ConfigHintProviderUrl": "Endpoint penyedia saat ini; Xiaomi: paket token | bayar sesuai pemakaian | URL kustom",
"ConfigLabelModel": "Model penyedia aktif",
"ConfigLabelFastModel": "Model cepat (turunan)",
"ConfigLabelDefaultModel": "Model cadangan lama (khusus rute DeepSeek)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "per giliran · heuristik lokal · tanpa permintaan router",
"ModelPickerAutoLastRoute": "terakhir {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} via {source} · Ctrl+O: detail rute",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code tidak dapat mengirim giliran ini dengan aman karena koneksi ini belum mendukung instruksi sistem. Tidak ada yang dikirim; pilih penyedia lain.",
"HelpTitle": "Bantuan",
"HelpFilterPlaceholder": "Ketik untuk menyaring",
"HelpFilterPrefix": "Saring: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Muat sesi dari file",
"CmdLogoutDescription": "Hapus kunci API dan kembali ke penyiapan",
"CmdMcpDescription": "Buka atau kelola server MCP",
"McpRecommendedUnknownId": "ID MCP rekomendasi tidak dikenal. Jalankan {recommendations_command} untuk memeriksa daftar pilihan.",
"McpRecommendationsSafety": "Melihat daftar ini tidak menambah atau mengaktifkan apa pun. Penambahan eksplisit hanya menulis konfigurasi; periksa sebelum {restart_command} menghubungkan server.",
"McpRecommendationGithub": "• github — endpoint MCP jarak jauh resmi GitHub\n endpoint: {endpoint}\n autentikasi terpisah: gunakan {login_command} hanya jika server menawarkan OAuth;\n jika tidak, atur PAT dengan hak minimum di luar riwayat perintah. Cakupan yang\n diberikan dapat menulis atau menghapus data repositori; mulai dengan akses hanya-baca jika memungkinkan.\n tambahkan secara eksplisit: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools resmi lewat paket npm dengan versi terkunci\n paket: {package} ({launcher})\n ini dapat memeriksa/mengontrol Chrome dan membaca halaman terautentikasi. Tutup tab\n sensitif dan verifikasi paket sebelum menambahkannya; {restart_command} dapat mengunduh dan menjalankannya.\n tambahkan secara eksplisit: {add_command}",
"PluginKimiUsage": "Penggunaan:\n {list_command}\n {approve_command}\nDaftar hanya-baca. Persetujuan menyalin satu plugin kanonis kelolaan Kimi melalui penginstal yang ditinjau; plugin tetap nonaktif dan tidak dipercaya.",
"PluginKimiManagedRootHeading": "Plugin kelolaan Kimi di {root}:",
"PluginKimiNoneFound": "Tidak ditemukan plugin terkelola yang valid.",
"PluginKimiLicenseUnspecified": "tidak ditentukan",
"PluginKimiApplicable": "berlaku pada OS ini",
"PluginKimiNotApplicable": "tidak berlaku pada OS ini",
"PluginKimiCandidateSummary": "{name} {version} — lisensi={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " path: {path}\n hash konten: {content_hash}\n hash kapabilitas: {capability_hash}\n setujui: {approve_command}",
"PluginKimiRejectedHeading": "Entri ditolak (tidak dapat diimpor):",
"PluginKimiInspectionFooter": "Pemeriksaan ini tidak menyalin, memercayai, mengaktifkan, atau menjalankan apa pun. Aplikasi, daemon, biner, ekstensi browser, kredensial, dan izin OS eksternal Kimi tidak diperiksa.",
"PluginKimiCandidateMissing": "Tidak ada plugin kanonis kelolaan Kimi yang valid bernama `{name}`. Jalankan lagi {list_command}.",
"PluginKimiCandidateChanged": "Plugin kelolaan Kimi `{name}` berubah sejak peninjauan. Hash yang diharapkan {expected}, kini {actual}. Tidak ada yang disalin; jalankan lagi {list_command}.",
"PluginKimiHomeMissing": "Direktori rumah pengguna untuk impor Kimi tidak ditemukan.",
"PluginKimiRootInspectFailed": "Root plugin Kimi {root} tidak dapat diperiksa: {error}",
"PluginKimiRootMustBeDirectory": "Root plugin Kimi {root} harus berupa direktori nyata, bukan tautan atau titik reparse.",
"PluginKimiRootCanonicalizeFailed": "Root plugin Kimi {root} tidak dapat dikanonisasi: {error}",
"PluginKimiRootListFailed": "Root plugin Kimi {root} tidak dapat didaftar: {error}",
"PluginKimiEntryReadFailed": "Entri plugin Kimi tidak dapat dibaca: {error}",
"PluginKimiEntryLimit": "Root plugin Kimi berisi {count} entri; maksimal {max} ditinjau per pemindaian.",
"PluginKimiEntryInspectFailed": "{path}: tidak dapat diperiksa: {error}",
"PluginKimiEntryLinksRefused": "{path}: tautan dan titik reparse ditolak",
"PluginKimiEntryOutsideRoot": "{path}: path kanonis {canonical_path} bukan anak langsung dari root terkelola",
"PluginKimiEntryCanonicalizeFailed": "{path}: tidak dapat dikanonisasi: {error}",
"PluginKimiManifestUnreadable": "{path}: tidak ada {manifest} yang dapat dibaca: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} harus berupa file biasa yang nyata",
"PluginKimiManifestInvalid": "{path}: manifes tidak valid: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: nama direktori harus sama persis dengan nama manifes `{name}`",
"PluginKimiHashUnavailable": "tidak tersedia",
"PluginKimiRollbackDestinationMissing": "Penginstal tidak melaporkan path tujuan.",
"PluginKimiMismatchRemoved": "Plugin `{name}` setelah disalin tidak cocok dengan konten yang disetujui (diharapkan {expected}, ditemukan {actual}). Salinan tak terduga telah dihapus; tinjau dan coba lagi.",
"PluginKimiMismatchRollbackFailed": "Kesalahan: plugin `{name}` setelah disalin tidak cocok dengan konten yang disetujui (diharapkan {expected}, ditemukan {actual}) dan penghapusan otomatis gagal: {error}. Plugin tetap nonaktif dan tidak dipercaya; periksa {path} sebelum melanjutkan.",
"PluginKimiUserPluginDirectory": "direktori plugin pengguna",
"PluginKimiMarketplaceZipUnsupported": "Penginstal Codewhale yang ditinjau tidak mendukung bundel ZIP Kimi; instal dari direktori lokal atau impor plugin kelolaan Kimi dari sumber hulu.",
"PluginKimiMarketplaceRemoteUnsupported": "Sumber jarak jauh Kimi harus berakhiran .tar.gz atau .tgz untuk dipasang Codewhale; .zip dikenali tetapi tidak didukung.",
"PluginKimiMarketplaceGzipTarball": "URL tarball gzip",
"CmdPluginDescription": "Periksa dan kelola bundel plugin tepercaya; alat eksekusi lama tetap terpisah",
"CmdPluginBundleUsage": "Penggunaan: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Tidak ada bundel plugin Codewhale yang ditemukan.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale tidak mengunggah, memigrasikan, atau menyinkronkan kode sumber lokal ke Work terhosting. Gunakan {command} untuk memulai dari ujung branch yang tersedia di GitHub atau CNB. Commit yang belum di-push, file yang berubah atau diabaikan, rahasia, dan status sesi tetap lokal.",
"CmdRemoteEnvBrowserLabel": "Work terhosting Codewhale",
"CmdRenameDescription": "Ubah nama sesi saat ini",
"CmdTitleDescription": "Beri nama sesi saat ini dan tab/jendela terminalnya",
"CmdRestoreDescription": "Kembalikan workspace ke snapshot pra/pasca-giliran sebelumnya. Tanpa argumen, menampilkan snapshot terbaru.",
"CmdRetryDescription": "Coba lagi permintaan terakhir",
"CmdReviewDescription": "Jalankan tinjauan kode terstruktur pada file, diff, atau PR",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP masih perlu tindakan; direkam untuk laporan setup (tidak menghambat run pertama).",
"SetupToolsMcpPreviewTitle": "Jalur awal aman Tools / MCP",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills & Plugins — Jalur Awal Aman\n\n/setup hanya membaca inventaris lokal. Tidak pernah memulai server MCP, menginstal skill, menjalankan plugin, atau mengeksekusi perintah tak tepercaya.\n\nInventaris saat ini:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Direktori tools: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (adapter bersama): {hotbar_result}\n\nPath (home disamarkan):\n- Konfigurasi MCP: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nBootstrap aman (jalankan sendiri di terminal normal atau perintah TUI):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills · /skills install <spec>\n- Plugins: /plugin · codewhale setup --plugins\n- Direktori tools: codewhale setup --tools\n\nAksi berefek samping selalu memerlukan konfirmasi eksplisit. Perintah plugin tetap berbeda dari perintah slash; sumber plugin Hotbar tetap ditunda sampai gate persetujuan hadir.\n\nLihat docs/MCP.md dan docs/skills/README.md untuk yang masih perlu setup eksternal manual.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — terhubung melalui Codewhale, bukan penjadwal kedua:\n- Status: {dsh_result}\n- Deteksi hanya-baca; hubungkan/rencanakan/jalankan/hapus: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale hanya menulis ke $CODEWHALE_HOME/integrations/dsh; tidak pernah menyalin kunci API atau mengubah berkas DSH.",
"HotbarActionModeOperateName": "Mode Operate",
"HotbarActionModeOperateDescription": "Kerahkan Fleet Anda untuk bekerja paralel.",
"HomeOperateModeTip": "Operate — kerahkan Fleet Anda untuk bekerja paralel",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet siap",
"EmptyStateHelpConnector": "atau",
"EmptyStateHelpHint": "— lihat semuanya",
"SessionsSurfaceTitle": "sesi",
"SessionsPaneTitle": " sesi (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "Tidak dapat memverifikasi identitas profil yang ada ({error}); perbaiki file yang disebutkan sebelum menyimpan.",
"FleetProfileIdConflict": "Id profil `{id}` sudah dipakai oleh {path}; buat ulang draf dengan peran berbeda atau hapus file lama dulu.",
"FleetProfileProviderUnconfigured": "Profil mengunci provider `{provider}` yang belum punya kredensial terkonfigurasi ({env}); atur di /provider sebelum menyimpan.",
"FleetDestStepTitle": "Di mana profil ini harus disimpan?",
"FleetDestStepSubtitle": "Tidak ada yang ditulis sampai Anda mengonfirmasi di langkah terakhir.",
"FleetDestProjectLabel": "Proyek ini",
"FleetDestPersonalLabel": "Pribadi",
"FleetDestProjectSummary": "Hanya proyek ini",
"FleetDestPersonalSummary": "Tersedia di semua proyek",
"FleetDestProjectDescription": "Disimpan di dalam proyek ini ({workspace}). Hanya berlaku di sini dan diprioritaskan di atas profil Pribadi dengan ID yang sama.",
"FleetDestPersonalDescription": "Disimpan di home Codewhale Anda. Berlaku di semua proyek — kecuali jika sebuah proyek memiliki profil sendiri dengan ID yang sama, yang diprioritaskan di sana.",
"FleetDestPathLine": "Berkas: {path}",
"FleetDestUnavailable": "Tidak tersedia: {reason}",
"FleetDestReasonNoProjectConfig": "profil proyek dinonaktifkan untuk sesi ini (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "folder workspace {path} tidak ada atau bukan direktori",
"FleetDestReasonHomeUnavailable": "home Codewhale Anda tidak dapat ditentukan ({error})",
"FleetDestWillReplace": "Akan mengganti berkas yang sudah ada {path}",
"FleetDestOverridesProject": "Proyek ini sudah memiliki profil '{id}' yang diprioritaskan di sini; profil Pribadi ini berlaku di proyek lain.",
"FleetDestOverridesPersonal": "Diprioritaskan di atas profil Pribadi '{id}' Anda di dalam proyek ini.",
"FleetDestOverridesBuiltIn": "Menggantikan peran {origin} '{id}' di daftar tim.",
"FleetSavesToChip": "Disimpan ke: {scope} · {path}",
"FleetSavesToUndecided": "Disimpan ke: pilih di langkah 3 — Proyek ini atau Pribadi",
"FleetActionSaveProject": "Simpan ke proyek ini",
"FleetActionSavePersonal": "Simpan sebagai profil Pribadi",
"FleetActionReplaceProject": "Ganti di proyek ini",
"FleetActionReplacePersonal": "Ganti profil Pribadi",
"FleetActionConfirmReplace": "Tekan Enter lagi untuk mengganti {file}",
"FleetActionChangeDestination": "Ubah tujuan",
"FleetActionBack": "Kembali",
"FleetReviewSavesTo": "Disimpan ke",
"FleetModelRowBlockedNotice": "Tidak dapat dipilih: {reason}. Atur di /provider atau pilih baris lain.",
"FleetDestProjectDisabledSave": "Profil proyek dinonaktifkan untuk sesi ini (--no-project-config); tidak ada yang disimpan. Pilih Pribadi atau mulai ulang tanpa flag tersebut.",
"WorkflowStatusWaiting": "menunggu",
"WorkflowDebrief": "debrief: {done}/{total} tuntas · {failed} gagal · {cancelled} dibatalkan · {elapsed}",
"WorkflowTranscriptDetails": "transkrip: JSON run lengkap tersedia di detail tool ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Eksekusi otomatisasi {id} dimasukkan ke antrean: {status} (tugas {task})",
"AutomationDeletePreview": "Penghapusan belum dikonfirmasi. Tidak ada yang dihapus.\nOtomatisasi: {id} ({name})\nEksekusi tercatat: {run_count}\nUntuk menghapus definisi dan riwayat eksekusi, jalankan:\n{command}",
"AutomationDeleteConfirmationStale": "Konfirmasi penghapusan tidak lagi cocok dengan otomatisasi {id}; tidak ada yang dihapus. Tinjau keadaan saat ini dengan {command}.",
"AutomationDeleted": "Otomatisasi {id} ({name}) dihapus. Eksekusi tercatat yang dihapus: {run_count}."
"AutomationDeleted": "Otomatisasi {id} ({name}) dihapus. Eksekusi tercatat yang dihapus: {run_count}.",
"WhaleStateResting": "Beristirahat",
"WhaleStateThinking": "Berpikir",
"WhaleStateWorking": "Bekerja",
"WhaleStateWaiting": "Menunggu Anda",
"WhaleStateBlocked": "Terblokir",
"WhaleStateOffline": "Luring",
"WhaleAnimalScout": "paus berparuh",
"WhaleAnimalPatch": "lumba-lumba pelabuhan",
"WhaleAnimalHarbor": "paus bungkuk",
"WhaleAnimalEcho": "paus pilot",
"WhaleAnimalKeel": "paus sperma",
"WhaleAnimalLantern": "orca",
"WhaleAnimalPlain": "paus",
"WhaleJobScout": "riset",
"WhaleJobPatch": "pengodean",
"WhaleJobHarbor": "koordinasi",
"WhaleJobEcho": "komunikasi",
"WhaleJobKeel": "operasi",
"WhaleJobLantern": "tinjauan",
"WhaleJobPlain": "pekerjaan umum",
"SessionMetricsTurn": "giliran",
"SessionMetricsTurns": "giliran",
"SessionMetricsStep": "langkah",
"SessionMetricsSteps": "langkah",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Alat",
"SessionMetricsTtft": "TTFT rerata",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache",
"SessionMetricsInput": "Masukan",
"SessionMetricsStatusLine": "Metrik sesi: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review mengizinkan '{tool}' (risiko {risk}, penjaga model): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review menolak '{tool}' (risiko {risk}, penjaga model): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review tidak dapat meninjau '{tool}' ({reason}); ditolak, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review memblokir '{tool}' (kebijakan deterministik): {reason}",
"AutoReviewReceiptHeld": "Auto-Review menahan '{tool}' tanpa berhenti; ditolak (perlu manusia — beralih ke Ask)",
"FooterHintEscInterrupt": "Esc untuk menghentikan",
"PermissionsPostureHeader": "Postur izin saat ini: {posture}",
"PermissionsPostureAsk": "Ask: panggilan tool yang mengubah wewenang, biaya, cakupan, atau hasil membuka prompt; panggilan hanya-baca yang terbukti aman berjalan tanpa prompt. Aturan ask di atas selalu memaksa prompt.",
"PermissionsPostureAuto": "Auto-Review: tidak pernah membuka prompt. Kebijakan deterministik mengizinkan panggilan yang terbukti aman dan memblokir keras pekerjaan latar belakang yang bersifat publikasi atau destruktif; panggilan yang tidak dapat dibuktikan aman diteruskan ke penjaga model sekali jalan yang mengizinkan atau menolak dengan alasan (risiko tinggi atau kritis tidak pernah berjalan otomatis; tinjauan gagal berarti ditolak, fail closed). Penahanan yang memerlukan manusia ditolak, bukan disembunyikan. Setiap keputusan seperti itu ditulis ke transkrip sebagai catatan dan ke log audit.",
"PermissionsPostureBypass": "Full Access: panggilan tool biasa berjalan tanpa prompt. Penahanan keamanan, hukum repositori, dan kebijakan terkelola yang tidak dapat dilewati fail closed sebagai blokir keras alih-alih bertanya.",
"PermissionsPostureNever": "never: hanya tool yang dianggap aman/hanya-baca yang berjalan; selebihnya diblokir tanpa prompt.",
"PermissionsReceiptsNote": "Keputusan yang dibuat tanpa prompt (putusan penjaga Auto-Review, blokir, dan penahanan) muncul sebagai catatan transkrip dan di log audit pada {audit_path}. Full Access dipilih dengan sengaja lewat Shift+Tab atau /config, tidak pernah oleh aturan.",
"AgentFocusOpened": "Fokus pada {agent}. Pesan Anda kini menuju worker ini; Esc kembali ke percakapan utama.",
"AgentFocusClosed": "Kembali ke percakapan utama.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Pesan untuk {agent} · Esc kembali ke utama",
"AgentFocusNoTranscript": "Belum ada transkrip untuk {agent}. Pesan muncul di sini saat worker bertukar pesan.",
"AgentFocusOmitted": "Pesan sebelumnya ({count}) dihilangkan dari transkrip dalam memori.",
"AgentFocusFollowUpDelivered": "Diantrekan untuk {agent}: pesan dibaca pada putaran berikutnya.",
"AgentFocusFollowUpQueued": "Diantrekan untuk {agent}",
"AgentFocusFollowUpContinued": "{agent} sudah selesai; dilanjutkan pada fork baru ({target}). Tampilan ini kini mengikuti fork tersebut.",
"AgentFocusFollowUpFailed": "Tidak dapat mengirim ke {agent}: {reason}",
"FooterHintForAgents": "agen",
"FooterHintToManage": "kelola",
"AgentRailQueuedCount": "{count} antre",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "menulis",
"AgentFocusPostureReadOnly": "hanya-baca",
"AgentFocusPostureNetwork": "jaringan",
"AgentFocusPostureNoNetwork": "tanpa jaringan",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell hanya-baca",
"AgentFocusPostureShellNone": "tanpa shell",
"GoalReceiptSet": "Tujuan ditetapkan: \"{objective}\" · /goal menampilkan progres · /goal pause atau /goal clear menghentikannya",
"GoalStatusIdleHint": "tidak sedang berjalan — kirim pesan atau /goal resume untuk melanjutkan"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "タスクを書くか / を使う。",
"ComposerOperatePlaceholder": "目標を説明してください — Codewhale は完了するまで作業を続けます",
"ComposerDispatchFailedRestored": "メッセージを送信できませんでした({error})。入力欄に復元しました。",
"DispatchFailedQueued": "送信に失敗しました({error})。キューされたフォローアップを {count} 件保持しました。",
"DispatchFailedInitial": "初期プロンプトを送信できませんでした: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "現在のプロバイダ",
"ConfigLabelBaseUrlDeepseek": "プロバイダ API URLDeepSeek ルート)",
"ConfigLabelProviderUrl": "プロバイダ API URL",
"ConfigHintProviderUrl": "現在のプロバイダーエンドポイント。Xiaomi: トークンプラン | 従量課金 | カスタム URL",
"ConfigLabelModel": "現在のプロバイダモデル",
"ConfigLabelFastModel": "高速モデル(派生)",
"ConfigLabelDefaultModel": "旧互換モデル(DeepSeek ルートのみ)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "ターンごと · ローカル判定 · ルーター送信なし",
"ModelPickerAutoLastRoute": "前回 {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model}{source})· Ctrl+O: ルート詳細",
"CloudCodeSystemPromptUnsupported": "この接続はシステム指示にまだ対応していないため、Antigravity cloud-code はこのターンを安全に送信できません。何も送信されていません。別のプロバイダーを選んでください。",
"HelpTitle": "ヘルプ",
"HelpFilterPlaceholder": "入力して絞り込み",
"HelpFilterPrefix": "絞り込み: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "ファイルからセッションを読み込み",
"CmdLogoutDescription": "API キーを消去してセットアップに戻る",
"CmdMcpDescription": "MCP サーバを開く・管理する",
"McpRecommendedUnknownId": "推奨 MCP ID が不明です。{recommendations_command} で精選リストを確認してください。",
"McpRecommendationsSafety": "この一覧を見ても、何も追加・有効化されません。明示的な追加は設定を書き込むだけです。{restart_command} がサーバーへ接続する前に確認してください。",
"McpRecommendationGithub": "• github — GitHub 公式のリモート MCP エンドポイント\n エンドポイント: {endpoint}\n 認証は別です。サーバーが OAuth を提供する場合だけ {login_command} を使用してください。\n それ以外は、コマンド履歴の外で最小権限の PAT を設定してください。付与した\n スコープはリポジトリデータを書き込み・削除できるため、可能なら読み取り専用で始めてください。\n 明示的に追加: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — バージョン固定 npm パッケージによる公式 Chrome DevTools MCP\n パッケージ: {package} ({launcher})\n Chrome の調査・操作や認証済みページの読み取りが可能です。機密タブを\n 閉じ、追加前にパッケージを確認してください。{restart_command} はダウンロードして実行する場合があります。\n 明示的に追加: {add_command}",
"PluginKimiUsage": "使い方:\n {list_command}\n {approve_command}\n一覧表示は読み取り専用です。承認すると、Kimi 管理の正規プラグイン 1 件を確認済みインストーラーでコピーします。コピー後も無効・未信頼のままです。",
"PluginKimiManagedRootHeading": "{root} の Kimi 管理プラグイン:",
"PluginKimiNoneFound": "有効な管理プラグインが見つかりません。",
"PluginKimiLicenseUnspecified": "未指定",
"PluginKimiApplicable": "この OS で使用可能",
"PluginKimiNotApplicable": "この OS では使用不可",
"PluginKimiCandidateSummary": "{name} {version} — ライセンス={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " パス: {path}\n コンテンツハッシュ: {content_hash}\n 権限ハッシュ: {capability_hash}\n 承認: {approve_command}",
"PluginKimiRejectedHeading": "拒否された項目(インポート不可):",
"PluginKimiInspectionFooter": "この調査ではコピー、信頼、有効化、実行を行っていません。外部の Kimi アプリ、デーモン、バイナリ、ブラウザー拡張、認証情報、OS 権限も確認していません。",
"PluginKimiCandidateMissing": "`{name}` という有効な Kimi 管理の正規プラグインはありません。{list_command} を再実行してください。",
"PluginKimiCandidateChanged": "Kimi 管理プラグイン `{name}` は確認後に変更されました。期待したハッシュは {expected}、現在は {actual} です。コピーは行われていません。{list_command} を再実行してください。",
"PluginKimiHomeMissing": "Kimi インポート用のユーザーホームディレクトリが見つかりません。",
"PluginKimiRootInspectFailed": "Kimi 管理プラグインのルート {root} を調査できません: {error}",
"PluginKimiRootMustBeDirectory": "Kimi 管理プラグインのルート {root} は、リンクや再解析ポイントではなく実ディレクトリである必要があります。",
"PluginKimiRootCanonicalizeFailed": "Kimi 管理プラグインのルート {root} を正規化できません: {error}",
"PluginKimiRootListFailed": "Kimi 管理プラグインのルート {root} を一覧できません: {error}",
"PluginKimiEntryReadFailed": "Kimi 管理プラグインの項目を読み取れません: {error}",
"PluginKimiEntryLimit": "Kimi 管理プラグインのルートには {count} 件あります。1 回の走査で確認する上限は {max} 件です。",
"PluginKimiEntryInspectFailed": "{path}: 調査できません: {error}",
"PluginKimiEntryLinksRefused": "{path}: リンクと再解析ポイントは拒否されます",
"PluginKimiEntryOutsideRoot": "{path}: 正規パス {canonical_path} は管理ルートの直下ではありません",
"PluginKimiEntryCanonicalizeFailed": "{path}: 正規化できません: {error}",
"PluginKimiManifestUnreadable": "{path}: 読み取り可能な {manifest} がありません: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} は実在する通常ファイルである必要があります",
"PluginKimiManifestInvalid": "{path}: マニフェストが無効です: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: ディレクトリ名はマニフェスト名 `{name}` と完全に一致する必要があります",
"PluginKimiHashUnavailable": "利用不可",
"PluginKimiRollbackDestinationMissing": "インストーラーからコピー先パスが報告されませんでした。",
"PluginKimiMismatchRemoved": "コピー後のプラグイン `{name}` は承認済み内容と一致しません(期待値 {expected}、検出値 {actual})。予期しないコピーを削除しました。確認して再試行してください。",
"PluginKimiMismatchRollbackFailed": "エラー: コピー後のプラグイン `{name}` は承認済み内容と一致せず(期待値 {expected}、検出値 {actual})、自動削除にも失敗しました: {error}。無効・未信頼のままです。続行前に {path} を調査してください。",
"PluginKimiUserPluginDirectory": "ユーザーのプラグインディレクトリ",
"PluginKimiMarketplaceZipUnsupported": "Codewhale の確認済みインストーラーは Kimi ZIP バンドルに対応していません。ローカルディレクトリからインストールするか、上流の Kimi 管理プラグインをインポートしてください。",
"PluginKimiMarketplaceRemoteUnsupported": "Codewhale でインストールする Kimi リモートソースは .tar.gz または .tgz で終わる必要があります。.zip は認識されますが未対応です。",
"PluginKimiMarketplaceGzipTarball": "gzip tarball の URL",
"CmdPluginDescription": "信頼済みプラグインバンドルを確認・管理します。従来の実行可能ツールは別に扱われます",
"CmdPluginBundleUsage": "使い方: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Codewhale プラグインバンドルは見つかりませんでした。",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale はローカルソースをホスト型 Work にアップロード、移行、同期しません。{command} を使用して、GitHub または CNB で利用可能なブランチ先端から開始してください。未プッシュのコミット、変更済みまたは無視対象のファイル、シークレット、セッション状態はローカルに残ります。",
"CmdRemoteEnvBrowserLabel": "Codewhale ホスト型 Work",
"CmdRenameDescription": "現在のセッションの名前を変更",
"CmdTitleDescription": "現在のセッションと端末タブ/ウィンドウに名前を付ける",
"CmdRestoreDescription": "ワークスペースを以前のターン前/後スナップショットへロールバック。引数なしで最近のスナップショットを一覧表示。",
"CmdRetryDescription": "直前のリクエストを再試行",
"CmdReviewDescription": "ファイル・diff・PR に対して構造化コードレビューを実行",
@@ -990,6 +1033,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP はまだ対応が必要です。セットアップ報告に記録しました(初回実行はブロックしません)。",
"SetupToolsMcpPreviewTitle": "Tools / MCP 安全なオンランプ",
"SetupToolsMcpOnRampText": "Tools / MCP / スキル / プラグイン — 安全なオンランプ\n\n/setup はローカル在庫を読むだけです。MCP サーバー起動・スキルインストール・プラグイン実行・未信頼コマンドの自動実行は行いません。\n\n現状:\n- MCP: {mcp_result}\n- スキル: {skills_result}\n- ツール: {tools_result}\n- プラグイン: {plugins_result}\n- ホットバー(共有アダプタ): {hotbar_result}\n\nパス:\n- MCP: {mcp_path}\n- スキル: {skills_path}\n- プラグイン: {plugins_path}\n\n安全なブートストラップ(明示実行):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- スキル: /skills · codewhale setup --skills\n- プラグイン: /plugin · codewhale setup --plugins\n- ツール: codewhale setup --tools\n\n副作用のある操作は常に確認が必要です。詳細は docs/MCP.md を参照してください。",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — Codewhale 経由で接続。第二のスケジューラではありません:\n- 状態: {dsh_result}\n- 読み取り専用で検出。接続/計画/起動/削除: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale が書き込むのは $CODEWHALE_HOME/integrations/dsh のみ。API キーのコピーや DSH ファイルの編集は行いません。",
"HotbarActionModeOperateName": "Operate モード",
"HotbarActionModeOperateDescription": "Fleet を並列で動かします。",
"HomeOperateModeTip": "Operate — Fleet を並列で動かす",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet 準備完了",
"EmptyStateHelpConnector": "または",
"EmptyStateHelpHint": "— すべてを表示",
"SessionsSurfaceTitle": "セッション",
"SessionsPaneTitle": " セッション (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "既存プロファイルの識別情報を確認できません({error})。記載のファイルを修正してから保存してください。",
"FleetProfileIdConflict": "プロファイル id `{id}` は {path} で既に使用されています。別のロールで作り直すか、先に古いファイルを削除してください。",
"FleetProfileProviderUnconfigured": "プロファイルはプロバイダー `{provider}` を指定していますが、認証情報が未設定です({env})。保存する前に /provider で設定してください。",
"FleetDestStepTitle": "このプロファイルをどこに保存しますか?",
"FleetDestStepSubtitle": "最後のステップで確定するまで何も書き込まれません。",
"FleetDestProjectLabel": "このプロジェクト",
"FleetDestPersonalLabel": "個人用",
"FleetDestProjectSummary": "このプロジェクトのみ",
"FleetDestPersonalSummary": "すべてのプロジェクトで利用可能",
"FleetDestProjectDescription": "このプロジェクト({workspace})の中に保存されます。ここでのみ適用され、同じ ID の個人用プロファイルより優先されます。",
"FleetDestPersonalDescription": "Codewhale ホームに保存されます。すべてのプロジェクトで適用されますが、同じ ID のプロジェクト用プロファイルがある場合はそちらが優先されます。",
"FleetDestPathLine": "ファイル: {path}",
"FleetDestUnavailable": "利用できません: {reason}",
"FleetDestReasonNoProjectConfig": "このセッションではプロジェクト用プロファイルが無効です (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "ワークスペースフォルダー {path} が存在しないか、ディレクトリではありません",
"FleetDestReasonHomeUnavailable": "Codewhale ホームを解決できませんでした ({error})",
"FleetDestWillReplace": "既存のファイル {path} を置き換えます",
"FleetDestOverridesProject": "このプロジェクトには既に '{id}' プロファイルがあり、ここではそちらが優先されます。この個人用プロファイルは他のプロジェクトで適用されます。",
"FleetDestOverridesPersonal": "このプロジェクト内では、個人用の '{id}' プロファイルより優先されます。",
"FleetDestOverridesBuiltIn": "ロスターの {origin} '{id}' ロールを置き換えます。",
"FleetSavesToChip": "保存先: {scope} · {path}",
"FleetSavesToUndecided": "保存先: ステップ 3 で選択 — このプロジェクト または 個人用",
"FleetActionSaveProject": "このプロジェクトに保存",
"FleetActionSavePersonal": "個人用プロファイルとして保存",
"FleetActionReplaceProject": "このプロジェクト内で置き換え",
"FleetActionReplacePersonal": "個人用プロファイルを置き換え",
"FleetActionConfirmReplace": "{file} を置き換えるには、もう一度 Enter を押してください",
"FleetActionChangeDestination": "保存先を変更",
"FleetActionBack": "戻る",
"FleetReviewSavesTo": "保存先",
"FleetModelRowBlockedNotice": "選択できません: {reason}。/provider で設定するか、別の行を選んでください。",
"FleetDestProjectDisabledSave": "このセッションではプロジェクト用プロファイルが無効です (--no-project-config)。何も保存されませんでした。個人用を選ぶか、フラグなしで再起動してください。",
"WorkflowStatusWaiting": "待機中",
"WorkflowDebrief": "総括: {done}/{total} 確定 · {failed} 失敗 · {cancelled} キャンセル · {elapsed}",
"WorkflowTranscriptDetails": "トランスクリプト: 完全な実行 JSON はツール詳細で表示できます ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "自動化 {id} の実行をキューに追加しました: {status}(タスク {task}",
"AutomationDeletePreview": "削除はまだ確定していません。何も削除されていません。\n自動化: {id}{name}\n実行記録: {run_count}\n定義と実行履歴を削除するには、次を実行してください:\n{command}",
"AutomationDeleteConfirmationStale": "削除確認が自動化 {id} の現在の状態と一致しないため、何も削除されていません。{command} で現在の状態を確認してください。",
"AutomationDeleted": "自動化 {id}({name})を削除しました。削除した実行記録: {run_count}。"
"AutomationDeleted": "自動化 {id}({name})を削除しました。削除した実行記録: {run_count}。",
"WhaleStateResting": "休止中",
"WhaleStateThinking": "思考中",
"WhaleStateWorking": "作業中",
"WhaleStateWaiting": "あなたの操作待ち",
"WhaleStateBlocked": "ブロック中",
"WhaleStateOffline": "オフライン",
"WhaleAnimalScout": "アカボウクジラ",
"WhaleAnimalPatch": "ネズミイルカ",
"WhaleAnimalHarbor": "ザトウクジラ",
"WhaleAnimalEcho": "ゴンドウクジラ",
"WhaleAnimalKeel": "マッコウクジラ",
"WhaleAnimalLantern": "シャチ",
"WhaleAnimalPlain": "クジラ",
"WhaleJobScout": "調査",
"WhaleJobPatch": "コーディング",
"WhaleJobHarbor": "調整",
"WhaleJobEcho": "連絡",
"WhaleJobKeel": "運用",
"WhaleJobLantern": "レビュー",
"WhaleJobPlain": "一般作業",
"SessionMetricsTurn": "ターン",
"SessionMetricsTurns": "ターン",
"SessionMetricsStep": "ステップ",
"SessionMetricsSteps": "ステップ",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "ツール呼出",
"SessionMetricsTtft": "TTFT平均",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "キャッシュ命中",
"SessionMetricsInput": "入力",
"SessionMetricsStatusLine": "セッション指標: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review が '{tool}' を許可しました(リスク {risk}、モデルガーディアン): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review が '{tool}' を拒否しました(リスク {risk}、モデルガーディアン): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review は '{tool}' を審査できませんでした({reason})。フェイルクローズで拒否",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review が '{tool}' をブロックしました(決定論的ポリシー): {reason}",
"AutoReviewReceiptHeld": "Auto-Review は一時停止せずに '{tool}' を保留し、拒否しました(人の判断が必要 — Ask に切り替え)",
"FooterHintEscInterrupt": "Esc で中断",
"PermissionsPostureHeader": "現在の権限ポスチャ: {posture}",
"PermissionsPostureAsk": "Ask: 権限・コスト・範囲・結果を変える tool 呼び出しはプロンプトを開きます。安全と証明された読み取り専用の呼び出しはプロンプトなしで実行されます。上記の ask ルールは常にプロンプトを強制します。",
"PermissionsPostureAuto": "Auto-Review: プロンプトは開きません。決定論的ポリシーが安全と証明された呼び出しを許可し、公開系や破壊的なバックグラウンド作業をハードブロックします。安全と証明できない呼び出しは一回限りのモデルガーディアンに送られ、理由付きで許可または拒否されます(高・重大リスクは自動実行されません。審査失敗はフェイルクローズで拒否)。人の判断が必要な保留は隠されず拒否されます。各判断はトランスクリプトのノートと監査ログに記録されます。",
"PermissionsPostureBypass": "Full Access: 通常の tool 呼び出しはプロンプトなしで実行されます。回避不能な安全・リポジトリ法・管理ポリシーの保留は、プロンプトの代わりにハードブロックとしてフェイルクローズします。",
"PermissionsPostureNever": "never: 安全・読み取り専用とみなされる tool のみ実行され、それ以外はプロンプトなしでブロックされます。",
"PermissionsReceiptsNote": "プロンプトなしで下された判断(Auto-Review ガーディアンの裁定、ブロック、保留)はトランスクリプトのノートと {audit_path} の監査ログに表示されます。Full Access は Shift+Tab または /config で意図的に選ぶものであり、ルールで選ばれることはありません。",
"AgentFocusOpened": "{agent} にフォーカスしました。以後のメッセージはこのワーカーに届きます。Esc でメイン会話に戻ります。",
"AgentFocusClosed": "メイン会話に戻りました。",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "{agent} にメッセージ · Esc でメインに戻る",
"AgentFocusNoTranscript": "{agent} のトランスクリプトはまだありません。ワーカーがやり取りを始めるとここに表示されます。",
"AgentFocusOmitted": "以前のメッセージ({count} 件)はメモリ内トランスクリプトから省略されています。",
"AgentFocusFollowUpDelivered": "{agent} 宛にキューしました。次のラウンドで読み取られます。",
"AgentFocusFollowUpQueued": "{agent} 宛にキュー済み",
"AgentFocusFollowUpContinued": "{agent} は完了済みだったため、新しいフォーク({target})で続行しました。この画面はフォークを追跡します。",
"AgentFocusFollowUpFailed": "{agent} に届けられませんでした: {reason}",
"FooterHintForAgents": "エージェント",
"FooterHintToManage": "管理",
"AgentRailQueuedCount": "{count} 件キュー",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "書き込み可",
"AgentFocusPostureReadOnly": "読み取り専用",
"AgentFocusPostureNetwork": "ネットワーク可",
"AgentFocusPostureNoNetwork": "ネットワーク不可",
"AgentFocusPostureShellFull": "シェル可",
"AgentFocusPostureShellReadOnly": "読み取り専用シェル",
"AgentFocusPostureShellNone": "シェル不可",
"GoalReceiptSet": "目標を設定: 「{objective}」 · /goal で進捗表示 · /goal pause または /goal clear で停止",
"GoalStatusIdleHint": "現在は実行中ではありません — メッセージを送るか /goal resume で続行"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "작업을 입력하거나 /를 사용하세요.",
"ComposerOperatePlaceholder": "목표를 설명하세요 — Codewhale이 완료될 때까지 계속 작업합니다",
"ComposerDispatchFailedRestored": "메시지를 보내지 못했습니다({error}). 작성란에 복원했습니다.",
"DispatchFailedQueued": "전송 실패 ({error}); 대기 중인 후속 메시지 {count}개를 보존했습니다.",
"DispatchFailedInitial": "초기 프롬프트를 전송할 수 없습니다: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "현재 프로바이더",
"ConfigLabelBaseUrlDeepseek": "프로바이더 API URL (DeepSeek 경로)",
"ConfigLabelProviderUrl": "프로바이더 API URL",
"ConfigHintProviderUrl": "현재 제공자 엔드포인트; Xiaomi: 토큰 요금제 | 사용량 기반 결제 | 사용자 지정 URL",
"ConfigLabelModel": "현재 프로바이더 모델",
"ConfigLabelFastModel": "빠른 모델 (파생됨)",
"ConfigLabelDefaultModel": "레거시 대체 모델 (DeepSeek 경로만)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "턴별 · 로컬 휴리스틱 · 라우터 요청 없음",
"ModelPickerAutoLastRoute": "최근 {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} ({source}) · Ctrl+O: 경로 세부 정보",
"CloudCodeSystemPromptUnsupported": "이 연결은 아직 시스템 지시를 지원하지 않으므로 Antigravity cloud-code가 이 턴을 안전하게 보낼 수 없습니다. 아무것도 보내지 않았습니다. 다른 공급자를 선택하세요.",
"HelpTitle": "도움말",
"HelpFilterPlaceholder": "입력하여 필터링",
"HelpFilterPrefix": "필터: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "파일에서 세션을 불러옵니다",
"CmdLogoutDescription": "API 키를 지우고 설정 화면으로 돌아갑니다",
"CmdMcpDescription": "MCP 서버를 열거나 관리합니다",
"McpRecommendedUnknownId": "알 수 없는 권장 MCP ID입니다. {recommendations_command} 명령으로 선별 목록을 확인하세요.",
"McpRecommendationsSafety": "이 목록을 보는 것만으로는 아무것도 추가하거나 활성화하지 않습니다. 명시적 추가는 설정만 기록합니다. {restart_command} 명령이 서버를 연결하기 전에 검토하세요.",
"McpRecommendationGithub": "• github — GitHub 공식 원격 MCP 엔드포인트\n 엔드포인트: {endpoint}\n 인증은 별도입니다. 서버가 OAuth를 제공할 때만 {login_command} 명령을 사용하세요.\n 그 외에는 명령 기록 밖에서 최소 권한 PAT를 설정하세요. 부여한 범위는\n 저장소 데이터를 쓰거나 삭제할 수 있으므로 가능하면 읽기 전용으로 시작하세요.\n 명시적으로 추가: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — 버전을 고정한 npm 패키지 기반 공식 Chrome DevTools MCP\n 패키지: {package} ({launcher})\n Chrome을 검사/제어하고 인증된 페이지를 읽을 수 있습니다. 민감한 탭을\n 닫고 추가 전에 패키지를 확인하세요. {restart_command} 명령이 다운로드하여 실행할 수 있습니다.\n 명시적으로 추가: {add_command}",
"PluginKimiUsage": "사용법:\n {list_command}\n {approve_command}\n목록은 읽기 전용입니다. 승인하면 Kimi 관리 정식 플러그인 하나를 검토된 설치 프로그램으로 복사하며, 비활성 및 신뢰되지 않은 상태를 유지합니다.",
"PluginKimiManagedRootHeading": "{root}의 Kimi 관리 플러그인:",
"PluginKimiNoneFound": "유효한 관리 플러그인이 없습니다.",
"PluginKimiLicenseUnspecified": "지정되지 않음",
"PluginKimiApplicable": "이 OS에서 사용 가능",
"PluginKimiNotApplicable": "이 OS에서 사용 불가",
"PluginKimiCandidateSummary": "{name} {version} — 라이선스={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " 경로: {path}\n 콘텐츠 해시: {content_hash}\n 기능 해시: {capability_hash}\n 승인: {approve_command}",
"PluginKimiRejectedHeading": "거부된 항목(가져올 수 없음):",
"PluginKimiInspectionFooter": "이 검사에서는 아무것도 복사, 신뢰, 활성화 또는 실행하지 않았습니다. 외부 Kimi 앱, 데몬, 바이너리, 브라우저 확장, 자격 증명 및 OS 권한도 확인하지 않았습니다.",
"PluginKimiCandidateMissing": "`{name}`이라는 유효한 Kimi 관리 정식 플러그인이 없습니다. {list_command} 명령을 다시 실행하세요.",
"PluginKimiCandidateChanged": "Kimi 관리 플러그인 `{name}`이 검토 후 변경되었습니다. 예상 해시는 {expected}, 현재 해시는 {actual}입니다. 복사하지 않았습니다. {list_command} 명령을 다시 실행하세요.",
"PluginKimiHomeMissing": "Kimi 가져오기에 사용할 사용자 홈 디렉터리를 찾을 수 없습니다.",
"PluginKimiRootInspectFailed": "Kimi 관리 플러그인 루트 {root}을(를) 검사할 수 없습니다: {error}",
"PluginKimiRootMustBeDirectory": "Kimi 관리 플러그인 루트 {root}은(는) 링크나 재분석 지점이 아닌 실제 디렉터리여야 합니다.",
"PluginKimiRootCanonicalizeFailed": "Kimi 관리 플러그인 루트 {root}을(를) 정규화할 수 없습니다: {error}",
"PluginKimiRootListFailed": "Kimi 관리 플러그인 루트 {root}의 목록을 볼 수 없습니다: {error}",
"PluginKimiEntryReadFailed": "Kimi 관리 플러그인 항목을 읽을 수 없습니다: {error}",
"PluginKimiEntryLimit": "Kimi 관리 플러그인 루트에 항목이 {count}개 있습니다. 한 번에 검토하는 최대 수는 {max}개입니다.",
"PluginKimiEntryInspectFailed": "{path}: 검사할 수 없음: {error}",
"PluginKimiEntryLinksRefused": "{path}: 링크와 재분석 지점은 거부됩니다",
"PluginKimiEntryOutsideRoot": "{path}: 정규 경로 {canonical_path}은(는) 관리 루트의 직접 하위가 아닙니다",
"PluginKimiEntryCanonicalizeFailed": "{path}: 정규화할 수 없음: {error}",
"PluginKimiManifestUnreadable": "{path}: 읽을 수 있는 {manifest} 없음: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest}은(는) 실제 일반 파일이어야 합니다",
"PluginKimiManifestInvalid": "{path}: 매니페스트가 잘못됨: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: 디렉터리 이름은 매니페스트 이름 `{name}`과 정확히 일치해야 합니다",
"PluginKimiHashUnavailable": "사용할 수 없음",
"PluginKimiRollbackDestinationMissing": "설치 프로그램이 대상 경로를 보고하지 않았습니다.",
"PluginKimiMismatchRemoved": "복사된 플러그인 `{name}`이 승인된 콘텐츠와 일치하지 않습니다(예상 {expected}, 발견 {actual}). 예상치 못한 복사본을 제거했습니다. 검토 후 다시 시도하세요.",
"PluginKimiMismatchRollbackFailed": "오류: 복사된 플러그인 `{name}`이 승인된 콘텐츠와 일치하지 않고(예상 {expected}, 발견 {actual}) 자동 제거도 실패했습니다: {error}. 비활성 및 신뢰되지 않은 상태입니다. 계속하기 전에 {path}을(를) 검사하세요.",
"PluginKimiUserPluginDirectory": "사용자 플러그인 디렉터리",
"PluginKimiMarketplaceZipUnsupported": "Codewhale의 검토된 설치 프로그램은 Kimi ZIP 번들을 지원하지 않습니다. 로컬 디렉터리에서 설치하거나 업스트림 Kimi 관리 플러그인을 가져오세요.",
"PluginKimiMarketplaceRemoteUnsupported": "Codewhale에서 설치할 Kimi 원격 소스는 .tar.gz 또는 .tgz로 끝나야 합니다. .zip은 인식하지만 지원하지 않습니다.",
"PluginKimiMarketplaceGzipTarball": "gzip tarball 주소",
"CmdPluginDescription": "신뢰한 플러그인 번들을 검사하고 관리합니다. 기존 실행형 도구는 별도로 유지됩니다",
"CmdPluginBundleUsage": "사용법: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Codewhale 플러그인 번들을 찾지 못했습니다.",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale은 로컬 소스를 호스팅 Work에 업로드, 마이그레이션 또는 동기화하지 않습니다. {command}을 사용하여 GitHub 또는 CNB에서 사용할 수 있는 브랜치 끝에서 시작하세요. 푸시하지 않은 커밋, 변경되었거나 무시된 파일, 비밀, 세션 상태는 로컬에 남습니다.",
"CmdRemoteEnvBrowserLabel": "Codewhale 호스팅 Work",
"CmdRenameDescription": "현재 세션 이름을 바꿉니다",
"CmdTitleDescription": "현재 세션과 터미널 탭/창의 이름을 설정",
"CmdRestoreDescription": "작업 공간을 이전 턴 전/후 스냅샷으로 되돌립니다. 인자가 없으면 최근 스냅샷 목록을 표시합니다.",
"CmdRetryDescription": "마지막 요청을 재시도합니다",
"CmdReviewDescription": "파일, diff, PR에 대해 구조화된 코드 리뷰를 실행합니다",
@@ -992,6 +1035,8 @@
"SetupToolsMcpNeedsActionSaved": "도구/MCP에 아직 조치가 필요합니다. 설정 리포트에 기록했습니다 (최초 실행을 막지는 않음).",
"SetupToolsMcpPreviewTitle": "도구 / MCP 안전 온램프",
"SetupToolsMcpOnRampText": "도구, MCP, 스킬 및 플러그인 — 안전한 온램프\n\n/setup은 로컬 인벤토리만 읽습니다. MCP 서버를 시작하거나, 스킬을 설치하거나, 플러그인을 실행하거나, 신뢰할 수 없는 명령을 실행하는 일은 절대 없습니다.\n\n현재 인벤토리:\n- MCP: {mcp_result}\n- 스킬: {skills_result}\n- 도구 디렉터리: {tools_result}\n- 플러그인: {plugins_result}\n- 핫바 (공유 어댑터): {hotbar_result}\n\n경로 (홈 디렉터리 가림):\n- MCP 설정: {mcp_path}\n- 스킬: {skills_path}\n- 플러그인: {plugins_path}\n\n안전한 부트스트랩 (일반 터미널이나 TUI 명령에서 직접 실행하세요):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- 스킬: /skills · codewhale setup --skills · /skills install <spec>\n- 플러그인: /plugin · codewhale setup --plugins\n- 도구 디렉터리: codewhale setup --tools\n\n부작용이 있는 동작은 항상 명시적 확인이 필요합니다. 플러그인 명령어는 슬래시 명령어와는 별개이며, 핫바 플러그인 소스는 승인 게이트가 도입될 때까지 보류됩니다.\n\n아직 수동으로 외부 설정이 필요한 항목은 docs/MCP.md와 docs/skills/README.md를 참고하세요.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — Codewhale를 통해 연결되며 두 번째 스케줄러가 아닙니다:\n- 상태: {dsh_result}\n- 읽기 전용 감지; 연결/계획/실행/제거: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale는 $CODEWHALE_HOME/integrations/dsh 에만 기록하며 API 키를 복사하거나 DSH 파일을 수정하지 않습니다.",
"HotbarActionModeOperateName": "운영 모드",
"HotbarActionModeOperateDescription": "Fleet를 병렬로 작업에 투입합니다.",
"HomeOperateModeTip": "Operate — Fleet를 병렬로 작업에 투입",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet 준비 완료",
"EmptyStateHelpConnector": "또는",
"EmptyStateHelpHint": "— 모두 보기",
"SessionsSurfaceTitle": "세션",
"SessionsPaneTitle": " 세션 (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "기존 프로필 식별 정보를 확인할 수 없습니다({error}). 표시된 파일을 수정한 뒤 저장하세요.",
"FleetProfileIdConflict": "프로필 id `{id}`는 이미 {path}에서 사용 중입니다. 다른 역할로 다시 작성하거나 이전 파일을 먼저 제거하세요.",
"FleetProfileProviderUnconfigured": "프로필이 자격 증명이 설정되지 않은 공급자 `{provider}`를 지정합니다({env}). 저장하기 전에 /provider에서 설정하세요.",
"FleetDestStepTitle": "이 프로필을 어디에 저장할까요?",
"FleetDestStepSubtitle": "마지막 단계에서 확인하기 전까지는 아무것도 기록되지 않습니다.",
"FleetDestProjectLabel": "이 프로젝트",
"FleetDestPersonalLabel": "개인",
"FleetDestProjectSummary": "이 프로젝트에서만",
"FleetDestPersonalSummary": "모든 프로젝트에서 사용 가능",
"FleetDestProjectDescription": "이 프로젝트({workspace}) 안에 저장됩니다. 여기에서만 적용되며, 같은 ID의 개인 프로필보다 우선합니다.",
"FleetDestPersonalDescription": "Codewhale 홈에 저장됩니다. 모든 프로젝트에 적용되지만, 같은 ID의 프로젝트 프로필이 있는 곳에서는 그 프로필이 우선합니다.",
"FleetDestPathLine": "파일: {path}",
"FleetDestUnavailable": "사용 불가: {reason}",
"FleetDestReasonNoProjectConfig": "이 세션에서는 프로젝트 프로필이 비활성화되어 있습니다 (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "워크스페이스 폴더 {path}이(가) 없거나 디렉터리가 아닙니다",
"FleetDestReasonHomeUnavailable": "Codewhale 홈을 확인할 수 없습니다 ({error})",
"FleetDestWillReplace": "기존 파일 {path}을(를) 교체합니다",
"FleetDestOverridesProject": "이 프로젝트에는 이미 '{id}' 프로필이 있어 여기서는 그것이 우선합니다. 이 개인 프로필은 다른 프로젝트에서 적용됩니다.",
"FleetDestOverridesPersonal": "이 프로젝트 안에서는 개인 '{id}' 프로필보다 우선합니다.",
"FleetDestOverridesBuiltIn": "로스터의 {origin} '{id}' 역할을 대체합니다.",
"FleetSavesToChip": "저장 위치: {scope} · {path}",
"FleetSavesToUndecided": "저장 위치: 3단계에서 선택 — 이 프로젝트 또는 개인",
"FleetActionSaveProject": "이 프로젝트에 저장",
"FleetActionSavePersonal": "개인 프로필로 저장",
"FleetActionReplaceProject": "이 프로젝트에서 교체",
"FleetActionReplacePersonal": "개인 프로필 교체",
"FleetActionConfirmReplace": "{file}을(를) 교체하려면 Enter를 한 번 더 누르세요",
"FleetActionChangeDestination": "저장 위치 변경",
"FleetActionBack": "뒤로",
"FleetReviewSavesTo": "저장 위치",
"FleetModelRowBlockedNotice": "선택할 수 없음: {reason}. /provider에서 설정하거나 다른 행을 고르세요.",
"FleetDestProjectDisabledSave": "이 세션에서는 프로젝트 프로필이 비활성화되어 있습니다 (--no-project-config). 아무것도 저장되지 않았습니다. 개인을 선택하거나 플래그 없이 다시 시작하세요.",
"WorkflowStatusWaiting": "대기",
"WorkflowDebrief": "결과 보고: {done}/{total} 종결 · {failed} 실패 · {cancelled} 취소 · {elapsed}",
"WorkflowTranscriptDetails": "트랜스크립트: 전체 실행 JSON은 도구 상세에서 확인할 수 있습니다 ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "자동화 {id} 실행이 대기열에 추가됨: {status} (작업 {task})",
"AutomationDeletePreview": "삭제가 아직 확인되지 않았습니다. 삭제된 항목이 없습니다.\n자동화: {id} ({name})\n기록된 실행: {run_count}\n정의와 실행 기록을 삭제하려면 다음을 실행하세요:\n{command}",
"AutomationDeleteConfirmationStale": "삭제 확인이 자동화 {id}의 현재 상태와 더 이상 일치하지 않습니다. 삭제된 항목이 없습니다. {command}로 현재 상태를 검토하세요.",
"AutomationDeleted": "자동화 {id} ({name})을(를) 삭제했습니다. 삭제된 실행 기록: {run_count}."
"AutomationDeleted": "자동화 {id} ({name})을(를) 삭제했습니다. 삭제된 실행 기록: {run_count}.",
"WhaleStateResting": "휴식 중",
"WhaleStateThinking": "생각 중",
"WhaleStateWorking": "작업 중",
"WhaleStateWaiting": "당신을 기다리는 중",
"WhaleStateBlocked": "차단됨",
"WhaleStateOffline": "오프라인",
"WhaleAnimalScout": "부리고래",
"WhaleAnimalPatch": "쇠돌고래",
"WhaleAnimalHarbor": "혹등고래",
"WhaleAnimalEcho": "들쇠고래",
"WhaleAnimalKeel": "향유고래",
"WhaleAnimalLantern": "범고래",
"WhaleAnimalPlain": "고래",
"WhaleJobScout": "조사",
"WhaleJobPatch": "코딩",
"WhaleJobHarbor": "조율",
"WhaleJobEcho": "소통",
"WhaleJobKeel": "운영",
"WhaleJobLantern": "리뷰",
"WhaleJobPlain": "일반 작업",
"SessionMetricsTurn": "턴",
"SessionMetricsTurns": "턴",
"SessionMetricsStep": "단계",
"SessionMetricsSteps": "단계",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "도구 호출",
"SessionMetricsTtft": "TTFT 평균",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "캐시 적중",
"SessionMetricsInput": "입력",
"SessionMetricsStatusLine": "세션 지표: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review가 '{tool}'을(를) 허용했습니다 (위험 {risk}, 모델 가디언): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review가 '{tool}'을(를) 거부했습니다 (위험 {risk}, 모델 가디언): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review가 '{tool}'을(를) 검토할 수 없었습니다 ({reason}); 거부됨, 페일 클로즈",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review가 '{tool}'을(를) 차단했습니다 (결정적 정책): {reason}",
"AutoReviewReceiptHeld": "Auto-Review가 일시 중지 없이 '{tool}'을(를) 보류했습니다; 거부됨 (사람이 필요 — Ask로 전환)",
"FooterHintEscInterrupt": "Esc로 중단",
"PermissionsPostureHeader": "현재 권한 태세: {posture}",
"PermissionsPostureAsk": "Ask: 권한, 비용, 범위 또는 결과를 바꾸는 tool 호출은 프롬프트를 엽니다. 안전이 입증된 읽기 전용 호출은 프롬프트 없이 실행됩니다. 위의 ask 규칙은 항상 프롬프트를 강제합니다.",
"PermissionsPostureAuto": "Auto-Review: 프롬프트를 열지 않습니다. 결정적 정책이 안전이 입증된 호출을 허용하고 게시성 또는 파괴적 백그라운드 작업을 강제 차단합니다. 안전을 입증할 수 없는 호출은 1회성 모델 가디언에게 넘어가 사유와 함께 허용 또는 거부됩니다(높음 또는 치명적 위험은 절대 자동 실행되지 않으며, 검토 실패 시 페일 클로즈로 거부). 사람이 필요한 보류는 숨기지 않고 거부합니다. 이런 결정은 모두 대화 기록에 노트로, 감사 로그에 기록됩니다.",
"PermissionsPostureBypass": "Full Access: 일반 tool 호출은 프롬프트 없이 실행됩니다. 우회할 수 없는 안전, 저장소 규칙, 관리 정책 보류는 프롬프트 대신 강제 차단으로 페일 클로즈됩니다.",
"PermissionsPostureNever": "never: 안전/읽기 전용으로 간주되는 tool만 실행되며, 나머지는 프롬프트 없이 차단됩니다.",
"PermissionsReceiptsNote": "프롬프트 없이 내린 결정(Auto-Review 가디언 판정, 차단, 보류)은 대화 기록의 노트와 {audit_path}의 감사 로그에 표시됩니다. Full Access는 Shift+Tab 또는 /config로 의도적으로 선택하며, 규칙에 의해 선택되지 않습니다.",
"AgentFocusOpened": "{agent}에 포커스했습니다. 이제 메시지가 이 워커에게 전달됩니다. Esc로 메인 대화로 돌아갑니다.",
"AgentFocusClosed": "메인 대화로 돌아왔습니다.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "{agent}에게 메시지 · Esc로 메인으로",
"AgentFocusNoTranscript": "{agent}의 대화 기록이 아직 없습니다. 워커가 메시지를 주고받으면 여기에 표시됩니다.",
"AgentFocusOmitted": "이전 메시지 {count}개가 메모리 내 기록에서 생략되었습니다.",
"AgentFocusFollowUpDelivered": "{agent}에게 대기열에 넣었습니다. 다음 라운드에서 읽습니다.",
"AgentFocusFollowUpQueued": "{agent} 대기열",
"AgentFocusFollowUpContinued": "{agent}은(는) 이미 완료되어 새 포크({target})에서 계속합니다. 이 화면은 이제 그 포크를 따라갑니다.",
"AgentFocusFollowUpFailed": "{agent}에게 전달할 수 없습니다: {reason}",
"FooterHintForAgents": "에이전트",
"FooterHintToManage": "관리",
"AgentRailQueuedCount": "{count}개 대기",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "쓰기 가능",
"AgentFocusPostureReadOnly": "읽기 전용",
"AgentFocusPostureNetwork": "네트워크 가능",
"AgentFocusPostureNoNetwork": "네트워크 없음",
"AgentFocusPostureShellFull": "셸 가능",
"AgentFocusPostureShellReadOnly": "읽기 전용 셸",
"AgentFocusPostureShellNone": "셸 없음",
"GoalReceiptSet": "목표 설정: \"{objective}\" · /goal 로 진행 상황 확인 · /goal pause 또는 /goal clear 로 중지",
"GoalStatusIdleHint": "지금은 실행 중이 아님 — 메시지를 보내거나 /goal resume 로 계속"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Escreva uma tarefa ou use /.",
"ComposerOperatePlaceholder": "Descreva o objetivo — o Codewhale continua trabalhando até concluí-lo",
"ComposerDispatchFailedRestored": "Mensagem não enviada ({error}); restaurada no compositor.",
"DispatchFailedQueued": "Falha ao enviar ({error}); {count} acompanhamentos na fila mantidos.",
"DispatchFailedInitial": "Não foi possível enviar o prompt inicial: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Provedor ativo",
"ConfigLabelBaseUrlDeepseek": "URL da API do provedor (rota DeepSeek)",
"ConfigLabelProviderUrl": "URL da API do provedor",
"ConfigHintProviderUrl": "Endpoint atual do provedor; Xiaomi: plano de tokens | pagamento conforme o uso | URL personalizada",
"ConfigLabelModel": "Modelo ativo do provedor",
"ConfigLabelFastModel": "Modelo rápido (derivado)",
"ConfigLabelDefaultModel": "Modelo alternativo legado (somente rotas DeepSeek)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "por turno · heurística local · sem solicitação ao roteador",
"ModelPickerAutoLastRoute": "última {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} via {source} · Ctrl+O: detalhes da rota",
"CloudCodeSystemPromptUnsupported": "O Antigravity cloud-code não pode enviar este turno com segurança porque esta conexão ainda não aceita instruções de sistema. Nada foi enviado; escolha outro provedor.",
"HelpTitle": "Ajuda",
"HelpFilterPlaceholder": "Digite para filtrar",
"HelpFilterPrefix": "Filtro: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "Carregar a sessão de um arquivo",
"CmdLogoutDescription": "Limpar a chave de API e voltar à configuração",
"CmdMcpDescription": "Abrir ou gerenciar servidores MCP",
"McpRecommendedUnknownId": "ID de MCP recomendado desconhecido. Execute {recommendations_command} para conferir a lista selecionada.",
"McpRecommendationsSafety": "Ver esta lista não adiciona nem ativa nada. Uma adição explícita só grava a configuração; revise-a antes que {restart_command} conecte o servidor.",
"McpRecommendationGithub": "• github — endpoint MCP remoto oficial do GitHub\n endpoint: {endpoint}\n a autenticação é separada: use {login_command} somente se o servidor anunciar OAuth;\n caso contrário, configure um PAT com privilégios mínimos fora do histórico de comandos. Os\n escopos concedidos podem gravar ou excluir dados do repositório; comece em modo somente leitura quando possível.\n adicionar explicitamente: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP oficial do Chrome DevTools via pacote npm com versão fixada\n pacote: {package} ({launcher})\n ele pode inspecionar/controlar o Chrome e ler páginas autenticadas. Feche abas\n confidenciais e verifique o pacote antes de adicioná-lo; {restart_command} pode baixá-lo e executá-lo.\n adicionar explicitamente: {add_command}",
"PluginKimiUsage": "Uso:\n {list_command}\n {approve_command}\nA listagem é somente leitura. A aprovação copia um plugin canônico gerenciado pelo Kimi pelo instalador revisado; ele permanece desativado e não confiável.",
"PluginKimiManagedRootHeading": "Plugins gerenciados pelo Kimi em {root}:",
"PluginKimiNoneFound": "Nenhum plugin gerenciado válido foi encontrado.",
"PluginKimiLicenseUnspecified": "não informada",
"PluginKimiApplicable": "aplicável neste SO",
"PluginKimiNotApplicable": "não aplicável neste SO",
"PluginKimiCandidateSummary": "{name} {version} — licença={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " caminho: {path}\n hash do conteúdo: {content_hash}\n hash de capacidades: {capability_hash}\n aprovar: {approve_command}",
"PluginKimiRejectedHeading": "Entradas rejeitadas (não importáveis):",
"PluginKimiInspectionFooter": "Esta inspeção não copiou, tornou confiável, ativou nem executou nada. Apps, daemons, binários, extensões do navegador, credenciais e permissões do SO externos do Kimi não foram verificados.",
"PluginKimiCandidateMissing": "Não há um plugin canônico válido gerenciado pelo Kimi chamado `{name}`. Execute {list_command} novamente.",
"PluginKimiCandidateChanged": "O plugin gerenciado pelo Kimi `{name}` mudou desde a revisão. Hash esperado: {expected}; atual: {actual}. Nada foi copiado; execute {list_command} novamente.",
"PluginKimiHomeMissing": "Não foi possível localizar a pasta pessoal para a importação do Kimi.",
"PluginKimiRootInspectFailed": "Não foi possível inspecionar a raiz de plugins do Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "A raiz de plugins do Kimi {root} deve ser uma pasta real, não um link nem ponto de nova análise.",
"PluginKimiRootCanonicalizeFailed": "Não foi possível canonizar a raiz de plugins do Kimi {root}: {error}",
"PluginKimiRootListFailed": "Não foi possível listar a raiz de plugins do Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "Não foi possível ler uma entrada de plugin do Kimi: {error}",
"PluginKimiEntryLimit": "A raiz de plugins do Kimi contém {count} entradas; o máximo revisado por varredura é {max}.",
"PluginKimiEntryInspectFailed": "{path}: não foi possível inspecionar: {error}",
"PluginKimiEntryLinksRefused": "{path}: links e pontos de nova análise são recusados",
"PluginKimiEntryOutsideRoot": "{path}: o caminho canônico {canonical_path} não é filho direto da raiz gerenciada",
"PluginKimiEntryCanonicalizeFailed": "{path}: não foi possível canonizar: {error}",
"PluginKimiManifestUnreadable": "{path}: nenhum {manifest} legível: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} deve ser um arquivo comum real",
"PluginKimiManifestInvalid": "{path}: manifesto inválido: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: o nome da pasta deve ser exatamente igual ao nome `{name}` do manifesto",
"PluginKimiHashUnavailable": "indisponível",
"PluginKimiRollbackDestinationMissing": "O instalador não informou o caminho de destino.",
"PluginKimiMismatchRemoved": "O plugin `{name}` copiado não corresponde ao conteúdo aprovado (esperado {expected}, encontrado {actual}). A cópia inesperada foi removida; revise e tente novamente.",
"PluginKimiMismatchRollbackFailed": "Erro: o plugin `{name}` copiado não corresponde ao conteúdo aprovado (esperado {expected}, encontrado {actual}) e a remoção automática falhou: {error}. Ele permanece desativado e não confiável; inspecione {path} antes de continuar.",
"PluginKimiUserPluginDirectory": "a pasta de plugins do usuário",
"PluginKimiMarketplaceZipUnsupported": "O instalador revisado do Codewhale não aceita pacotes ZIP do Kimi; instale de uma pasta local ou importe um plugin gerenciado pelo Kimi upstream.",
"PluginKimiMarketplaceRemoteUnsupported": "Fontes remotas do Kimi devem terminar em .tar.gz ou .tgz para instalação pelo Codewhale; .zip é reconhecido, mas não aceito.",
"PluginKimiMarketplaceGzipTarball": "URL de tarball gzip",
"CmdPluginDescription": "Inspecionar e gerenciar pacotes de plugins confiáveis; ferramentas executáveis legadas permanecem separadas",
"CmdPluginBundleUsage": "Uso: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Nenhum pacote de plugin do Codewhale foi encontrado.",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "O Codewhale não envia, migra nem sincroniza o código-fonte local com o Work hospedado. Use {command} para iniciar a partir da ponta da branch disponível no GitHub ou CNB. Commits não enviados, arquivos modificados ou ignorados, segredos e o estado da sessão permanecem locais.",
"CmdRemoteEnvBrowserLabel": "Work hospedado do Codewhale",
"CmdRenameDescription": "Renomear a sessão atual",
"CmdTitleDescription": "Nomear a sessão atual e sua aba/janela do terminal",
"CmdRestoreDescription": "Reverter o workspace a um snapshot pré/pós-turno anterior. Sem argumento, lista os snapshots recentes.",
"CmdRetryDescription": "Repetir a última requisição",
"CmdReviewDescription": "Executar uma revisão de código estruturada em um arquivo, diff ou PR",
@@ -990,6 +1033,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP ainda precisa de ação; registrado no relatório (não bloqueia o primeiro uso).",
"SetupToolsMcpPreviewTitle": "On-ramps seguros de Tools / MCP",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills e Plugins — On-ramps seguros\n\n/setup só lê o inventário local. Não inicia servidores MCP, não instala skills, não executa plugins nem comandos não confiáveis.\n\nInventário atual:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (adaptadores compartilhados): {hotbar_result}\n\nCaminhos:\n- MCP: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nBootstrap seguro (execute você mesmo):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills\n- Plugins: /plugin · codewhale setup --plugins\n- Tools: codewhale setup --tools\n\nAções com efeitos colaterais sempre exigem confirmação explícita. Veja docs/MCP.md.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — conectado através do Codewhale, nunca um segundo agendador:\n- Estado: {dsh_result}\n- Detecção somente leitura; conectar/planejar/iniciar/remover: codewhale integrations dsh status · plan · connect · launch · remove\n- O Codewhale grava apenas em $CODEWHALE_HOME/integrations/dsh; nunca copia chaves de API nem edita arquivos do DSH.",
"HotbarActionModeOperateName": "Modo Operate",
"HotbarActionModeOperateDescription": "Coloque sua Fleet para trabalhar em paralelo.",
"HomeOperateModeTip": "Operate — coloque sua Fleet para trabalhar em paralelo",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet pronta",
"EmptyStateHelpConnector": "ou",
"EmptyStateHelpHint": "— ver tudo",
"SessionsSurfaceTitle": "sessões",
"SessionsPaneTitle": " sessões (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "Não foi possível verificar as identidades dos perfis existentes ({error}); corrija o arquivo indicado antes de salvar.",
"FleetProfileIdConflict": "O id de perfil `{id}` já está em uso por {path}; redija novamente com outro papel ou remova o arquivo antigo primeiro.",
"FleetProfileProviderUnconfigured": "O perfil fixa o provedor `{provider}`, que não tem credenciais configuradas ({env}); configure-o em /provider antes de salvar.",
"FleetDestStepTitle": "Onde este perfil deve ficar?",
"FleetDestStepSubtitle": "Nada é gravado até você confirmar no último passo.",
"FleetDestProjectLabel": "Este projeto",
"FleetDestPersonalLabel": "Pessoal",
"FleetDestProjectSummary": "Somente este projeto",
"FleetDestPersonalSummary": "Disponível em todos os projetos",
"FleetDestProjectDescription": "Salvo dentro deste projeto ({workspace}). Vale apenas aqui e tem precedência sobre um perfil Pessoal com o mesmo ID.",
"FleetDestPersonalDescription": "Salvo no seu diretório Codewhale. Vale em todos os projetos — exceto onde um projeto tem seu próprio perfil com o mesmo ID, que tem precedência ali.",
"FleetDestPathLine": "Arquivo: {path}",
"FleetDestUnavailable": "Indisponível: {reason}",
"FleetDestReasonNoProjectConfig": "perfis de projeto estão desativados nesta sessão (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "a pasta do workspace {path} não existe ou não é um diretório",
"FleetDestReasonHomeUnavailable": "não foi possível resolver seu diretório Codewhale ({error})",
"FleetDestWillReplace": "Vai substituir o arquivo existente {path}",
"FleetDestOverridesProject": "Este projeto já tem um perfil '{id}', que tem precedência aqui; este perfil Pessoal vale nos outros projetos.",
"FleetDestOverridesPersonal": "Tem precedência sobre o seu perfil Pessoal '{id}' dentro deste projeto.",
"FleetDestOverridesBuiltIn": "Substitui a função {origin} '{id}' na escalação.",
"FleetSavesToChip": "Salva em: {scope} · {path}",
"FleetSavesToUndecided": "Salva em: escolha no passo 3 — Este projeto ou Pessoal",
"FleetActionSaveProject": "Salvar neste projeto",
"FleetActionSavePersonal": "Salvar como perfil Pessoal",
"FleetActionReplaceProject": "Substituir neste projeto",
"FleetActionReplacePersonal": "Substituir perfil Pessoal",
"FleetActionConfirmReplace": "Pressione Enter de novo para substituir {file}",
"FleetActionChangeDestination": "Mudar destino",
"FleetActionBack": "Voltar",
"FleetReviewSavesTo": "Salva em",
"FleetModelRowBlockedNotice": "Não selecionável: {reason}. Configure em /provider ou escolha outra linha.",
"FleetDestProjectDisabledSave": "Perfis de projeto estão desativados nesta sessão (--no-project-config); nada foi salvo. Escolha Pessoal ou reinicie sem a flag.",
"WorkflowStatusWaiting": "aguardando",
"WorkflowDebrief": "balanço: {done}/{total} encerrados · {failed} falhou · {cancelled} cancelado · {elapsed}",
"WorkflowTranscriptDetails": "transcrição: JSON completo disponível nos detalhes da ferramenta ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Execução da automação {id} adicionada à fila: {status} (tarefa {task})",
"AutomationDeletePreview": "A exclusão ainda não foi confirmada. Nada foi excluído.\nAutomação: {id} ({name})\nExecuções registradas: {run_count}\nPara excluir a definição e o histórico de execuções, execute:\n{command}",
"AutomationDeleteConfirmationStale": "A confirmação de exclusão não corresponde mais à automação {id}; nada foi excluído. Revise o estado atual com {command}.",
"AutomationDeleted": "A automação {id} ({name}) foi excluída. Execuções registradas excluídas: {run_count}."
"AutomationDeleted": "A automação {id} ({name}) foi excluída. Execuções registradas excluídas: {run_count}.",
"WhaleStateResting": "Descansando",
"WhaleStateThinking": "Pensando",
"WhaleStateWorking": "Trabalhando",
"WhaleStateWaiting": "Aguardando você",
"WhaleStateBlocked": "Bloqueada",
"WhaleStateOffline": "Offline",
"WhaleAnimalScout": "baleia-bicuda",
"WhaleAnimalPatch": "boto-do-porto",
"WhaleAnimalHarbor": "baleia-jubarte",
"WhaleAnimalEcho": "baleia-piloto",
"WhaleAnimalKeel": "cachalote",
"WhaleAnimalLantern": "orca",
"WhaleAnimalPlain": "baleia",
"WhaleJobScout": "pesquisa",
"WhaleJobPatch": "programação",
"WhaleJobHarbor": "coordenação",
"WhaleJobEcho": "comunicação",
"WhaleJobKeel": "operações",
"WhaleJobLantern": "revisão",
"WhaleJobPlain": "trabalho geral",
"SessionMetricsTurn": "turno",
"SessionMetricsTurns": "turnos",
"SessionMetricsStep": "passo",
"SessionMetricsSteps": "passos",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Ferramentas",
"SessionMetricsTtft": "TTFT méd.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache",
"SessionMetricsInput": "Entrada",
"SessionMetricsStatusLine": "Métricas da sessão: {metrics}",
"AutoReviewReceiptGuardianAllowed": "O Auto-Review permitiu '{tool}' (risco {risk}, guardião do modelo): {reason}",
"AutoReviewReceiptGuardianDenied": "O Auto-Review negou '{tool}' (risco {risk}, guardião do modelo): {reason}",
"AutoReviewReceiptGuardianUnavailable": "O Auto-Review não conseguiu revisar '{tool}' ({reason}); negado, fail closed",
"AutoReviewReceiptDeterministicBlocked": "O Auto-Review bloqueou '{tool}' (política determinística): {reason}",
"AutoReviewReceiptHeld": "O Auto-Review reteve '{tool}' sem pausar; negado (precisa de uma pessoa — mude para Ask)",
"FooterHintEscInterrupt": "Esc para interromper",
"PermissionsPostureHeader": "Postura de permissão atual: {posture}",
"PermissionsPostureAsk": "Ask: chamadas de tool que alteram autoridade, custo, escopo ou resultado abrem um prompt; chamadas somente leitura comprovadamente seguras rodam sem ele. As regras ask acima sempre forçam um prompt.",
"PermissionsPostureAuto": "Auto-Review: nunca abre um prompt. Uma política determinística permite chamadas comprovadamente seguras e bloqueia de forma rígida trabalho de publicação ou destrutivo em segundo plano; chamadas que não consegue provar seguras vão a um guardião de modelo de uma única rodada, que permite ou nega com um motivo declarado (risco alto ou crítico nunca roda automaticamente; uma revisão falha nega, fail closed). Retenções que exigem uma pessoa são negadas, não ocultadas. Cada decisão dessas é escrita na transcrição como nota e no log de auditoria.",
"PermissionsPostureBypass": "Full Access: chamadas de tool comuns rodam sem prompts. Retenções não contornáveis de segurança, lei do repositório e política gerenciada fazem fail closed como bloqueios rígidos em vez de perguntar.",
"PermissionsPostureNever": "never: só rodam tools consideradas seguras/somente leitura; todo o resto é bloqueado sem prompt.",
"PermissionsReceiptsNote": "Decisões tomadas sem prompt (veredictos do guardião do Auto-Review, bloqueios e retenções) aparecem como notas na transcrição e no log de auditoria em {audit_path}. Full Access é escolhido deliberadamente com Shift+Tab ou /config, nunca por uma regra.",
"AgentFocusOpened": "Foco em {agent}. Suas mensagens agora vão para este worker; Esc volta à conversa principal.",
"AgentFocusClosed": "De volta à conversa principal.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Mensagem para {agent} · Esc volta ao principal",
"AgentFocusNoTranscript": "Ainda não há transcrição de {agent}. As mensagens aparecem aqui conforme o worker as troca.",
"AgentFocusOmitted": "Mensagens anteriores ({count}) foram omitidas da transcrição em memória.",
"AgentFocusFollowUpDelivered": "Na fila para {agent}: ele lê a mensagem na próxima rodada.",
"AgentFocusFollowUpQueued": "Na fila para {agent}",
"AgentFocusFollowUpContinued": "{agent} já havia terminado; continuou em um novo fork ({target}). Esta visão agora segue o fork.",
"AgentFocusFollowUpFailed": "Não foi possível entregar a {agent}: {reason}",
"FooterHintForAgents": "agentes",
"FooterHintToManage": "gerenciar",
"AgentRailQueuedCount": "{count} na fila",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "escreve",
"AgentFocusPostureReadOnly": "somente leitura",
"AgentFocusPostureNetwork": "rede",
"AgentFocusPostureNoNetwork": "sem rede",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell somente leitura",
"AgentFocusPostureShellNone": "sem shell",
"GoalReceiptSet": "Meta definida: \"{objective}\" · /goal mostra o progresso · /goal pause ou /goal clear interrompe",
"GoalStatusIdleHint": "parada no momento — envie uma mensagem ou /goal resume para continuar"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Напишите задачу или используйте /.",
"ComposerOperatePlaceholder": "Опишите цель — Codewhale продолжит работу до её выполнения",
"ComposerDispatchFailedRestored": "Сообщение не отправлено ({error}); возвращено в поле ввода.",
"DispatchFailedQueued": "Отправка не удалась ({error}); в очереди оставлено отложенных сообщений: {count}.",
"DispatchFailedInitial": "Не удалось отправить начальный запрос: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Текущий провайдер",
"ConfigLabelBaseUrlDeepseek": "URL API провайдера (маршрут DeepSeek)",
"ConfigLabelProviderUrl": "URL API провайдера",
"ConfigHintProviderUrl": "Текущая конечная точка провайдера; Xiaomi: пакет токенов | оплата по мере использования | пользовательский URL",
"ConfigLabelModel": "Модель текущего провайдера",
"ConfigLabelFastModel": "Быстрая модель (производная)",
"ConfigLabelDefaultModel": "Резервная модель (только маршруты DeepSeek)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "на ход · локальная эвристика · без запроса к маршрутизатору",
"ModelPickerAutoLastRoute": "последний {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} через {source} · Ctrl+O: детали маршрута",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code не может безопасно отправить этот ход: это подключение пока не поддерживает системные инструкции. Ничего не отправлено; выберите другого провайдера.",
"HelpTitle": "Справка",
"HelpFilterPlaceholder": "Введите для фильтра",
"HelpFilterPrefix": "Фильтр: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Загрузить сессию из файла",
"CmdLogoutDescription": "Удалить API-ключ и вернуться к настройке",
"CmdMcpDescription": "Открыть список серверов MCP или управлять ими",
"McpRecommendedUnknownId": "Неизвестный идентификатор рекомендованного MCP. Выполните {recommendations_command}, чтобы просмотреть отобранный список.",
"McpRecommendationsSafety": "Просмотр списка ничего не добавляет и не включает. Явное добавление только записывает конфигурацию; проверьте её до подключения сервера командой {restart_command}.",
"McpRecommendationGithub": "• github — официальный удалённый MCP-адрес GitHub\n адрес: {endpoint}\n аутентификация выполняется отдельно: используйте {login_command}, только если сервер заявляет OAuth;\n иначе настройте PAT с минимальными правами вне истории команд. Выданные\n области доступа могут изменять или удалять данные репозитория; по возможности начните с чтения.\n добавить явно: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — официальный Chrome DevTools MCP через npm-пакет закреплённой версии\n пакет: {package} ({launcher})\n он может исследовать/управлять Chrome и читать страницы с авторизацией. Закройте\n конфиденциальные вкладки и проверьте пакет до добавления; {restart_command} может скачать и запустить его.\n добавить явно: {add_command}",
"PluginKimiUsage": "Использование:\n {list_command}\n {approve_command}\nСписок доступен только для чтения. После подтверждения один канонический плагин под управлением Kimi копируется проверенным установщиком; он остаётся отключённым и недоверенным.",
"PluginKimiManagedRootHeading": "Плагины под управлением Kimi в {root}:",
"PluginKimiNoneFound": "Допустимые управляемые плагины не найдены.",
"PluginKimiLicenseUnspecified": "не указана",
"PluginKimiApplicable": "подходит для этой ОС",
"PluginKimiNotApplicable": "не подходит для этой ОС",
"PluginKimiCandidateSummary": "{name} {version} — лицензия={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " путь: {path}\n хэш содержимого: {content_hash}\n хэш возможностей: {capability_hash}\n подтвердить: {approve_command}",
"PluginKimiRejectedHeading": "Отклонённые записи (импорт невозможен):",
"PluginKimiInspectionFooter": "Проверка ничего не копировала, не объявляла доверенным, не включала и не запускала. Внешние приложения, службы, бинарные файлы, расширения браузера, учётные данные и разрешения ОС Kimi не проверялись.",
"PluginKimiCandidateMissing": "Нет допустимого канонического плагина под управлением Kimi с именем `{name}`. Снова выполните {list_command}.",
"PluginKimiCandidateChanged": "Плагин под управлением Kimi `{name}` изменился после проверки. Ожидался хэш {expected}, теперь {actual}. Ничего не скопировано; снова выполните {list_command}.",
"PluginKimiHomeMissing": "Не удалось найти домашний каталог пользователя для импорта Kimi.",
"PluginKimiRootInspectFailed": "Не удалось проверить корень плагинов Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "Корень плагинов Kimi {root} должен быть настоящим каталогом, а не ссылкой или точкой повторного анализа.",
"PluginKimiRootCanonicalizeFailed": "Не удалось канонизировать корень плагинов Kimi {root}: {error}",
"PluginKimiRootListFailed": "Не удалось прочитать список корня плагинов Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "Не удалось прочитать запись плагина Kimi: {error}",
"PluginKimiEntryLimit": "В корне плагинов Kimi {count} записей; за одно сканирование проверяется не более {max}.",
"PluginKimiEntryInspectFailed": "{path}: не удалось проверить: {error}",
"PluginKimiEntryLinksRefused": "{path}: ссылки и точки повторного анализа запрещены",
"PluginKimiEntryOutsideRoot": "{path}: канонический путь {canonical_path} не является непосредственным потомком управляемого корня",
"PluginKimiEntryCanonicalizeFailed": "{path}: не удалось канонизировать: {error}",
"PluginKimiManifestUnreadable": "{path}: нет читаемого {manifest}: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} должен быть настоящим обычным файлом",
"PluginKimiManifestInvalid": "{path}: недопустимый манифест: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: имя каталога должно точно совпадать с именем `{name}` в манифесте",
"PluginKimiHashUnavailable": "недоступно",
"PluginKimiRollbackDestinationMissing": "Установщик не сообщил путь назначения.",
"PluginKimiMismatchRemoved": "Скопированный плагин `{name}` не совпал с одобренным содержимым (ожидалось {expected}, найдено {actual}). Неожиданная копия удалена; проверьте и повторите попытку.",
"PluginKimiMismatchRollbackFailed": "Ошибка: скопированный плагин `{name}` не совпал с одобренным содержимым (ожидалось {expected}, найдено {actual}), а автоматическое удаление завершилось с ошибкой: {error}. Он остаётся отключённым и недоверенным; проверьте {path} перед продолжением.",
"PluginKimiUserPluginDirectory": "каталог пользовательских плагинов",
"PluginKimiMarketplaceZipUnsupported": "Проверенный установщик Codewhale не поддерживает ZIP-пакеты Kimi; установите из локального каталога или импортируйте вышестоящий плагин под управлением Kimi.",
"PluginKimiMarketplaceRemoteUnsupported": "Удалённый источник Kimi для установки Codewhale должен оканчиваться на .tar.gz или .tgz; .zip распознаётся, но не поддерживается.",
"PluginKimiMarketplaceGzipTarball": "URL gzip-архива tar",
"CmdPluginDescription": "Просмотр и управление доверенными пакетами плагинов; устаревшие исполняемые инструменты — отдельно",
"CmdPluginBundleUsage": "Использование: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Пакеты плагинов Codewhale не найдены.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale не загружает, не переносит и не синхронизирует локальный исходный код с размещённым Work. Используйте {command}, чтобы начать с вершины ветки, доступной на GitHub или CNB. Неотправленные коммиты, изменённые или игнорируемые файлы, секреты и состояние сеанса остаются локальными.",
"CmdRemoteEnvBrowserLabel": "Размещённый Work Codewhale",
"CmdRenameDescription": "Переименовать текущую сессию",
"CmdTitleDescription": "Задать имя текущей сессии и вкладки/окна терминала",
"CmdRestoreDescription": "Откатить рабочую область к прежнему снимку до/после хода. Без аргумента — список недавних снимков.",
"CmdRetryDescription": "Повторить последний запрос",
"CmdReviewDescription": "Запустить структурированное ревью кода для файла, diff или PR",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Инструменты/MCP всё ещё требуют действия; записано для отчёта настройки (не блокирует первый запуск).",
"SetupToolsMcpPreviewTitle": "Инструменты / MCP: безопасное начало",
"SetupToolsMcpOnRampText": "Инструменты, MCP, навыки и плагины — безопасное начало\n\n/setup только читает локальный инвентарь. Он никогда не запускает серверы MCP, не устанавливает навыки, не запускает плагины и не выполняет недоверенные команды.\n\nТекущий инвентарь:\n- MCP: {mcp_result}\n- Навыки: {skills_result}\n- Каталог инструментов: {tools_result}\n- Плагины: {plugins_result}\n- Hotbar (общие адаптеры): {hotbar_result}\n\nПути (домашний каталог скрыт):\n- Конфигурация MCP: {mcp_path}\n- Навыки: {skills_path}\n- Плагины: {plugins_path}\n\nБезопасная инициализация (выполните сами в обычном терминале или командой TUI):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Навыки: /skills · codewhale setup --skills · /skills install <spec>\n- Плагины: /plugin · codewhale setup --plugins\n- Каталог инструментов: codewhale setup --tools\n\nДействия с побочными эффектами всегда требуют явного подтверждения. Команды плагинов отличаются от slash-команд; источник плагинов Hotbar отложен до появления барьеров одобрения.\n\nСм. docs/MCP.md и docs/skills/README.md о том, что ещё требует ручной внешней настройки.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — подключён через Codewhale, никогда не второй планировщик:\n- Состояние: {dsh_result}\n- Обнаружение только для чтения; подключить/план/запуск/удалить: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale пишет только в $CODEWHALE_HOME/integrations/dsh; никогда не копирует API-ключи и не изменяет файлы DSH.",
"HotbarActionModeOperateName": "Режим Operate",
"HotbarActionModeOperateDescription": "Параллельная работа Fleet.",
"HomeOperateModeTip": "Operate — параллельная работа Fleet",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet готов",
"EmptyStateHelpConnector": "или",
"EmptyStateHelpHint": "— показать всё",
"SessionsSurfaceTitle": "сессии",
"SessionsPaneTitle": " сессии (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "Не удалось проверить существующие идентификаторы профилей ({error}); исправьте указанный файл перед сохранением.",
"FleetProfileIdConflict": "Идентификатор профиля `{id}` уже используется в {path}; пересоздайте черновик с другой ролью или сначала удалите старый файл.",
"FleetProfileProviderUnconfigured": "Профиль закрепляет провайдера `{provider}`, для которого нет настроенных учётных данных ({env}); настройте его в /provider перед сохранением.",
"FleetDestStepTitle": "Где сохранить этот профиль?",
"FleetDestStepSubtitle": "Ничего не записывается, пока вы не подтвердите на последнем шаге.",
"FleetDestProjectLabel": "Этот проект",
"FleetDestPersonalLabel": "Личный",
"FleetDestProjectSummary": "Только этот проект",
"FleetDestPersonalSummary": "Доступен во всех проектах",
"FleetDestProjectDescription": "Сохраняется внутри этого проекта ({workspace}). Действует только здесь и имеет приоритет над личным профилем с тем же ID.",
"FleetDestPersonalDescription": "Сохраняется в вашем домашнем каталоге Codewhale. Действует во всех проектах — кроме тех, где у проекта есть собственный профиль с тем же ID; там приоритет у него.",
"FleetDestPathLine": "Файл: {path}",
"FleetDestUnavailable": "Недоступно: {reason}",
"FleetDestReasonNoProjectConfig": "профили проекта отключены в этом сеансе (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "папка рабочего пространства {path} не существует или не является каталогом",
"FleetDestReasonHomeUnavailable": "не удалось определить ваш домашний каталог Codewhale ({error})",
"FleetDestWillReplace": "Заменит существующий файл {path}",
"FleetDestOverridesProject": "В этом проекте уже есть профиль '{id}', и здесь приоритет у него; этот личный профиль действует в других проектах.",
"FleetDestOverridesPersonal": "Имеет приоритет над вашим личным профилем '{id}' внутри этого проекта.",
"FleetDestOverridesBuiltIn": "Заменяет роль {origin} '{id}' в составе.",
"FleetSavesToChip": "Сохраняется в: {scope} · {path}",
"FleetSavesToUndecided": "Сохраняется в: выберите на шаге 3 — Этот проект или Личный",
"FleetActionSaveProject": "Сохранить в этот проект",
"FleetActionSavePersonal": "Сохранить как личный профиль",
"FleetActionReplaceProject": "Заменить в этом проекте",
"FleetActionReplacePersonal": "Заменить личный профиль",
"FleetActionConfirmReplace": "Нажмите Enter ещё раз, чтобы заменить {file}",
"FleetActionChangeDestination": "Изменить место сохранения",
"FleetActionBack": "Назад",
"FleetReviewSavesTo": "Сохраняется в",
"FleetModelRowBlockedNotice": "Нельзя выбрать: {reason}. Настройте в /provider или выберите другую строку.",
"FleetDestProjectDisabledSave": "Профили проекта отключены в этом сеансе (--no-project-config); ничего не сохранено. Выберите «Личный» или перезапустите без этого флага.",
"WorkflowStatusWaiting": "ожидание",
"WorkflowDebrief": "итоги: {done}/{total} завершено · {failed} ошибок · {cancelled} отменено · {elapsed}",
"WorkflowTranscriptDetails": "транскрипт: полный JSON запуска доступен в деталях инструмента ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Запуск автоматизации {id} поставлен в очередь: {status} (задача {task})",
"AutomationDeletePreview": "Удаление ещё не подтверждено. Ничего не удалено.\nАвтоматизация: {id} ({name})\nЗаписанных запусков: {run_count}\nЧтобы удалить определение и историю запусков, выполните:\n{command}",
"AutomationDeleteConfirmationStale": "Подтверждение удаления больше не соответствует автоматизации {id}; ничего не удалено. Проверьте текущее состояние с помощью {command}.",
"AutomationDeleted": "Автоматизация {id} ({name}) удалена. Удалено записанных запусков: {run_count}."
"AutomationDeleted": "Автоматизация {id} ({name}) удалена. Удалено записанных запусков: {run_count}.",
"WhaleStateResting": "Отдыхает",
"WhaleStateThinking": "Думает",
"WhaleStateWorking": "Работает",
"WhaleStateWaiting": "Ждёт вас",
"WhaleStateBlocked": "Заблокирован",
"WhaleStateOffline": "Не в сети",
"WhaleAnimalScout": "клюворыл",
"WhaleAnimalPatch": "морская свинья",
"WhaleAnimalHarbor": "горбатый кит",
"WhaleAnimalEcho": "гринда",
"WhaleAnimalKeel": "кашалот",
"WhaleAnimalLantern": "косатка",
"WhaleAnimalPlain": "кит",
"WhaleJobScout": "исследование",
"WhaleJobPatch": "программирование",
"WhaleJobHarbor": "координация",
"WhaleJobEcho": "коммуникации",
"WhaleJobKeel": "эксплуатация",
"WhaleJobLantern": "ревью",
"WhaleJobPlain": "общая работа",
"SessionMetricsTurn": "ход",
"SessionMetricsTurns": "ходов",
"SessionMetricsStep": "шаг",
"SessionMetricsSteps": "шагов",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Инстр.",
"SessionMetricsTtft": "TTFT ср.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Кэш",
"SessionMetricsInput": "Ввод",
"SessionMetricsStatusLine": "Метрики сессии: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review разрешил '{tool}' (риск {risk}, модельный страж): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review отклонил '{tool}' (риск {risk}, модельный страж): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review не смог проверить '{tool}' ({reason}); отклонено, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review заблокировал '{tool}' (детерминированная политика): {reason}",
"AutoReviewReceiptHeld": "Auto-Review удержал '{tool}' без паузы; отклонено (нужен человек — переключитесь на Ask)",
"FooterHintEscInterrupt": "Esc — прервать",
"PermissionsPostureHeader": "Текущий режим разрешений: {posture}",
"PermissionsPostureAsk": "Ask: вызовы tool, меняющие полномочия, стоимость, охват или результат, открывают запрос; доказанно безопасные вызовы только для чтения выполняются без него. Правила ask выше всегда принудительно показывают запрос.",
"PermissionsPostureAuto": "Auto-Review: никогда не открывает запрос. Детерминированная политика разрешает доказанно безопасные вызовы и жёстко блокирует публикационную или разрушительную фоновую работу; вызовы, безопасность которых доказать нельзя, отправляются одноразовому модельному стражу, который разрешает или отклоняет с указанием причины (высокий или критический риск никогда не выполняется автоматически; неудачная проверка отклоняет по принципу fail closed). Удержания, требующие человека, отклоняются, а не скрываются. Каждое такое решение записывается заметкой в транскрипт и в журнал аудита.",
"PermissionsPostureBypass": "Full Access: обычные вызовы tool выполняются без запросов. Необходимые удержания безопасности, правил репозитория и управляемой политики завершаются fail closed как жёсткие блокировки вместо запроса.",
"PermissionsPostureNever": "never: выполняются только tool, считающиеся безопасными/только для чтения; всё остальное блокируется без запроса.",
"PermissionsReceiptsNote": "Решения, принятые без запроса (вердикты стража Auto-Review, блокировки и удержания), отображаются заметками в транскрипте и в журнале аудита {audit_path}. Full Access выбирается осознанно через Shift+Tab или /config, никогда правилом.",
"AgentFocusOpened": "Фокус на {agent}. Теперь ваши сообщения идут этому воркеру; Esc возвращает в основной разговор.",
"AgentFocusClosed": "Снова в основном разговоре.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Сообщение для {agent} · Esc — в основной разговор",
"AgentFocusNoTranscript": "Для {agent} пока нет записи разговора. Сообщения появятся здесь, как только воркер начнёт ими обмениваться.",
"AgentFocusOmitted": "Более ранние сообщения ({count}) не вошли в запись разговора в памяти.",
"AgentFocusFollowUpDelivered": "В очереди для {agent}: он прочитает сообщение на следующем раунде.",
"AgentFocusFollowUpQueued": "В очереди для {agent}",
"AgentFocusFollowUpContinued": "{agent} уже завершился; продолжен в новом форке ({target}). Этот вид теперь следует за форком.",
"AgentFocusFollowUpFailed": "Не удалось доставить {agent}: {reason}",
"FooterHintForAgents": "агенты",
"FooterHintToManage": "управление",
"AgentRailQueuedCount": "{count} в очереди",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "пишет",
"AgentFocusPostureReadOnly": "только чтение",
"AgentFocusPostureNetwork": "сеть",
"AgentFocusPostureNoNetwork": "без сети",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell только чтение",
"AgentFocusPostureShellNone": "без shell",
"GoalReceiptSet": "Цель задана: «{objective}» · /goal показывает прогресс · /goal pause или /goal clear останавливает",
"GoalStatusIdleHint": "сейчас не выполняется — отправьте сообщение или /goal resume, чтобы продолжить"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Напишіть завдання або використайте /.",
"ComposerOperatePlaceholder": "Опишіть мету — Codewhale працюватиме до її виконання",
"ComposerDispatchFailedRestored": "Повідомлення не надіслано ({error}); повернено в композер.",
"DispatchFailedQueued": "Надсилання не вдалося ({error}); залишено {count} повідомлень у черзі.",
"DispatchFailedInitial": "Початковий промпт не вдалося надіслати: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Активний провайдер",
"ConfigLabelBaseUrlDeepseek": "URL API провайдера (маршрут DeepSeek)",
"ConfigLabelProviderUrl": "URL API провайдера",
"ConfigHintProviderUrl": "Поточна кінцева точка провайдера; Xiaomi: пакет токенів | оплата за використання | власна URL-адреса",
"ConfigLabelModel": "Модель активного провайдера",
"ConfigLabelFastModel": "Швидка модель (похідна)",
"ConfigLabelDefaultModel": "Резервна застаріла модель (лише маршрути DeepSeek)",
@@ -178,6 +180,7 @@
"ModelPickerAutoLocalHint": "за крок · локальна евристика · без запиту до маршрутизатора",
"ModelPickerAutoLastRoute": "останній {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} через {source} · Ctrl+O: деталі маршруту",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code не може безпечно надіслати цей хід: це з’єднання ще не підтримує системні інструкції. Нічого не надіслано; виберіть іншого провайдера.",
"HelpTitle": "Довідка",
"HelpFilterPlaceholder": "Вводьте для фільтра",
"HelpFilterPrefix": "Фільтр: ",
@@ -249,6 +252,45 @@
"CmdLoadDescription": "Завантажити сеанс з файлу",
"CmdLogoutDescription": "Очистити ключ API та повернутися до налаштування",
"CmdMcpDescription": "Відкрити або керувати серверами MCP",
"McpRecommendedUnknownId": "Невідомий ідентифікатор рекомендованого MCP. Виконайте {recommendations_command}, щоб переглянути відібраний список.",
"McpRecommendationsSafety": "Перегляд списку нічого не додає й не вмикає. Явне додавання лише записує конфігурацію; перевірте її до підключення сервера командою {restart_command}.",
"McpRecommendationGithub": "• github — офіційна віддалена MCP-адреса GitHub\n адреса: {endpoint}\n автентифікація виконується окремо: використовуйте {login_command}, лише якщо сервер заявляє OAuth;\n інакше налаштуйте PAT із мінімальними правами поза історією команд. Надані\n області доступу можуть змінювати або видаляти дані репозиторію; за можливості почніть із читання.\n додати явно: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — офіційний Chrome DevTools MCP через npm-пакет закріпленої версії\n пакет: {package} ({launcher})\n він може досліджувати/керувати Chrome і читати сторінки з авторизацією. Закрийте\n конфіденційні вкладки й перевірте пакет до додавання; {restart_command} може завантажити та запустити його.\n додати явно: {add_command}",
"PluginKimiUsage": "Використання:\n {list_command}\n {approve_command}\nСписок доступний лише для читання. Після підтвердження один канонічний плагін під керуванням Kimi копіюється перевіреним інсталятором; він залишається вимкненим і недовіреним.",
"PluginKimiManagedRootHeading": "Плагіни під керуванням Kimi у {root}:",
"PluginKimiNoneFound": "Припустимих керованих плагінів не знайдено.",
"PluginKimiLicenseUnspecified": "не вказана",
"PluginKimiApplicable": "підходить для цієї ОС",
"PluginKimiNotApplicable": "не підходить для цієї ОС",
"PluginKimiCandidateSummary": "{name} {version} — ліцензія={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " шлях: {path}\n хеш вмісту: {content_hash}\n хеш можливостей: {capability_hash}\n підтвердити: {approve_command}",
"PluginKimiRejectedHeading": "Відхилені записи (імпорт неможливий):",
"PluginKimiInspectionFooter": "Перевірка нічого не копіювала, не оголошувала довіреним, не вмикала й не запускала. Зовнішні застосунки, служби, бінарні файли, розширення браузера, облікові дані та дозволи ОС Kimi не перевірялися.",
"PluginKimiCandidateMissing": "Немає припустимого канонічного плагіна під керуванням Kimi з назвою `{name}`. Знову виконайте {list_command}.",
"PluginKimiCandidateChanged": "Плагін під керуванням Kimi `{name}` змінився після перевірки. Очікувався хеш {expected}, тепер {actual}. Нічого не скопійовано; знову виконайте {list_command}.",
"PluginKimiHomeMissing": "Не вдалося знайти домашній каталог користувача для імпорту Kimi.",
"PluginKimiRootInspectFailed": "Не вдалося перевірити корінь плагінів Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "Корінь плагінів Kimi {root} має бути справжнім каталогом, а не посиланням чи точкою повторного аналізу.",
"PluginKimiRootCanonicalizeFailed": "Не вдалося канонізувати корінь плагінів Kimi {root}: {error}",
"PluginKimiRootListFailed": "Не вдалося прочитати список кореня плагінів Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "Не вдалося прочитати запис плагіна Kimi: {error}",
"PluginKimiEntryLimit": "У корені плагінів Kimi {count} записів; за одне сканування перевіряється не більше {max}.",
"PluginKimiEntryInspectFailed": "{path}: не вдалося перевірити: {error}",
"PluginKimiEntryLinksRefused": "{path}: посилання й точки повторного аналізу заборонено",
"PluginKimiEntryOutsideRoot": "{path}: канонічний шлях {canonical_path} не є безпосереднім нащадком керованого кореня",
"PluginKimiEntryCanonicalizeFailed": "{path}: не вдалося канонізувати: {error}",
"PluginKimiManifestUnreadable": "{path}: немає читабельного {manifest}: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} має бути справжнім звичайним файлом",
"PluginKimiManifestInvalid": "{path}: неприпустимий маніфест: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: назва каталогу має точно збігатися з назвою `{name}` у маніфесті",
"PluginKimiHashUnavailable": "недоступно",
"PluginKimiRollbackDestinationMissing": "Інсталятор не повідомив шлях призначення.",
"PluginKimiMismatchRemoved": "Скопійований плагін `{name}` не збігся зі схваленим вмістом (очікувалося {expected}, знайдено {actual}). Неочікувану копію видалено; перевірте й повторіть спробу.",
"PluginKimiMismatchRollbackFailed": "Помилка: скопійований плагін `{name}` не збігся зі схваленим вмістом (очікувалося {expected}, знайдено {actual}), а автоматичне видалення завершилося помилкою: {error}. Він залишається вимкненим і недовіреним; перевірте {path} перед продовженням.",
"PluginKimiUserPluginDirectory": "каталог користувацьких плагінів",
"PluginKimiMarketplaceZipUnsupported": "Перевірений інсталятор Codewhale не підтримує ZIP-пакети Kimi; установіть із локального каталогу або імпортуйте вищий плагін під керуванням Kimi.",
"PluginKimiMarketplaceRemoteUnsupported": "Віддалене джерело Kimi для встановлення Codewhale має закінчуватися на .tar.gz або .tgz; .zip розпізнається, але не підтримується.",
"PluginKimiMarketplaceGzipTarball": "URL gzip-архіву tar",
"CmdPluginDescription": "Переглянути й керувати довіреними наборами плагінів; застарілі виконувані інструменти лишаються окремо",
"CmdPluginBundleUsage": "Використання: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Наборів плагінів Codewhale не знайдено.",
@@ -303,6 +345,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale не завантажує, не переносить і не синхронізує локальний вихідний код із розміщеним Work. Скористайтеся {command}, щоб почати з вершини гілки, доступної на GitHub або CNB. Ненадіслані коміти, змінені чи ігноровані файли, секрети й стан сеансу залишаються локальними.",
"CmdRemoteEnvBrowserLabel": "Розміщений Work Codewhale",
"CmdRenameDescription": "Перейменувати поточний сеанс",
"CmdTitleDescription": "Назвати поточний сеанс і вкладку/вікно термінала",
"CmdRestoreDescription": "Відкотити робочу область до попереднього знімка до/після кроку. Без аргумента — показує останні знімки.",
"CmdRetryDescription": "Повторити останній запит",
"CmdReviewDescription": "Запустити структурований огляд коду для файлу, diff або PR",
@@ -969,6 +1012,8 @@
"SetupToolsMcpNeedsActionSaved": "Інструменти/MCP досі потребують дії; записано для звіту налаштування (не блокує перший запуск).",
"SetupToolsMcpPreviewTitle": "Інструменти / MCP — безпечний старт",
"SetupToolsMcpOnRampText": "Інструменти, MCP, навички й плагіни — безпечний старт\n\n/setup лише читає локальний інвентар. Він ніколи не запускає сервери MCP, не встановлює навички, не запускає плагіни й не виконує недовірені команди.\n\nПоточний інвентар:\n- MCP: {mcp_result}\n- Навички: {skills_result}\n- Каталог інструментів: {tools_result}\n- Плагіни: {plugins_result}\n- Hotbar (спільні адаптери): {hotbar_result}\n\nШляхи (домівка прихована):\n- Конфігурація MCP: {mcp_path}\n- Навички: {skills_path}\n- Плагіни: {plugins_path}\n\nБезпечне початкове налаштування (виконайте самі у звичайному терміналі або командою TUI):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Навички: /skills · codewhale setup --skills · /skills install <spec>\n- Плагіни: /plugin · codewhale setup --plugins\n- Каталог інструментів: codewhale setup --tools\n\nДії з побічними ефектами завжди потребують явного підтвердження. Команди плагінів залишаються відмінними від слеш-команд; джерело плагінів Hotbar відкладено, доки не з'являться ворота схвалення.\n\nДивіться docs/MCP.md і docs/skills/README.md про те, що ще потребує ручного зовнішнього налаштування.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — під'єднано через Codewhale, ніколи не другий планувальник:\n- Стан: {dsh_result}\n- Виявлення лише для читання; під'єднати/план/запуск/вилучити: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale пише лише в $CODEWHALE_HOME/integrations/dsh; ніколи не копіює API-ключі й не змінює файли DSH.",
"HotbarActionModeOperateName": "Режим Operate",
"HotbarActionModeOperateDescription": "Залучіть Fleet до паралельної роботи.",
"HomeOperateModeTip": "Operate — паралельна робота Fleet",
@@ -1021,6 +1066,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet готовий",
"EmptyStateHelpConnector": "або",
"EmptyStateHelpHint": "— показати все",
"SessionsSurfaceTitle": "сесії",
"SessionsPaneTitle": " сесії (1-9) ",
@@ -1170,6 +1216,35 @@
"FleetProfileIdentityVerifyFailed": "Не вдалося перевірити наявні ідентичності профілів ({error}); виправте названий файл перед збереженням.",
"FleetProfileIdConflict": "Ідентифікатор профілю `{id}` уже використовується в {path}; створіть чернетку з іншою роллю або спочатку видаліть старий файл.",
"FleetProfileProviderUnconfigured": "Профіль закріплює провайдера `{provider}`, для якого не налаштовано облікові дані ({env}); налаштуйте його в /provider перед збереженням.",
"FleetDestStepTitle": "Де зберегти цей профіль?",
"FleetDestStepSubtitle": "Нічого не записується, доки ви не підтвердите на останньому кроці.",
"FleetDestProjectLabel": "Цей проєкт",
"FleetDestPersonalLabel": "Особистий",
"FleetDestProjectSummary": "Лише цей проєкт",
"FleetDestPersonalSummary": "Доступний у всіх проєктах",
"FleetDestProjectDescription": "Зберігається всередині цього проєкту ({workspace}). Діє лише тут і має пріоритет над особистим профілем із тим самим ID.",
"FleetDestPersonalDescription": "Зберігається у вашому домашньому каталозі Codewhale. Діє в усіх проєктах — окрім тих, де проєкт має власний профіль із тим самим ID; там пріоритет у нього.",
"FleetDestPathLine": "Файл: {path}",
"FleetDestUnavailable": "Недоступно: {reason}",
"FleetDestReasonNoProjectConfig": "профілі проєкту вимкнено в цьому сеансі (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "тека робочого простору {path} не існує або не є каталогом",
"FleetDestReasonHomeUnavailable": "не вдалося визначити ваш домашній каталог Codewhale ({error})",
"FleetDestWillReplace": "Замінить наявний файл {path}",
"FleetDestOverridesProject": "У цьому проєкті вже є профіль '{id}', і тут пріоритет у нього; цей особистий профіль діє в інших проєктах.",
"FleetDestOverridesPersonal": "Має пріоритет над вашим особистим профілем '{id}' у цьому проєкті.",
"FleetDestOverridesBuiltIn": "Замінює роль {origin} '{id}' у складі.",
"FleetSavesToChip": "Зберігається в: {scope} · {path}",
"FleetSavesToUndecided": "Зберігається в: виберіть на кроці 3 — Цей проєкт або Особистий",
"FleetActionSaveProject": "Зберегти в цей проєкт",
"FleetActionSavePersonal": "Зберегти як особистий профіль",
"FleetActionReplaceProject": "Замінити в цьому проєкті",
"FleetActionReplacePersonal": "Замінити особистий профіль",
"FleetActionConfirmReplace": "Натисніть Enter ще раз, щоб замінити {file}",
"FleetActionChangeDestination": "Змінити місце збереження",
"FleetActionBack": "Назад",
"FleetReviewSavesTo": "Зберігається в",
"FleetModelRowBlockedNotice": "Не можна вибрати: {reason}. Налаштуйте в /provider або виберіть інший рядок.",
"FleetDestProjectDisabledSave": "Профілі проєкту вимкнено в цьому сеансі (--no-project-config); нічого не збережено. Виберіть «Особистий» або перезапустіть без цього прапорця.",
"WorkflowStatusWaiting": "очікування",
"WorkflowDebrief": "підсумок: {done}/{total} завершено · {failed} невдало · {cancelled} скасовано · {elapsed}",
"WorkflowTranscriptDetails": "стенограма: повний JSON запуску доступний у деталях інструмента ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Запуск автоматизації {id} поставлено в чергу: {status} (завдання {task})",
"AutomationDeletePreview": "Видалення ще не підтверджено. Нічого не видалено.\nАвтоматизація: {id} ({name})\nЗаписаних запусків: {run_count}\nЩоб видалити визначення та історію запусків, виконайте:\n{command}",
"AutomationDeleteConfirmationStale": "Підтвердження видалення більше не відповідає автоматизації {id}; нічого не видалено. Перевірте поточний стан за допомогою {command}.",
"AutomationDeleted": "Автоматизацію {id} ({name}) видалено. Видалено записаних запусків: {run_count}."
"AutomationDeleted": "Автоматизацію {id} ({name}) видалено. Видалено записаних запусків: {run_count}.",
"WhaleStateResting": "Відпочиває",
"WhaleStateThinking": "Міркує",
"WhaleStateWorking": "Працює",
"WhaleStateWaiting": "Чекає на вас",
"WhaleStateBlocked": "Заблоковано",
"WhaleStateOffline": "Поза мережею",
"WhaleAnimalScout": "дзьоборил",
"WhaleAnimalPatch": "морська свиня",
"WhaleAnimalHarbor": "горбатий кит",
"WhaleAnimalEcho": "гринда",
"WhaleAnimalKeel": "кашалот",
"WhaleAnimalLantern": "косатка",
"WhaleAnimalPlain": "кит",
"WhaleJobScout": "дослідження",
"WhaleJobPatch": "програмування",
"WhaleJobHarbor": "координація",
"WhaleJobEcho": "комунікації",
"WhaleJobKeel": "експлуатація",
"WhaleJobLantern": "рев'ю",
"WhaleJobPlain": "загальна робота",
"SessionMetricsTurn": "хід",
"SessionMetricsTurns": "ходів",
"SessionMetricsStep": "крок",
"SessionMetricsSteps": "кроків",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Інстр.",
"SessionMetricsTtft": "TTFT сер.",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Кеш",
"SessionMetricsInput": "Ввід",
"SessionMetricsStatusLine": "Метрики сесії: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review дозволив '{tool}' (ризик {risk}, модельний вартовий): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review відхилив '{tool}' (ризик {risk}, модельний вартовий): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review не зміг перевірити '{tool}' ({reason}); відхилено, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review заблокував '{tool}' (детермінована політика): {reason}",
"AutoReviewReceiptHeld": "Auto-Review утримав '{tool}' без паузи; відхилено (потрібна людина — перемкніться на Ask)",
"FooterHintEscInterrupt": "Esc — перервати",
"PermissionsPostureHeader": "Поточний режим дозволів: {posture}",
"PermissionsPostureAsk": "Ask: виклики tool, що змінюють повноваження, вартість, обсяг або результат, відкривають запит; доведено безпечні виклики лише для читання виконуються без нього. Правила ask вище завжди примусово показують запит.",
"PermissionsPostureAuto": "Auto-Review: ніколи не відкриває запит. Детермінована політика дозволяє доведено безпечні виклики та жорстко блокує публікаційну або руйнівну фонову роботу; виклики, безпечність яких довести не можна, передаються одноразовому модельному вартовому, який дозволяє або відхиляє з указанням причини (високий або критичний ризик ніколи не виконується автоматично; невдала перевірка відхиляє за принципом fail closed). Утримання, що потребують людини, відхиляються, а не приховуються. Кожне таке рішення записується нотаткою в транскрипт і в журнал аудиту.",
"PermissionsPostureBypass": "Full Access: звичайні виклики tool виконуються без запитів. Необхідні утримання безпеки, правил репозиторію та керованої політики завершуються fail closed як жорсткі блокування замість запиту.",
"PermissionsPostureNever": "never: виконуються лише tool, що вважаються безпечними/лише для читання; усе інше блокується без запиту.",
"PermissionsReceiptsNote": "Рішення, ухвалені без запиту (вердикти вартового Auto-Review, блокування та утримання), відображаються нотатками в транскрипті та в журналі аудиту {audit_path}. Full Access обирається свідомо через Shift+Tab або /config, ніколи правилом.",
"AgentFocusOpened": "Фокус на {agent}. Тепер ваші повідомлення надходять цьому воркеру; Esc повертає до основної розмови.",
"AgentFocusClosed": "Знову в основній розмові.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Повідомлення для {agent} · Esc — до основної розмови",
"AgentFocusNoTranscript": "Для {agent} ще немає запису розмови. Повідомлення з'являться тут, щойно воркер почне ними обмінюватися.",
"AgentFocusOmitted": "Попередні повідомлення ({count}) не увійшли до запису розмови в пам'яті.",
"AgentFocusFollowUpDelivered": "У черзі для {agent}: він прочитає повідомлення на наступному раунді.",
"AgentFocusFollowUpQueued": "У черзі для {agent}",
"AgentFocusFollowUpContinued": "{agent} уже завершився; продовжено в новому форку ({target}). Це подання тепер стежить за форком.",
"AgentFocusFollowUpFailed": "Не вдалося доставити {agent}: {reason}",
"FooterHintForAgents": "агенти",
"FooterHintToManage": "керування",
"AgentRailQueuedCount": "{count} у черзі",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "пише",
"AgentFocusPostureReadOnly": "лише читання",
"AgentFocusPostureNetwork": "мережа",
"AgentFocusPostureNoNetwork": "без мережі",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell лише читання",
"AgentFocusPostureShellNone": "без shell",
"GoalReceiptSet": "Ціль задано: «{objective}» · /goal показує прогрес · /goal pause або /goal clear зупиняє",
"GoalStatusIdleHint": "зараз не виконується — надішліть повідомлення або /goal resume, щоб продовжити"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "Nhập nhiệm vụ hoặc sử dụng /.",
"ComposerOperatePlaceholder": "Mô tả mục tiêu — Codewhale tiếp tục làm việc cho đến khi hoàn tất",
"ComposerDispatchFailedRestored": "Chưa gửi được tin nhắn ({error}); đã khôi phục vào ô soạn thảo.",
"DispatchFailedQueued": "Gửi thất bại ({error}); giữ lại {count} tin nhắn theo dõi trong hàng đợi.",
"DispatchFailedInitial": "Không thể gửi lời nhắc ban đầu: {error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "Nhà cung cấp đang dùng",
"ConfigLabelBaseUrlDeepseek": "URL API nhà cung cấp (tuyến DeepSeek)",
"ConfigLabelProviderUrl": "URL API nhà cung cấp",
"ConfigHintProviderUrl": "Điểm cuối hiện tại của nhà cung cấp; Xiaomi: gói token | trả theo mức dùng | URL tùy chỉnh",
"ConfigLabelModel": "Mô hình đang dùng của nhà cung cấp",
"ConfigLabelFastModel": "Mô hình nhanh (suy ra)",
"ConfigLabelDefaultModel": "Mô hình dự phòng cũ (chỉ tuyến DeepSeek)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "mỗi lượt · heuristic cục bộ · không gửi yêu cầu định tuyến",
"ModelPickerAutoLastRoute": "lần trước {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model} qua {source} · Ctrl+O: chi tiết tuyến",
"CloudCodeSystemPromptUnsupported": "Antigravity cloud-code không thể gửi lượt này an toàn vì kết nối này chưa hỗ trợ chỉ dẫn hệ thống. Chưa gửi gì; hãy chọn nhà cung cấp khác.",
"HelpTitle": "Trợ giúp",
"HelpFilterPlaceholder": "Nhập để lọc",
"HelpFilterPrefix": "Bộ lọc: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "Tải phiên làm việc từ tệp",
"CmdLogoutDescription": "Xóa khóa API và quay lại bước thiết lập",
"CmdMcpDescription": "Mở hoặc quản lý các máy chủ MCP",
"McpRecommendedUnknownId": "ID MCP được đề xuất không xác định. Chạy {recommendations_command} để xem danh sách tuyển chọn.",
"McpRecommendationsSafety": "Xem danh sách này không thêm hoặc bật gì cả. Thao tác thêm rõ ràng chỉ ghi cấu hình; hãy kiểm tra trước khi {restart_command} kết nối máy chủ.",
"McpRecommendationGithub": "• github — điểm cuối MCP từ xa chính thức của GitHub\n điểm cuối: {endpoint}\n xác thực là bước riêng: chỉ dùng {login_command} khi máy chủ công bố OAuth;\n nếu không, hãy cấu hình PAT có quyền tối thiểu ngoài lịch sử lệnh. Phạm vi được\n cấp có thể ghi hoặc xóa dữ liệu kho mã; nên bắt đầu ở chế độ chỉ đọc khi có thể.\n thêm rõ ràng: {add_command}",
"McpRecommendationChrome": "• chrome-devtools — MCP Chrome DevTools chính thức qua gói npm đã ghim phiên bản\n gói: {package} ({launcher})\n có thể kiểm tra/điều khiển Chrome và đọc trang đã xác thực. Đóng các thẻ\n nhạy cảm và xác minh gói trước khi thêm; {restart_command} có thể tải xuống và chạy gói.\n thêm rõ ràng: {add_command}",
"PluginKimiUsage": "Cách dùng:\n {list_command}\n {approve_command}\nDanh sách chỉ để đọc. Khi phê duyệt, một plugin chuẩn do Kimi quản lý được sao chép qua trình cài đặt đã duyệt; plugin vẫn bị tắt và chưa được tin cậy.",
"PluginKimiManagedRootHeading": "Plugin do Kimi quản lý tại {root}:",
"PluginKimiNoneFound": "Không tìm thấy plugin được quản lý hợp lệ.",
"PluginKimiLicenseUnspecified": "chưa chỉ định",
"PluginKimiApplicable": "dùng được trên HĐH này",
"PluginKimiNotApplicable": "không dùng được trên HĐH này",
"PluginKimiCandidateSummary": "{name} {version} — giấy phép={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " đường dẫn: {path}\n hàm băm nội dung: {content_hash}\n hàm băm khả năng: {capability_hash}\n phê duyệt: {approve_command}",
"PluginKimiRejectedHeading": "Mục bị từ chối (không thể nhập):",
"PluginKimiInspectionFooter": "Lần kiểm tra này không sao chép, tin cậy, bật hoặc chạy gì cả. Ứng dụng, daemon, tệp nhị phân, tiện ích trình duyệt, thông tin xác thực và quyền HĐH bên ngoài của Kimi chưa được kiểm tra.",
"PluginKimiCandidateMissing": "Không có plugin chuẩn hợp lệ do Kimi quản lý tên `{name}`. Chạy lại {list_command}.",
"PluginKimiCandidateChanged": "Plugin do Kimi quản lý `{name}` đã thay đổi sau khi duyệt. Hàm băm dự kiến {expected}, hiện là {actual}. Chưa sao chép gì; chạy lại {list_command}.",
"PluginKimiHomeMissing": "Không tìm thấy thư mục nhà người dùng để nhập từ Kimi.",
"PluginKimiRootInspectFailed": "Không thể kiểm tra thư mục gốc plugin Kimi {root}: {error}",
"PluginKimiRootMustBeDirectory": "Thư mục gốc plugin Kimi {root} phải là thư mục thật, không phải liên kết hay điểm phân tích lại.",
"PluginKimiRootCanonicalizeFailed": "Không thể chuẩn hóa thư mục gốc plugin Kimi {root}: {error}",
"PluginKimiRootListFailed": "Không thể liệt kê thư mục gốc plugin Kimi {root}: {error}",
"PluginKimiEntryReadFailed": "Không thể đọc mục plugin Kimi: {error}",
"PluginKimiEntryLimit": "Thư mục gốc plugin Kimi có {count} mục; mỗi lần quét chỉ duyệt tối đa {max}.",
"PluginKimiEntryInspectFailed": "{path}: không thể kiểm tra: {error}",
"PluginKimiEntryLinksRefused": "{path}: từ chối liên kết và điểm phân tích lại",
"PluginKimiEntryOutsideRoot": "{path}: đường dẫn chuẩn {canonical_path} không phải con trực tiếp của thư mục gốc được quản lý",
"PluginKimiEntryCanonicalizeFailed": "{path}: không thể chuẩn hóa: {error}",
"PluginKimiManifestUnreadable": "{path}: không có {manifest} đọc được: {error}",
"PluginKimiManifestMustBeFile": "{path}: {manifest} phải là tệp thông thường thật",
"PluginKimiManifestInvalid": "{path}: bản kê khai không hợp lệ: {error}",
"PluginKimiDirectoryNameMismatch": "{path}: tên thư mục phải khớp chính xác với tên `{name}` trong bản kê khai",
"PluginKimiHashUnavailable": "không khả dụng",
"PluginKimiRollbackDestinationMissing": "Trình cài đặt không báo đường dẫn đích.",
"PluginKimiMismatchRemoved": "Plugin `{name}` sau khi sao chép không khớp nội dung đã duyệt (dự kiến {expected}, tìm thấy {actual}). Bản sao ngoài dự kiến đã bị xóa; hãy duyệt và thử lại.",
"PluginKimiMismatchRollbackFailed": "Lỗi: plugin `{name}` sau khi sao chép không khớp nội dung đã duyệt (dự kiến {expected}, tìm thấy {actual}) và xóa tự động thất bại: {error}. Plugin vẫn bị tắt và chưa được tin cậy; hãy kiểm tra {path} trước khi tiếp tục.",
"PluginKimiUserPluginDirectory": "thư mục plugin của người dùng",
"PluginKimiMarketplaceZipUnsupported": "Trình cài đặt đã duyệt của Codewhale không hỗ trợ gói ZIP Kimi; hãy cài từ thư mục cục bộ hoặc nhập plugin do Kimi quản lý từ nguồn thượng lưu.",
"PluginKimiMarketplaceRemoteUnsupported": "Nguồn Kimi từ xa phải kết thúc bằng .tar.gz hoặc .tgz để Codewhale cài đặt; .zip được nhận diện nhưng không được hỗ trợ.",
"PluginKimiMarketplaceGzipTarball": "URL tarball gzip",
"CmdPluginDescription": "Kiểm tra và quản lý gói plugin đáng tin cậy; công cụ thực thi cũ vẫn được tách riêng",
"CmdPluginBundleUsage": "Cách dùng: /plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "Không tìm thấy gói plugin Codewhale nào.",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale không tải lên, di chuyển hoặc đồng bộ mã nguồn cục bộ vào Work được lưu trữ. Dùng {command} để bắt đầu từ đầu nhánh hiện có trên GitHub hoặc CNB. Các commit chưa đẩy, tệp đã thay đổi hoặc bị bỏ qua, bí mật và trạng thái phiên vẫn ở cục bộ.",
"CmdRemoteEnvBrowserLabel": "Work được Codewhale lưu trữ",
"CmdRenameDescription": "Đổi tên phiên làm việc hiện tại",
"CmdTitleDescription": "Đặt tên phiên hiện tại và tab/cửa sổ terminal",
"CmdRestoreDescription": "Khôi phục không gian làm việc về bản chụp trước/sau lượt. Nếu không có đối số, hiển thị các bản chụp gần đây.",
"CmdRetryDescription": "Thử lại yêu cầu gần nhất",
"CmdReviewDescription": "Chạy một quy trình xem xét mã nguồn có cấu trúc trên tệp, diff hoặc PR",
@@ -990,6 +1033,8 @@
"SetupToolsMcpNeedsActionSaved": "Tools/MCP vẫn cần xử lý; đã ghi vào báo cáo (không chặn lần chạy đầu).",
"SetupToolsMcpPreviewTitle": "On-ramp an toàn Tools / MCP",
"SetupToolsMcpOnRampText": "Tools, MCP, Skills & Plugins — On-ramp an toàn\n\n/setup chỉ đọc kho cục bộ. Không khởi động máy chủ MCP, không cài skill, không chạy plugin hay lệnh không tin cậy.\n\nKho hiện tại:\n- MCP: {mcp_result}\n- Skills: {skills_result}\n- Tools: {tools_result}\n- Plugins: {plugins_result}\n- Hotbar (adapter dùng chung): {hotbar_result}\n\nĐường dẫn:\n- MCP: {mcp_path}\n- Skills: {skills_path}\n- Plugins: {plugins_path}\n\nBootstrap an toàn (bạn tự chạy):\n- MCP: /mcp · codewhale mcp init · codewhale doctor\n- Skills: /skills · codewhale setup --skills\n- Plugins: /plugin · codewhale setup --plugins\n- Tools: codewhale setup --tools\n\nMọi thao tác có tác dụng phụ luôn cần xác nhận rõ ràng. Xem docs/MCP.md.",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh):",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — kết nối thông qua Codewhale, không bao giờ là bộ lập lịch thứ hai:\n- Trạng thái: {dsh_result}\n- Phát hiện chỉ đọc; kết nối/lập kế hoạch/khởi chạy/gỡ bỏ: codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale chỉ ghi vào $CODEWHALE_HOME/integrations/dsh; không bao giờ sao chép khóa API hay sửa tệp của DSH.",
"HotbarActionModeOperateName": "Chế độ Operate",
"HotbarActionModeOperateDescription": "Cho Fleet của bạn làm việc song song.",
"HomeOperateModeTip": "Operate — cho Fleet của bạn làm việc song song",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet sẵn sàng",
"EmptyStateHelpConnector": "hoặc",
"EmptyStateHelpHint": "— xem mọi thứ",
"SessionsSurfaceTitle": "phiên",
"SessionsPaneTitle": " phiên (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "Không thể xác minh danh tính hồ sơ hiện có ({error}); hãy sửa tệp được nêu trước khi lưu.",
"FleetProfileIdConflict": "Id hồ sơ `{id}` đã được {path} sử dụng; hãy soạn lại với vai trò khác hoặc xóa tệp cũ trước.",
"FleetProfileProviderUnconfigured": "Hồ sơ chỉ định nhà cung cấp `{provider}` chưa có thông tin xác thực ({env}); hãy thiết lập trong /provider trước khi lưu.",
"FleetDestStepTitle": "Hồ sơ này nên được lưu ở đâu?",
"FleetDestStepSubtitle": "Không có gì được ghi cho đến khi bạn xác nhận ở bước cuối.",
"FleetDestProjectLabel": "Dự án này",
"FleetDestPersonalLabel": "Cá nhân",
"FleetDestProjectSummary": "Chỉ dự án này",
"FleetDestPersonalSummary": "Dùng được trong mọi dự án",
"FleetDestProjectDescription": "Lưu bên trong dự án này ({workspace}). Chỉ áp dụng tại đây và được ưu tiên hơn hồ sơ Cá nhân có cùng ID.",
"FleetDestPersonalDescription": "Lưu trong thư mục Codewhale của bạn. Áp dụng trong mọi dự án — trừ khi một dự án có hồ sơ riêng cùng ID, khi đó hồ sơ dự án được ưu tiên tại đó.",
"FleetDestPathLine": "Tệp: {path}",
"FleetDestUnavailable": "Không khả dụng: {reason}",
"FleetDestReasonNoProjectConfig": "hồ sơ dự án đã bị tắt trong phiên này (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "thư mục workspace {path} không tồn tại hoặc không phải là thư mục",
"FleetDestReasonHomeUnavailable": "không thể xác định thư mục Codewhale của bạn ({error})",
"FleetDestWillReplace": "Sẽ thay thế tệp hiện có {path}",
"FleetDestOverridesProject": "Dự án này đã có hồ sơ '{id}' và nó được ưu tiên tại đây; hồ sơ Cá nhân này áp dụng trong các dự án khác.",
"FleetDestOverridesPersonal": "Được ưu tiên hơn hồ sơ Cá nhân '{id}' của bạn trong dự án này.",
"FleetDestOverridesBuiltIn": "Thay thế vai trò {origin} '{id}' trong danh sách đội.",
"FleetSavesToChip": "Lưu vào: {scope} · {path}",
"FleetSavesToUndecided": "Lưu vào: chọn ở bước 3 — Dự án này hoặc Cá nhân",
"FleetActionSaveProject": "Lưu vào dự án này",
"FleetActionSavePersonal": "Lưu làm hồ sơ Cá nhân",
"FleetActionReplaceProject": "Thay thế trong dự án này",
"FleetActionReplacePersonal": "Thay thế hồ sơ Cá nhân",
"FleetActionConfirmReplace": "Nhấn Enter lần nữa để thay thế {file}",
"FleetActionChangeDestination": "Đổi nơi lưu",
"FleetActionBack": "Quay lại",
"FleetReviewSavesTo": "Lưu vào",
"FleetModelRowBlockedNotice": "Không thể chọn: {reason}. Hãy cấu hình trong /provider hoặc chọn dòng khác.",
"FleetDestProjectDisabledSave": "Hồ sơ dự án đã bị tắt trong phiên này (--no-project-config); chưa lưu gì. Hãy chọn Cá nhân hoặc khởi động lại không có cờ đó.",
"WorkflowStatusWaiting": "chờ",
"WorkflowDebrief": "tổng kết: {done}/{total} đã chốt · {failed} thất bại · {cancelled} đã hủy · {elapsed}",
"WorkflowTranscriptDetails": "bản ghi: JSON đầy đủ của lượt chạy có trong chi tiết công cụ ({details})",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "Lần chạy tự động hóa {id} đã được đưa vào hàng đợi: {status} (tác vụ {task})",
"AutomationDeletePreview": "Thao tác xóa chưa được xác nhận. Chưa có gì bị xóa.\nTự động hóa: {id} ({name})\nLần chạy đã ghi: {run_count}\nĐể xóa định nghĩa và lịch sử chạy, hãy chạy:\n{command}",
"AutomationDeleteConfirmationStale": "Xác nhận xóa không còn khớp với tự động hóa {id}; chưa có gì bị xóa. Xem lại trạng thái hiện tại bằng {command}.",
"AutomationDeleted": "Đã xóa tự động hóa {id} ({name}). Số lần chạy đã ghi bị xóa: {run_count}."
"AutomationDeleted": "Đã xóa tự động hóa {id} ({name}). Số lần chạy đã ghi bị xóa: {run_count}.",
"WhaleStateResting": "Đang nghỉ",
"WhaleStateThinking": "Đang suy nghĩ",
"WhaleStateWorking": "Đang làm việc",
"WhaleStateWaiting": "Đang chờ bạn",
"WhaleStateBlocked": "Bị chặn",
"WhaleStateOffline": "Ngoại tuyến",
"WhaleAnimalScout": "cá voi mõm khoằm",
"WhaleAnimalPatch": "cá heo chuột",
"WhaleAnimalHarbor": "cá voi lưng gù",
"WhaleAnimalEcho": "cá voi hoa tiêu",
"WhaleAnimalKeel": "cá nhà táng",
"WhaleAnimalLantern": "cá voi sát thủ",
"WhaleAnimalPlain": "cá voi",
"WhaleJobScout": "nghiên cứu",
"WhaleJobPatch": "lập trình",
"WhaleJobHarbor": "điều phối",
"WhaleJobEcho": "liên lạc",
"WhaleJobKeel": "vận hành",
"WhaleJobLantern": "đánh giá",
"WhaleJobPlain": "việc chung",
"SessionMetricsTurn": "lượt",
"SessionMetricsTurns": "lượt",
"SessionMetricsStep": "bước",
"SessionMetricsSteps": "bước",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "Công cụ",
"SessionMetricsTtft": "TTFT TB",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "Cache",
"SessionMetricsInput": "Đầu vào",
"SessionMetricsStatusLine": "Số liệu phiên: {metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review đã cho phép '{tool}' (rủi ro {risk}, bộ giám hộ mô hình): {reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review đã từ chối '{tool}' (rủi ro {risk}, bộ giám hộ mô hình): {reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review không thể xem xét '{tool}' ({reason}); đã từ chối, fail closed",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review đã chặn '{tool}' (chính sách tất định): {reason}",
"AutoReviewReceiptHeld": "Auto-Review đã giữ lại '{tool}' mà không tạm dừng; đã từ chối (cần con người — chuyển sang Ask)",
"FooterHintEscInterrupt": "Esc để ngắt",
"PermissionsPostureHeader": "Tư thế quyền hiện tại: {posture}",
"PermissionsPostureAsk": "Ask: các lệnh gọi tool làm thay đổi thẩm quyền, chi phí, phạm vi hoặc kết quả sẽ mở lời nhắc; các lệnh chỉ đọc đã được chứng minh an toàn chạy mà không cần lời nhắc. Các quy tắc ask ở trên luôn buộc phải nhắc.",
"PermissionsPostureAuto": "Auto-Review: không bao giờ mở lời nhắc. Chính sách tất định cho phép các lệnh đã được chứng minh an toàn và chặn cứng công việc nền mang tính xuất bản hoặc phá hủy; các lệnh không thể chứng minh an toàn được chuyển đến bộ giám hộ mô hình một lần, cho phép hoặc từ chối kèm lý do (rủi ro cao hoặc nghiêm trọng không bao giờ tự chạy; xem xét thất bại sẽ từ chối, fail closed). Các lệnh cần người quyết định bị từ chối chứ không bị ẩn. Mỗi quyết định như vậy được ghi vào bản ghi dưới dạng ghi chú và vào nhật ký kiểm toán.",
"PermissionsPostureBypass": "Full Access: các lệnh gọi tool thông thường chạy không cần lời nhắc. Các lệnh giữ không thể bỏ qua về an toàn, luật kho lưu trữ và chính sách quản lý sẽ fail closed dưới dạng chặn cứng thay vì hỏi.",
"PermissionsPostureNever": "never: chỉ các tool được coi là an toàn/chỉ đọc mới chạy; mọi thứ khác bị chặn mà không nhắc.",
"PermissionsReceiptsNote": "Các quyết định không qua lời nhắc (phán quyết của bộ giám hộ Auto-Review, chặn và giữ) xuất hiện dưới dạng ghi chú trong bản ghi và trong nhật ký kiểm toán tại {audit_path}. Full Access được chọn có chủ ý bằng Shift+Tab hoặc /config, không bao giờ do quy tắc.",
"AgentFocusOpened": "Đang tập trung vào {agent}. Tin nhắn của bạn giờ gửi tới worker này; nhấn Esc để về cuộc trò chuyện chính.",
"AgentFocusClosed": "Đã quay lại cuộc trò chuyện chính.",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "Nhắn cho {agent} · Esc để về chính",
"AgentFocusNoTranscript": "Chưa có bản ghi nào của {agent}. Tin nhắn sẽ hiện ở đây khi worker trao đổi.",
"AgentFocusOmitted": "Các tin nhắn trước đó ({count}) đã bị lược khỏi bản ghi trong bộ nhớ.",
"AgentFocusFollowUpDelivered": "Đã xếp hàng cho {agent}: nó sẽ đọc tin nhắn ở vòng tiếp theo.",
"AgentFocusFollowUpQueued": "Đã xếp hàng cho {agent}",
"AgentFocusFollowUpContinued": "{agent} đã hoàn thành; tiếp tục trên một nhánh mới ({target}). Khung nhìn này giờ theo nhánh đó.",
"AgentFocusFollowUpFailed": "Không thể gửi tới {agent}: {reason}",
"FooterHintForAgents": "agent",
"FooterHintToManage": "quản lý",
"AgentRailQueuedCount": "{count} đang chờ",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "được ghi",
"AgentFocusPostureReadOnly": "chỉ đọc",
"AgentFocusPostureNetwork": "có mạng",
"AgentFocusPostureNoNetwork": "không mạng",
"AgentFocusPostureShellFull": "shell",
"AgentFocusPostureShellReadOnly": "shell chỉ đọc",
"AgentFocusPostureShellNone": "không shell",
"GoalReceiptSet": "Đã đặt mục tiêu: \"{objective}\" · /goal hiển thị tiến độ · /goal pause hoặc /goal clear để dừng",
"GoalStatusIdleHint": "hiện không chạy — gửi tin nhắn hoặc /goal resume để tiếp tục"
}
+143 -1
View File
@@ -1,5 +1,6 @@
{
"ComposerPlaceholder": "编写任务或使用 /。",
"ComposerOperatePlaceholder": "描述目标 — Codewhale 会持续工作,直到完成",
"ComposerDispatchFailedRestored": "消息未发送({error});已恢复到输入框。",
"DispatchFailedQueued": "发送失败({error});保留了 {count} 个排队后续消息。",
"DispatchFailedInitial": "初始提示无法发送:{error}",
@@ -112,6 +113,7 @@
"ConfigLabelProvider": "当前提供商",
"ConfigLabelBaseUrlDeepseek": "提供商 API 地址(DeepSeek 路由)",
"ConfigLabelProviderUrl": "提供商 API 地址",
"ConfigHintProviderUrl": "当前提供商端点;Xiaomi:令牌套餐 | 按量付费 | 自定义 URL",
"ConfigLabelModel": "当前提供商模型",
"ConfigLabelFastModel": "快速模型(派生)",
"ConfigLabelDefaultModel": "旧版后备模型(仅 DeepSeek 路由)",
@@ -181,6 +183,7 @@
"ModelPickerAutoLocalHint": "每轮 · 本地启发式 · 不发送路由请求",
"ModelPickerAutoLastRoute": "上次 {provider} · {model}",
"AutoRouteSelectedToast": "Auto: {provider} / {model}{source})· Ctrl+O:路由详情",
"CloudCodeSystemPromptUnsupported": "此连接尚不支持系统指令,因此 Antigravity cloud-code 无法安全发送本轮。未发送任何内容;请选择其他提供商。",
"HelpTitle": "帮助",
"HelpFilterPlaceholder": "输入以筛选",
"HelpFilterPrefix": "筛选: ",
@@ -252,6 +255,45 @@
"CmdLoadDescription": "从文件加载会话",
"CmdLogoutDescription": "清除 API 密钥并返回设置",
"CmdMcpDescription": "打开或管理 MCP 服务器",
"McpRecommendedUnknownId": "未知的推荐 MCP ID。运行 {recommendations_command} 查看精选列表。",
"McpRecommendationsSafety": "查看此列表不会添加或启用任何内容。显式添加只会写入配置;请在 {restart_command} 连接服务器前检查配置。",
"McpRecommendationGithub": "• github — GitHub 官方远程 MCP 端点\n 端点:{endpoint}\n 身份验证独立进行:仅在服务器声明支持 OAuth 时使用 {login_command};\n 否则请在命令历史之外配置最小权限 PAT。授予的范围可能写入或删除\n 仓库数据,因此请尽可能从只读权限开始。\n 显式添加:{add_command}",
"McpRecommendationChrome": "• chrome-devtools — 通过锁定版本的 npm 包提供的官方 Chrome DevTools MCP\n 包:{package}{launcher}\n 它可以检查或控制 Chrome,并读取已认证页面。请关闭敏感标签页并在\n 添加前验证软件包;{restart_command} 可能会下载并运行它。\n 显式添加:{add_command}",
"PluginKimiUsage": "用法:\n {list_command}\n {approve_command}\n列表操作为只读。批准后会通过已审查的安装器复制一个规范的 Kimi 托管插件;它仍保持禁用且不受信任。",
"PluginKimiManagedRootHeading": "{root} 中的 Kimi 托管插件:",
"PluginKimiNoneFound": "未找到有效的托管插件。",
"PluginKimiLicenseUnspecified": "未指定",
"PluginKimiApplicable": "适用于此操作系统",
"PluginKimiNotApplicable": "不适用于此操作系统",
"PluginKimiCandidateSummary": "{name} {version} — 许可证={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " 路径:{path}\n 内容哈希:{content_hash}\n 能力哈希:{capability_hash}\n 批准:{approve_command}",
"PluginKimiRejectedHeading": "已拒绝的条目(不可导入):",
"PluginKimiInspectionFooter": "本次检查没有复制、信任、启用或执行任何内容,也未检查外部 Kimi 应用、守护进程、二进制文件、浏览器扩展、凭据或操作系统权限。",
"PluginKimiCandidateMissing": "没有名为 `{name}` 的有效 Kimi 托管规范插件。请重新运行 {list_command}。",
"PluginKimiCandidateChanged": "Kimi 托管插件 `{name}` 自审查后已更改。预期内容哈希为 {expected},当前为 {actual}。未复制任何内容;请重新运行 {list_command}。",
"PluginKimiHomeMissing": "找不到用于 Kimi 导入的用户主目录。",
"PluginKimiRootInspectFailed": "无法检查 Kimi 托管插件根目录 {root}{error}",
"PluginKimiRootMustBeDirectory": "Kimi 托管插件根目录 {root} 必须是真实目录,不能是链接或重解析点。",
"PluginKimiRootCanonicalizeFailed": "无法规范化 Kimi 托管插件根目录 {root}{error}",
"PluginKimiRootListFailed": "无法列出 Kimi 托管插件根目录 {root}{error}",
"PluginKimiEntryReadFailed": "无法读取 Kimi 托管插件条目:{error}",
"PluginKimiEntryLimit": "Kimi 托管插件根目录包含 {count} 个条目;每次扫描最多审查 {max} 个。",
"PluginKimiEntryInspectFailed": "{path}:无法检查:{error}",
"PluginKimiEntryLinksRefused": "{path}:拒绝链接和重解析点",
"PluginKimiEntryOutsideRoot": "{path}:规范路径 {canonical_path} 不是托管根目录的直接子项",
"PluginKimiEntryCanonicalizeFailed": "{path}:无法规范化:{error}",
"PluginKimiManifestUnreadable": "{path}:没有可读的 {manifest}{error}",
"PluginKimiManifestMustBeFile": "{path}{manifest} 必须是真实的常规文件",
"PluginKimiManifestInvalid": "{path}:清单无效:{error}",
"PluginKimiDirectoryNameMismatch": "{path}:目录名必须与清单名称 `{name}` 完全一致",
"PluginKimiHashUnavailable": "不可用",
"PluginKimiRollbackDestinationMissing": "安装器未报告目标路径。",
"PluginKimiMismatchRemoved": "复制后的插件 `{name}` 与批准的内容不符(预期 {expected},实际 {actual})。已移除意外副本;请审查后重试。",
"PluginKimiMismatchRollbackFailed": "错误:复制后的插件 `{name}` 与批准的内容不符(预期 {expected},实际 {actual}),且自动移除失败:{error}。它仍处于禁用且不受信任状态;继续前请检查 {path}。",
"PluginKimiUserPluginDirectory": "用户插件目录",
"PluginKimiMarketplaceZipUnsupported": "Codewhale 的已审查安装器不支持 Kimi ZIP 包;请从本地目录安装,或导入上游 Kimi 托管插件。",
"PluginKimiMarketplaceRemoteUnsupported": "供 Codewhale 安装的 Kimi 远程源必须以 .tar.gz 或 .tgz 结尾;可识别 .zip,但暂不支持。",
"PluginKimiMarketplaceGzipTarball": "gzip 压缩 tar 包 URL",
"CmdPluginDescription": "检查和管理受信任的插件包;旧版可执行工具保持独立",
"CmdPluginBundleUsage": "用法:/plugin [list|show <name>|validate [name]|install <spec>|update <name>|uninstall <name>|trust <name> [review-token]|enable <name>|disable <name>|revoke <name>|reload|tools [name]]",
"CmdPluginBundleNoneFound": "未发现 Codewhale 插件包。",
@@ -306,6 +348,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale 不会将本地源代码上传、迁移或同步到托管 Work。使用 {command} 从 GitHub 或 CNB 上可用的分支尖端开始。未推送的提交、已修改或已忽略的文件、密钥和会话状态仍保留在本地。",
"CmdRemoteEnvBrowserLabel": "Codewhale 托管 Work",
"CmdRenameDescription": "重命名当前会话",
"CmdTitleDescription": "命名当前会话及其终端标签页/窗口",
"CmdRestoreDescription": "将工作区回滚到此前的轮次前/后快照。不带参数时列出最近的快照。",
"CmdRetryDescription": "重试上一次请求",
"CmdReviewDescription": "对文件、diff 或 PR 进行结构化代码审查",
@@ -990,6 +1033,8 @@
"SetupToolsMcpNeedsActionSaved": "工具/MCP 仍需处理;已记录到设置报告(不阻塞首次运行)。",
"SetupToolsMcpPreviewTitle": "工具 / MCP 安全引导",
"SetupToolsMcpOnRampText": "工具、MCP、技能与插件 — 安全引导\n\n/setup 只读取本地清单。不会启动 MCP 服务器、安装技能、运行插件或自动执行不受信任的命令。\n\n当前清单:\n- MCP{mcp_result}\n- 技能:{skills_result}\n- 工具目录:{tools_result}\n- 插件:{plugins_result}\n- 快捷栏(共享适配器):{hotbar_result}\n\n路径:\n- MCP{mcp_path}\n- 技能:{skills_path}\n- 插件:{plugins_path}\n\n安全引导(请自行在终端或 TUI 中执行):\n- MCP/mcp · codewhale mcp init · codewhale doctor\n- 技能:/skills · codewhale setup --skills\n- 插件:/plugin · codewhale setup --plugins\n- 工具:codewhale setup --tools\n\n有副作用的操作始终需要明确确认。详见 docs/MCP.md。",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh)",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — 通过 Codewhale 连接,绝不是第二个调度器:\n- 状态:{dsh_result}\n- 只读检测;连接/计划/启动/移除:codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale 只写入 $CODEWHALE_HOME/integrations/dsh,绝不复制 API 密钥或修改 DSH 文件。",
"HotbarActionModeOperateName": "Operate 模式",
"HotbarActionModeOperateDescription": "让 Fleet 并行开展工作。",
"HomeOperateModeTip": "Operate — 让 Fleet 并行开展工作",
@@ -1044,6 +1089,7 @@
"EmptyStateMcpLabel": "mcp",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet 已就绪",
"EmptyStateHelpConnector": "或",
"EmptyStateHelpHint": "— 查看所有功能",
"SessionsSurfaceTitle": "会话",
"SessionsPaneTitle": " 会话 (1-9) ",
@@ -1193,6 +1239,35 @@
"FleetProfileIdentityVerifyFailed": "无法校验现有配置标识({error});请先修复列出的文件再保存。",
"FleetProfileIdConflict": "配置 id `{id}` 已被 {path} 占用;请重新起草为不同的角色或先移除旧文件。",
"FleetProfileProviderUnconfigured": "配置指定的 provider `{provider}` 尚未配置凭据({env});请先在 /provider 中设置,再保存。",
"FleetDestStepTitle": "这个配置文件应保存在哪里?",
"FleetDestStepSubtitle": "在最后一步确认之前不会写入任何内容。",
"FleetDestProjectLabel": "此项目",
"FleetDestPersonalLabel": "个人",
"FleetDestProjectSummary": "仅此项目",
"FleetDestPersonalSummary": "在所有项目中可用",
"FleetDestProjectDescription": "保存在此项目({workspace})内。仅在此处生效,并优先于同 ID 的个人配置文件。",
"FleetDestPersonalDescription": "保存在你的 Codewhale 主目录。在所有项目中生效——除非某个项目有同 ID 的自有配置文件,那里以项目配置为准。",
"FleetDestPathLine": "文件: {path}",
"FleetDestUnavailable": "不可用: {reason}",
"FleetDestReasonNoProjectConfig": "本次会话已禁用项目配置文件 (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "工作区文件夹 {path} 不存在或不是目录",
"FleetDestReasonHomeUnavailable": "无法解析你的 Codewhale 主目录 ({error})",
"FleetDestWillReplace": "将替换现有文件 {path}",
"FleetDestOverridesProject": "此项目已有 '{id}' 配置文件,在此处以它为准;这个个人配置文件将在其他项目中生效。",
"FleetDestOverridesPersonal": "在此项目内优先于你的个人 '{id}' 配置文件。",
"FleetDestOverridesBuiltIn": "替换名册中的 {origin} '{id}' 角色。",
"FleetSavesToChip": "保存到: {scope} · {path}",
"FleetSavesToUndecided": "保存到: 在第 3 步选择——此项目 或 个人",
"FleetActionSaveProject": "保存到此项目",
"FleetActionSavePersonal": "保存为个人配置文件",
"FleetActionReplaceProject": "替换此项目中的文件",
"FleetActionReplacePersonal": "替换个人配置文件",
"FleetActionConfirmReplace": "再按一次 Enter 以替换 {file}",
"FleetActionChangeDestination": "更改保存位置",
"FleetActionBack": "返回",
"FleetReviewSavesTo": "保存到",
"FleetModelRowBlockedNotice": "不可选择: {reason}。请在 /provider 中配置或选择其他行。",
"FleetDestProjectDisabledSave": "本次会话已禁用项目配置文件 (--no-project-config);未保存任何内容。请选择个人,或不带该参数重新启动。",
"WorkflowStatusWaiting": "等待中",
"WorkflowDebrief": "复盘:{done}/{total} 已完结 · {failed} 失败 · {cancelled} 已取消 · {elapsed}",
"WorkflowTranscriptDetails": "记录:完整运行 JSON 可在工具详情中查看({details}",
@@ -1338,5 +1413,72 @@
"AutomationRunEnqueued": "自动化 {id} 的运行已入队: {status}(任务 {task}",
"AutomationDeletePreview": "删除尚未确认,未删除任何内容。\n自动化: {id}{name}\n运行记录: {run_count}\n要删除定义和运行历史,请运行:\n{command}",
"AutomationDeleteConfirmationStale": "删除确认与自动化 {id} 的当前状态不再匹配,未删除任何内容。请用 {command} 查看当前状态。",
"AutomationDeleted": "已删除自动化 {id}({name})。已删除的运行记录: {run_count}。"
"AutomationDeleted": "已删除自动化 {id}({name})。已删除的运行记录: {run_count}。",
"WhaleStateResting": "休息中",
"WhaleStateThinking": "思考中",
"WhaleStateWorking": "工作中",
"WhaleStateWaiting": "等待你",
"WhaleStateBlocked": "已阻塞",
"WhaleStateOffline": "离线",
"WhaleAnimalScout": "喙鲸",
"WhaleAnimalPatch": "港湾鼠海豚",
"WhaleAnimalHarbor": "座头鲸",
"WhaleAnimalEcho": "领航鲸",
"WhaleAnimalKeel": "抹香鲸",
"WhaleAnimalLantern": "虎鲸",
"WhaleAnimalPlain": "鲸",
"WhaleJobScout": "调研",
"WhaleJobPatch": "编码",
"WhaleJobHarbor": "协调",
"WhaleJobEcho": "沟通",
"WhaleJobKeel": "运维",
"WhaleJobLantern": "评审",
"WhaleJobPlain": "通用工作",
"SessionMetricsTurn": "轮",
"SessionMetricsTurns": "轮",
"SessionMetricsStep": "步",
"SessionMetricsSteps": "步",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "工具调用",
"SessionMetricsTtft": "TTFT 平均",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "缓存命中",
"SessionMetricsInput": "输入",
"SessionMetricsStatusLine": "会话指标:{metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review 已允许 '{tool}'(风险 {risk},模型守护者):{reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review 已拒绝 '{tool}'(风险 {risk},模型守护者):{reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review 无法审查 '{tool}'{reason});已拒绝,故障关闭",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review 已阻止 '{tool}'(确定性策略):{reason}",
"AutoReviewReceiptHeld": "Auto-Review 未暂停即搁置了 '{tool}';已拒绝(需要人工决定 — 切换到 Ask)",
"FooterHintEscInterrupt": "Esc 中断",
"PermissionsPostureHeader": "当前权限姿态:{posture}",
"PermissionsPostureAsk": "Ask:会改变权限、成本、范围或结果的 tool 调用会弹出提示;已证明安全的只读调用无需提示即可运行。上面的 ask 规则始终强制提示。",
"PermissionsPostureAuto": "Auto-Review:从不弹出提示。确定性策略允许已证明安全的调用,并硬性阻止类似发布或破坏性的后台工作;无法证明安全的调用交给一次性模型守护者,其给出理由后允许或拒绝(高或严重风险绝不自动运行;审查失败即拒绝,故障关闭)。需要人工决定的搁置会被拒绝而非隐藏。每个此类决定都会作为备注写入对话记录和审计日志。",
"PermissionsPostureBypass": "Full Access:普通 tool 调用无需提示即可运行。不可绕过的安全、仓库规则和托管策略搁置会作为硬性阻止故障关闭,而不是弹出提示。",
"PermissionsPostureNever": "never:仅运行被视为安全/只读的 tool;其他一切均在不提示的情况下被阻止。",
"PermissionsReceiptsNote": "未经提示做出的决定(Auto-Review 守护者裁定、阻止和搁置)会作为对话记录备注显示,并写入 {audit_path} 的审计日志。Full Access 只能通过 Shift+Tab 或 /config 有意选择,绝不会由规则选择。",
"AgentFocusOpened": "已聚焦 {agent}。之后的消息将发送给该工作者;按 Esc 返回主对话。",
"AgentFocusClosed": "已返回主对话。",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "给 {agent} 发消息 · Esc 返回主对话",
"AgentFocusNoTranscript": "{agent} 尚无对话记录。工作者开始交流后会在此显示。",
"AgentFocusOmitted": "内存中的对话记录省略了较早的 {count} 条消息。",
"AgentFocusFollowUpDelivered": "已为 {agent} 排队:它会在下一轮读取该消息。",
"AgentFocusFollowUpQueued": "已为 {agent} 排队",
"AgentFocusFollowUpContinued": "{agent} 已完成;已在新分支({target})上继续。此视图现在跟随该分支。",
"AgentFocusFollowUpFailed": "无法发送给 {agent}{reason}",
"FooterHintForAgents": "智能体",
"FooterHintToManage": "管理",
"AgentRailQueuedCount": "{count} 条排队",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "可写入",
"AgentFocusPostureReadOnly": "只读",
"AgentFocusPostureNetwork": "可联网",
"AgentFocusPostureNoNetwork": "无网络",
"AgentFocusPostureShellFull": "可用 shell",
"AgentFocusPostureShellReadOnly": "只读 shell",
"AgentFocusPostureShellNone": "无 shell",
"GoalReceiptSet": "已设定目标:“{objective}” · /goal 查看进度 · /goal pause 或 /goal clear 停止",
"GoalStatusIdleHint": "当前未运行 — 发送消息或 /goal resume 以继续"
}
+143 -1
View File
@@ -77,6 +77,7 @@
"ApprovalTruncationHint": " … 已截斷 · 按 {details} 查看完整內容",
"AutoReviewQuestionSkipped": "Auto-Review 已略過使用者問題並自主繼續",
"AutoRouteSelectedToast": "Auto: {provider} / {model}{source})· Ctrl+O:路由詳情",
"CloudCodeSystemPromptUnsupported": "此連線尚不支援系統指令,因此 Antigravity cloud-code 無法安全傳送本輪。未傳送任何內容;請選擇其他供應商。",
"BehavioralTipBackgroundReceipt": "回執在 Work 面板中 — 按 {key} 開啟檢查器",
"BehavioralTipClearedInput": "已清空 · 按 {chord} 還原",
"BehavioralTipMcpValidation": "{command} 會啟動伺服器並顯示原因",
@@ -198,6 +199,45 @@
"CmdLogoutDescription": "清除 API 金鑰並返回設定",
"CmdLspDescription": "切換 LSP 診斷的開啟或關閉",
"CmdMcpDescription": "開啟或管理 MCP 伺服器",
"McpRecommendedUnknownId": "未知的建議 MCP ID。執行 {recommendations_command} 查看精選清單。",
"McpRecommendationsSafety": "查看此清單不會新增或啟用任何內容。明確新增只會寫入設定;請在 {restart_command} 連線伺服器前檢查設定。",
"McpRecommendationGithub": "• github — GitHub 官方遠端 MCP 端點\n 端點:{endpoint}\n 驗證會另外進行:只有伺服器宣告支援 OAuth 時才使用 {login_command};\n 否則請在指令歷程之外設定最小權限 PAT。授予的範圍可能寫入或刪除\n 儲存庫資料,因此請盡可能從唯讀權限開始。\n 明確新增:{add_command}",
"McpRecommendationChrome": "• chrome-devtools — 透過鎖定版本 npm 套件提供的官方 Chrome DevTools MCP\n 套件:{package}{launcher}\n 它可檢查或控制 Chrome,並讀取已驗證頁面。請關閉敏感分頁並在\n 新增前驗證套件;{restart_command} 可能會下載並執行它。\n 明確新增:{add_command}",
"PluginKimiUsage": "用法:\n {list_command}\n {approve_command}\n列出操作為唯讀。核准後會透過已審查的安裝程式複製一個標準的 Kimi 受管外掛程式;它仍保持停用且不受信任。",
"PluginKimiManagedRootHeading": "{root} 中的 Kimi 受管外掛程式:",
"PluginKimiNoneFound": "找不到有效的受管外掛程式。",
"PluginKimiLicenseUnspecified": "未指定",
"PluginKimiApplicable": "適用於此作業系統",
"PluginKimiNotApplicable": "不適用於此作業系統",
"PluginKimiCandidateSummary": "{name} {version} — 授權={license} — {applicability} — {inventory}",
"PluginKimiCandidateDetails": " 路徑:{path}\n 內容雜湊:{content_hash}\n 能力雜湊:{capability_hash}\n 核准:{approve_command}",
"PluginKimiRejectedHeading": "已拒絕的項目(不可匯入):",
"PluginKimiInspectionFooter": "本次檢查沒有複製、信任、啟用或執行任何內容,也未檢查外部 Kimi 應用程式、常駐程式、二進位檔、瀏覽器擴充功能、憑證或作業系統權限。",
"PluginKimiCandidateMissing": "沒有名為 `{name}` 的有效 Kimi 受管標準外掛程式。請重新執行 {list_command}。",
"PluginKimiCandidateChanged": "Kimi 受管外掛程式 `{name}` 自審查後已變更。預期內容雜湊為 {expected},目前為 {actual}。未複製任何內容;請重新執行 {list_command}。",
"PluginKimiHomeMissing": "找不到用於 Kimi 匯入的使用者家目錄。",
"PluginKimiRootInspectFailed": "無法檢查 Kimi 受管外掛程式根目錄 {root}{error}",
"PluginKimiRootMustBeDirectory": "Kimi 受管外掛程式根目錄 {root} 必須是真實目錄,不能是連結或重新剖析點。",
"PluginKimiRootCanonicalizeFailed": "無法標準化 Kimi 受管外掛程式根目錄 {root}{error}",
"PluginKimiRootListFailed": "無法列出 Kimi 受管外掛程式根目錄 {root}{error}",
"PluginKimiEntryReadFailed": "無法讀取 Kimi 受管外掛程式項目:{error}",
"PluginKimiEntryLimit": "Kimi 受管外掛程式根目錄包含 {count} 個項目;每次掃描最多審查 {max} 個。",
"PluginKimiEntryInspectFailed": "{path}:無法檢查:{error}",
"PluginKimiEntryLinksRefused": "{path}:拒絕連結和重新剖析點",
"PluginKimiEntryOutsideRoot": "{path}:標準路徑 {canonical_path} 不是受管根目錄的直接子項",
"PluginKimiEntryCanonicalizeFailed": "{path}:無法標準化:{error}",
"PluginKimiManifestUnreadable": "{path}:沒有可讀取的 {manifest}{error}",
"PluginKimiManifestMustBeFile": "{path}{manifest} 必須是真實的一般檔案",
"PluginKimiManifestInvalid": "{path}:資訊清單無效:{error}",
"PluginKimiDirectoryNameMismatch": "{path}:目錄名稱必須與資訊清單名稱 `{name}` 完全一致",
"PluginKimiHashUnavailable": "無法使用",
"PluginKimiRollbackDestinationMissing": "安裝程式未回報目的地路徑。",
"PluginKimiMismatchRemoved": "複製後的外掛程式 `{name}` 與核准的內容不符(預期 {expected},實際 {actual})。已移除非預期副本;請審查後重試。",
"PluginKimiMismatchRollbackFailed": "錯誤:複製後的外掛程式 `{name}` 與核准的內容不符(預期 {expected},實際 {actual}),且自動移除失敗:{error}。它仍處於停用且不受信任狀態;繼續前請檢查 {path}。",
"PluginKimiUserPluginDirectory": "使用者外掛程式目錄",
"PluginKimiMarketplaceZipUnsupported": "Codewhale 的已審查安裝程式不支援 Kimi ZIP 套件;請從本機目錄安裝,或匯入上游 Kimi 受管外掛程式。",
"PluginKimiMarketplaceRemoteUnsupported": "供 Codewhale 安裝的 Kimi 遠端來源必須以 .tar.gz 或 .tgz 結尾;可辨識 .zip,但目前不支援。",
"PluginKimiMarketplaceGzipTarball": "gzip 壓縮 tar 套件 URL",
"CmdMemoryDescription": "檢視或管理持久使用者記憶檔案",
"CmdModeDescription": "切換權限級別或開啟模式選擇器",
"CmdModelDbDescription": "參考:瀏覽內置的模型資料庫",
@@ -255,6 +295,7 @@
"CmdRemoteEnvSourceCustodyPolicy": "Codewhale 不會將本機原始碼上傳、移轉或同步到託管 Work。使用 {command} 從 GitHub 或 CNB 上可用的分支尖端開始。尚未推送的提交、已修改或已忽略的檔案、密鑰和工作階段狀態仍保留在本機。",
"CmdRemoteEnvBrowserLabel": "Codewhale 託管 Work",
"CmdRenameDescription": "重新命名目前工作階段",
"CmdTitleDescription": "命名目前工作階段及其終端機標籤頁/視窗",
"CmdRestoreDescription": "將工作區回復到此前的輪次前/後快照。不帶參數時列出最近的快照。",
"CmdRetryDescription": "重試上一次請求",
"CmdReviewDescription": "對檔案、diff 或 PR 進行結構化程式碼審查",
@@ -314,6 +355,7 @@
"CommandPaletteTitle": "命令",
"ComposerDispatchFailedRestored": "訊息未傳送({error});已還原到輸入框。",
"ComposerPlaceholder": "編寫任務或使用 /。",
"ComposerOperatePlaceholder": "描述目標 — Codewhale 會持續工作,直到完成",
"ComposerSlashMenuHint": " enter:執行 · tab:補全 · ↑↓:選擇 · esc:繼續輸入 ",
"ConfigActionChoose": "按 Enter 開啟選項",
"ConfigActionEdit": "按 Enter 編輯",
@@ -376,6 +418,7 @@
"ConfigLabelPermissionPosture": "新工作階段權限",
"ConfigLabelProvider": "目前提供商",
"ConfigLabelProviderUrl": "提供商 API 地址",
"ConfigHintProviderUrl": "目前的提供商端點;XiaomiToken 方案 | 按量付費 | 自訂 URL",
"ConfigLabelReasoningEffort": "推理級別",
"ConfigLabelSessionAutoResume": "自動還原上次工作階段",
"ConfigLabelSessionsRail": "工作階段欄",
@@ -578,6 +621,7 @@
"ElevationTitleSandboxDenied": " ⚠, 沙箱拒絕 ",
"EmptyStateFleetLabel": "Fleet",
"EmptyStateFleetSetupLabel": "Fleet 已就緒",
"EmptyStateHelpConnector": "或",
"EmptyStateHelpHint": "— 檢視所有功能",
"EmptyStateMcpLabel": "mcp",
"EmptyStateNoGit": "無 git",
@@ -590,6 +634,35 @@
"FleetProfileIdConflict": "設定 id `{id}` 已被 {path} 佔用;請重新起草為不同的角色或先移除舊檔案。",
"FleetProfileIdentityVerifyFailed": "無法校驗現有設定標識({error});請先修復列出的檔案再儲存。",
"FleetProfileProviderUnconfigured": "設定指定的 provider `{provider}` 尚未設定憑據({env});請先在 /provider 中設定,再儲存。",
"FleetDestStepTitle": "這個設定檔應儲存在哪裡?",
"FleetDestStepSubtitle": "在最後一步確認之前不會寫入任何內容。",
"FleetDestProjectLabel": "此專案",
"FleetDestPersonalLabel": "個人",
"FleetDestProjectSummary": "僅此專案",
"FleetDestPersonalSummary": "在所有專案中可用",
"FleetDestProjectDescription": "儲存在此專案({workspace})內。僅在此處生效,並優先於同 ID 的個人設定檔。",
"FleetDestPersonalDescription": "儲存在你的 Codewhale 主目錄。在所有專案中生效——除非某個專案有同 ID 的自有設定檔,那裡以專案設定為準。",
"FleetDestPathLine": "檔案: {path}",
"FleetDestUnavailable": "無法使用: {reason}",
"FleetDestReasonNoProjectConfig": "本次工作階段已停用專案設定檔 (--no-project-config)",
"FleetDestReasonWorkspaceMissing": "工作區資料夾 {path} 不存在或不是目錄",
"FleetDestReasonHomeUnavailable": "無法解析你的 Codewhale 主目錄 ({error})",
"FleetDestWillReplace": "將取代現有檔案 {path}",
"FleetDestOverridesProject": "此專案已有 '{id}' 設定檔,在此處以它為準;這個個人設定檔將在其他專案中生效。",
"FleetDestOverridesPersonal": "在此專案內優先於你的個人 '{id}' 設定檔。",
"FleetDestOverridesBuiltIn": "取代名冊中的 {origin} '{id}' 角色。",
"FleetSavesToChip": "儲存到: {scope} · {path}",
"FleetSavesToUndecided": "儲存到: 在第 3 步選擇——此專案 或 個人",
"FleetActionSaveProject": "儲存到此專案",
"FleetActionSavePersonal": "儲存為個人設定檔",
"FleetActionReplaceProject": "取代此專案中的檔案",
"FleetActionReplacePersonal": "取代個人設定檔",
"FleetActionConfirmReplace": "再按一次 Enter 以取代 {file}",
"FleetActionChangeDestination": "變更儲存位置",
"FleetActionBack": "返回",
"FleetReviewSavesTo": "儲存到",
"FleetModelRowBlockedNotice": "無法選擇: {reason}。請在 /provider 中設定或選擇其他列。",
"FleetDestProjectDisabledSave": "本次工作階段已停用專案設定檔 (--no-project-config);未儲存任何內容。請選擇個人,或不帶該參數重新啟動。",
"FleetReadyNotice": "Fleet 已就緒 · /fleet 開啟角色 · /fleet setup 自定義路線",
"FleetRosterHeaderLabel": "fleet",
"FleetRosterMembersCount": "{count} 個成員",
@@ -1253,6 +1326,8 @@
"SetupToolsMcpHotbarLabel": "快捷列來源:",
"SetupToolsMcpNeedsActionSaved": "工具/MCP 仍需處理;已記錄到設定報告(不阻塞首次執行)。",
"SetupToolsMcpOnRampText": "工具、MCP、技能與外掛 — 安全引導\n\n/setup 只讀取本機清單。不會啟動 MCP 伺服器、安裝技能、執行外掛或自動執行不受信任的命令。\n\n目前清單:\n- MCP{mcp_result}\n- 技能:{skills_result}\n- 工具目錄:{tools_result}\n- 外掛:{plugins_result}\n- 快捷列(共用轉接器):{hotbar_result}\n\n路徑:\n- MCP{mcp_path}\n- 技能:{skills_path}\n- 外掛:{plugins_path}\n\n安全引導(請自行在終端機或 TUI 執行):\n- MCP/mcp · codewhale mcp init · codewhale doctor\n- 技能:/skills · codewhale setup --skills\n- 外掛:/plugin · codewhale setup --plugins\n- 工具:codewhale setup --tools\n\n有副作用的操作一律需要明確確認。詳見 docs/MCP.md。",
"SetupToolsMcpDshLabel": "DeepSeek Harness (dsh)",
"SetupToolsMcpDshRow": "DeepSeek Harness (dsh) — 透過 Codewhale 連接,絕不是第二個排程器:\n- 狀態:{dsh_result}\n- 唯讀偵測;連接/計畫/啟動/移除:codewhale integrations dsh status · plan · connect · launch · remove\n- Codewhale 只寫入 $CODEWHALE_HOME/integrations/dsh,絕不複製 API 金鑰或修改 DSH 檔案。",
"SetupToolsMcpPluginsLabel": "外掛:",
"SetupToolsMcpPreviewTitle": "工具 / MCP 安全引導",
"SetupToolsMcpReviewHint": "按 Enter 記錄目前工具/MCP 事實。按 R 查看安全引導(不會自動執行)。",
@@ -1338,5 +1413,72 @@
"XaiAuthChoiceApiKeyOption": "xAI API 金鑰 — 輸入或粘貼,然後儲存到 xAI 提供商槽位",
"XaiAuthChoiceDeviceOAuthOption": "原生設備 OAuth — 通過瀏覽器/設備程式碼登入,並使用 Codewhale 自有儲存",
"XaiAuthChoiceIntro": "請選擇一個明確的憑據來源。金鑰文本絕不會被當作 OAuth 令牌。",
"XaiAuthChoiceTitle": " xAI 身份驗證 "
"XaiAuthChoiceTitle": " xAI 身份驗證 ",
"WhaleStateResting": "休息中",
"WhaleStateThinking": "思考中",
"WhaleStateWorking": "工作中",
"WhaleStateWaiting": "等待你",
"WhaleStateBlocked": "已阻塞",
"WhaleStateOffline": "離線",
"WhaleAnimalScout": "喙鯨",
"WhaleAnimalPatch": "港灣鼠海豚",
"WhaleAnimalHarbor": "座頭鯨",
"WhaleAnimalEcho": "領航鯨",
"WhaleAnimalKeel": "抹香鯨",
"WhaleAnimalLantern": "虎鯨",
"WhaleAnimalPlain": "鯨",
"WhaleJobScout": "研究",
"WhaleJobPatch": "編碼",
"WhaleJobHarbor": "協調",
"WhaleJobEcho": "溝通",
"WhaleJobKeel": "營運",
"WhaleJobLantern": "審查",
"WhaleJobPlain": "一般工作",
"SessionMetricsTurn": "輪",
"SessionMetricsTurns": "輪",
"SessionMetricsStep": "步",
"SessionMetricsSteps": "步",
"SessionMetricsLlm": "LLM",
"SessionMetricsTools": "工具呼叫",
"SessionMetricsTtft": "TTFT 平均",
"SessionMetricsTokensPerSecond": "tok/s",
"SessionMetricsCache": "快取命中",
"SessionMetricsInput": "輸入",
"SessionMetricsStatusLine": "工作階段指標:{metrics}",
"AutoReviewReceiptGuardianAllowed": "Auto-Review 已允許 '{tool}'(風險 {risk},模型守護者):{reason}",
"AutoReviewReceiptGuardianDenied": "Auto-Review 已拒絕 '{tool}'(風險 {risk},模型守護者):{reason}",
"AutoReviewReceiptGuardianUnavailable": "Auto-Review 無法審查 '{tool}'{reason});已拒絕,故障關閉",
"AutoReviewReceiptDeterministicBlocked": "Auto-Review 已封鎖 '{tool}'(確定性策略):{reason}",
"AutoReviewReceiptHeld": "Auto-Review 未暫停即擱置了 '{tool}';已拒絕(需要人工決定 — 切換到 Ask)",
"FooterHintEscInterrupt": "Esc 中斷",
"PermissionsPostureHeader": "目前權限姿態:{posture}",
"PermissionsPostureAsk": "Ask:會改變權限、成本、範圍或結果的 tool 呼叫會開啟提示;已證明安全的唯讀呼叫無需提示即可執行。上面的 ask 規則始終強制提示。",
"PermissionsPostureAuto": "Auto-Review:從不開啟提示。確定性策略允許已證明安全的呼叫,並硬性封鎖類似發佈或破壞性的背景工作;無法證明安全的呼叫交給一次性模型守護者,其給出理由後允許或拒絕(高或嚴重風險絕不自動執行;審查失敗即拒絕,故障關閉)。需要人工決定的擱置會被拒絕而非隱藏。每個此類決定都會作為備註寫入對話記錄和稽核日誌。",
"PermissionsPostureBypass": "Full Access:普通 tool 呼叫無需提示即可執行。不可繞過的安全、儲存庫規則和託管策略擱置會作為硬性封鎖故障關閉,而不是開啟提示。",
"PermissionsPostureNever": "never:僅執行被視為安全/唯讀的 tool;其他一切均在不提示的情況下被封鎖。",
"PermissionsReceiptsNote": "未經提示做出的決定(Auto-Review 守護者裁定、封鎖和擱置)會作為對話記錄備註顯示,並寫入 {audit_path} 的稽核日誌。Full Access 只能透過 Shift+Tab 或 /config 有意選擇,絕不會由規則選擇。",
"AgentFocusOpened": "已聚焦 {agent}。之後的訊息將送給此工作者;按 Esc 返回主對話。",
"AgentFocusClosed": "已返回主對話。",
"AgentFocusBanner": "{agent} · {status}",
"AgentFocusComposerChip": "→ {agent}",
"AgentFocusPlaceholder": "傳訊給 {agent} · Esc 返回主對話",
"AgentFocusNoTranscript": "{agent} 尚無對話紀錄。工作者開始交流後會顯示於此。",
"AgentFocusOmitted": "記憶體中的對話紀錄省略了較早的 {count} 則訊息。",
"AgentFocusFollowUpDelivered": "已為 {agent} 排入佇列:它會在下一輪讀取此訊息。",
"AgentFocusFollowUpQueued": "已為 {agent} 排入佇列",
"AgentFocusFollowUpContinued": "{agent} 已完成;已在新分支({target})上繼續。此檢視現在跟隨該分支。",
"AgentFocusFollowUpFailed": "無法傳送給 {agent}{reason}",
"FooterHintForAgents": "代理",
"FooterHintToManage": "管理",
"AgentRailQueuedCount": "{count} 則排隊",
"AgentFocusPosture": "{role} · {write} · {network} · {shell}",
"AgentFocusPostureWrites": "可寫入",
"AgentFocusPostureReadOnly": "唯讀",
"AgentFocusPostureNetwork": "可連網",
"AgentFocusPostureNoNetwork": "無網路",
"AgentFocusPostureShellFull": "可用 shell",
"AgentFocusPostureShellReadOnly": "唯讀 shell",
"AgentFocusPostureShellNone": "無 shell",
"GoalReceiptSet": "已設定目標:「{objective}」 · /goal 查看進度 · /goal pause 或 /goal clear 停止",
"GoalStatusIdleHint": "目前未執行 — 傳送訊息或 /goal resume 以繼續"
}
+8
View File
@@ -43,3 +43,11 @@ fn append_event(event: &str, details: Value) -> anyhow::Result<()> {
fn default_audit_path() -> anyhow::Result<PathBuf> {
Ok(codewhale_config::codewhale_home()?.join("audit.log"))
}
/// Where audit events are written, for surfaces that point a person at the
/// full record (for example `/permissions`). `None` when no Codewhale home
/// resolves; callers show a placeholder rather than guessing a path.
#[must_use]
pub fn audit_log_path() -> Option<PathBuf> {
default_audit_path().ok()
}
+364 -33
View File
@@ -1654,9 +1654,11 @@ pub async fn verify_provider_api_key(
// malformed; in that case failure-preserving catalog semantics keep the
// existing/static rows.
let body = response.text().await.unwrap_or_default();
if provider == ApiProvider::Telecomjs
&& let Ok(offerings) = telecomjs_catalog_offerings_from_body(
if matches!(provider, ApiProvider::Telecomjs | ApiProvider::Edenai)
&& let Some(kind) = provider.kind()
&& let Ok(offerings) = named_gateway_catalog_offerings_from_body(
&body,
kind,
provider.as_str(),
&base_url_fingerprint(base_url),
now_unix(),
@@ -1787,18 +1789,18 @@ impl DeepSeekClient {
request: MessageRequest,
stream: bool,
) -> Result<PreparedOutboundRequest> {
// Antigravity is credential-plane only: the agy login can be
// imported read-only with consent, but Google's cloud-code wire
// protocol is not implemented, and pretending it is OpenAI
// compatible would send credentials to a route that cannot serve
// them. Fail closed before any body is built.
if self.api_provider == crate::config::ApiProvider::Antigravity {
anyhow::bail!(
"Antigravity (agy) requests are not implemented yet: Codewhale can import the \
official CLI's login read-only (`codewhale auth external-consent`), but the \
cloud-code wire protocol is unavailable, so no request is sent. Use the \
`google` provider for Gemini models."
);
let body = cloud_code::build_generate_content_body(&request)?;
let url = cloud_code::stream_generate_content_url(&self.base_url);
return Ok(PreparedOutboundRequest::new(
WireDialect::GoogleCloudCode,
self.endpoint_identity(url, RouteShape::CloudCode),
request.model.clone(),
body,
request.reasoning_effort.clone(),
None,
CallerStreamMode::from_stream_flag(stream),
));
}
let mut request =
self.bind_request_to_protocol(self.prepare_model_bound_request(request))?;
@@ -2042,7 +2044,7 @@ impl DeepSeekClient {
let response = match prepared.dialect {
WireDialect::OpenAiResponses => self.handle_responses_message(&prepared).await?,
WireDialect::AnthropicMessages => self.handle_anthropic_message(&prepared).await?,
WireDialect::ChatCompletions => unreachable!(),
WireDialect::ChatCompletions | WireDialect::GoogleCloudCode => unreachable!(),
};
return translation_text_from_response(&response);
}
@@ -2184,7 +2186,21 @@ impl DeepSeekClient {
})
.collect()
} else if provider == "telecomjs" {
telecomjs_catalog_offerings_from_body(&body, &provider, &fingerprint, fetched_at)?
named_gateway_catalog_offerings_from_body(
&body,
codewhale_config::ProviderKind::Telecomjs,
&provider,
&fingerprint,
fetched_at,
)?
} else if provider == "edenai" {
named_gateway_catalog_offerings_from_body(
&body,
codewhale_config::ProviderKind::Edenai,
&provider,
&fingerprint,
fetched_at,
)?
} else {
let models = apply_provider_model_cutline(
self.api_provider,
@@ -2269,7 +2285,7 @@ impl DeepSeekClient {
let provider = config.api_provider();
// Only refresh for providers that serve their own model list and are
// not already covered by the Models.dev catalog.
if !matches!(provider, ApiProvider::Telecomjs) {
if !matches!(provider, ApiProvider::Telecomjs | ApiProvider::Edenai) {
return;
}
@@ -2688,6 +2704,9 @@ impl DeepSeekClient {
WireDialect::OpenAiResponses => isolated.handle_responses_message(&prepared).await,
WireDialect::AnthropicMessages => isolated.handle_anthropic_message(&prepared).await,
WireDialect::ChatCompletions => isolated.create_message_chat(&prepared, false).await,
WireDialect::GoogleCloudCode => anyhow::bail!(
"Antigravity cloud-code is stream-only; blocking create_message is not implemented"
),
}
}
}
@@ -2751,6 +2770,9 @@ impl LlmClient for DeepSeekClient {
WireDialect::OpenAiResponses => self.handle_responses_message(&prepared).await,
WireDialect::AnthropicMessages => self.handle_anthropic_message(&prepared).await,
WireDialect::ChatCompletions => self.create_message_chat(&prepared, cacheable).await,
WireDialect::GoogleCloudCode => anyhow::bail!(
"Antigravity cloud-code is stream-only; blocking create_message is not implemented"
),
}
}
@@ -2760,10 +2782,19 @@ impl LlmClient for DeepSeekClient {
) -> Result<crate::llm_client::StreamEventBox> {
let permit = self.acquire_provider_request_permit().await;
let prepared = self.prepare_outbound_request(request, true)?;
if self.api_provider == crate::config::ApiProvider::Antigravity {
return Ok(Self::hold_provider_request_permit_for_stream(
self.handle_cloud_code_stream(&prepared).await?,
permit,
));
}
let stream = match prepared.dialect {
WireDialect::OpenAiResponses => self.handle_responses_stream(&prepared).await?,
WireDialect::AnthropicMessages => self.handle_anthropic_stream(&prepared).await?,
WireDialect::ChatCompletions => self.handle_chat_completion_stream(prepared).await?,
WireDialect::GoogleCloudCode => {
unreachable!("Antigravity streams before dialect match")
}
};
Ok(Self::hold_provider_request_permit_for_stream(
stream, permit,
@@ -2894,12 +2925,13 @@ fn apply_provider_model_cutline(
models
}
/// Convert TelecomJS's bare `/models` response into truthful provider-scoped
/// Convert a named gateway's `/models` response into truthful provider-scoped
/// catalog rows. Matching model ids on other providers prove no capabilities,
/// limits, or prices; only an explicit same-provider bundled row may enrich a
/// live offering.
fn telecomjs_catalog_offerings_from_body(
fn named_gateway_catalog_offerings_from_body(
body: &str,
kind: codewhale_config::ProviderKind,
provider: &str,
fingerprint: &str,
fetched_at: u64,
@@ -2910,9 +2942,7 @@ fn telecomjs_catalog_offerings_from_body(
}
let bundled = codewhale_config::catalog::bundled_catalog_offerings();
let default_model_id = codewhale_config::ProviderKind::Telecomjs
.provider()
.default_model();
let default_model_id = kind.provider().default_model();
Ok(models
.into_iter()
.map(|model| {
@@ -3189,6 +3219,10 @@ pub(super) fn apply_reasoning_effort(
// (qwen-max, deepseek-chat, gpt-4o, claude, etc.) accepts the same
// reasoning dialect (#4188 review: verify against actual behavior).
ApiProvider::Telecomjs => {}
// Eden AI documents `thinking` only for Anthropic Claude models.
// This gateway can route unrelated model families, so the generic
// provider must not inject a model-specific reasoning dialect.
ApiProvider::Edenai => {}
// Model Studio (DashScope): its top-level controls are route- AND
// model-specific, so the provider enum alone cannot decide them —
// a custom `base_url` on the same identity is an arbitrary
@@ -3232,6 +3266,12 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama OpenAI-compat endpoint accepts think param.
body["think"] = json!(false);
}
ApiProvider::OllamaCloud => {
// Ollama Cloud stays on the documented OpenAI-compatible
// `/v1/chat/completions` wire. Native `/api/chat` uses
// `think`; this wire uses `reasoning_effort`.
body["reasoning_effort"] = json!("none");
}
ApiProvider::Anthropic
| ApiProvider::DeepseekAnthropic
| ApiProvider::MinimaxAnthropic
@@ -3273,6 +3313,7 @@ pub(super) fn apply_reasoning_effort(
// TelecomJS: see comment in the "off" branch above — the gateway's
// Chat Completions API does not support reasoning_effort or thinking.
ApiProvider::Telecomjs => {}
ApiProvider::Edenai => {}
// Model Studio: see the "off" branch — the route- and model-aware
// shaper in client::chat is the sole writer of these fields.
ApiProvider::ModelstudioTokenPlan
@@ -3336,6 +3377,14 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama think param.
body["think"] = json!(true);
}
ApiProvider::OllamaCloud => {
let value = match normalized.as_str() {
"low" | "minimal" => "low",
"medium" | "mid" => "medium",
_ => "high",
};
body["reasoning_effort"] = json!(value);
}
ApiProvider::Anthropic
| ApiProvider::DeepseekAnthropic
| ApiProvider::MinimaxAnthropic
@@ -3366,7 +3415,7 @@ pub(super) fn apply_reasoning_effort(
ApiProvider::Google => {}
ApiProvider::Antigravity => {}
},
"xhigh" | "max" | "highest" | "ultracode" => match provider {
"xhigh" | "max" | "highest" | "ultra" | "ultracode" => match provider {
// Handled by the shared DeepSeek table above, before this match.
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {}
ApiProvider::Siliconflow
@@ -3381,6 +3430,7 @@ pub(super) fn apply_reasoning_effort(
// TelecomJS: see comment in the "off" branch above — the gateway's
// Chat Completions API does not support reasoning_effort or thinking.
ApiProvider::Telecomjs => {}
ApiProvider::Edenai => {}
// Model Studio: see the "off" branch — the route- and model-aware
// shaper in client::chat is the sole writer of these fields.
ApiProvider::ModelstudioTokenPlan
@@ -3424,6 +3474,9 @@ pub(super) fn apply_reasoning_effort(
// #3024: Ollama think param.
body["think"] = json!(true);
}
ApiProvider::OllamaCloud => {
body["reasoning_effort"] = json!("max");
}
ApiProvider::Anthropic
| ApiProvider::DeepseekAnthropic
| ApiProvider::MinimaxAnthropic
@@ -3593,6 +3646,7 @@ impl DeepSeekClient {
mod anthropic;
mod chat;
pub(crate) mod cloud_code;
mod deepseek_effort;
#[cfg(test)]
mod ds4_tests;
@@ -3606,19 +3660,79 @@ fn extract_sse_data_value(line: &str) -> Option<&str> {
.map(|value| value.strip_prefix(' ').unwrap_or(value))
}
/// A complete SSE line that was not valid UTF-8. Callers must fail closed:
/// substituting U+FFFD would hide the error and can rewrite provider/model
/// text into a different string (#5374).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct InvalidSseUtf8 {
valid_up_to: usize,
error_len: Option<usize>,
}
impl std::fmt::Display for InvalidSseUtf8 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.error_len {
None => write!(
f,
"SSE line is not valid UTF-8: incomplete sequence at byte {}",
self.valid_up_to
),
Some(len) => write!(
f,
"SSE line is not valid UTF-8: invalid sequence of {len} byte(s) at byte {}",
self.valid_up_to
),
}
}
}
impl std::error::Error for InvalidSseUtf8 {}
fn decode_sse_line_bytes(bytes: &[u8]) -> Result<String, InvalidSseUtf8> {
match std::str::from_utf8(bytes) {
Ok(line) => Ok(line.trim().to_string()),
Err(err) => Err(InvalidSseUtf8 {
valid_up_to: err.valid_up_to(),
error_len: err.error_len(),
}),
}
}
/// Take the next COMPLETE line (up to the first `\n`) off a raw byte buffer,
/// draining it, and return it trimmed. Returns `None` when no full line is
/// draining it, and return it trimmed. Returns `Ok(None)` when no full line is
/// buffered yet. Decoding only complete lines (never an arbitrary network-read
/// boundary) means a multi-byte UTF-8 char — CJK, emoji, accented letter —
/// split across two reads is never corrupted to U+FFFD, since the `\n`
/// delimiter is ASCII and can never fall inside a multi-byte sequence.
fn take_sse_line(buffer: &mut Vec<u8>) -> Option<String> {
let line_end = buffer.iter().position(|&b| b == b'\n')?;
let line = String::from_utf8_lossy(&buffer[..line_end])
.trim()
.to_string();
///
/// Complete lines are decoded strictly. Invalid bytes are an error, not a
/// silent U+FFFD substitution (#5374).
fn take_sse_line(buffer: &mut Vec<u8>) -> Result<Option<String>, InvalidSseUtf8> {
let Some(line_end) = buffer.iter().position(|&b| b == b'\n') else {
return Ok(None);
};
let mut end = line_end;
if end > 0 && buffer[end - 1] == b'\r' {
end -= 1;
}
let decoded = decode_sse_line_bytes(&buffer[..end]);
buffer.drain(..=line_end);
Some(line)
decoded.map(Some)
}
/// Decode leftover bytes after the HTTP body ends (final `data:` line with no
/// trailing newline). Same strict UTF-8 contract as [`take_sse_line`].
fn flush_sse_line(buffer: &mut Vec<u8>) -> Result<Option<String>, InvalidSseUtf8> {
if buffer.is_empty() {
return Ok(None);
}
let mut end = buffer.len();
if buffer[end - 1] == b'\r' {
end -= 1;
}
let decoded = decode_sse_line_bytes(&buffer[..end]);
buffer.clear();
decoded.map(|line| (!line.is_empty()).then_some(line))
}
pub(crate) use chat::{CacheWarmupKey, PromptInspection};
@@ -3648,7 +3762,9 @@ mod tests {
tool_to_chat_for_base_url,
};
use crate::client::responses::build_responses_body;
use crate::config::{DEFAULT_TELECOMJS_MODEL, ProviderConfig, ProvidersConfig};
use crate::config::{
DEFAULT_EDENAI_MODEL, DEFAULT_TELECOMJS_MODEL, ProviderConfig, ProvidersConfig,
};
use crate::models::{
ContentBlock, ContentBlockStart, Delta, Message, MessageRequest, MessageResponse,
StreamEvent, Tool,
@@ -3886,6 +4002,29 @@ mod tests {
client
}
fn ollama_cloud_request_boundary_client(transport_base_url: String) -> DeepSeekClient {
let mut client = DeepSeekClient::new(&Config {
provider: Some("ollama-cloud".to_string()),
providers: Some(ProvidersConfig {
ollama_cloud: ProviderConfig {
api_key: Some("ollama-cloud-request-boundary-key".to_string()),
base_url: Some(crate::config::DEFAULT_OLLAMA_CLOUD_BASE_URL.to_string()),
model: Some("gpt-oss:120b".to_string()),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
})
.expect("Ollama Cloud request-boundary client");
assert_eq!(
client.base_url,
crate::config::DEFAULT_OLLAMA_CLOUD_BASE_URL
);
client.test_chat_transport_base_url = Some(transport_base_url);
client
}
async fn capture_deepseek_chat_request(
route_base_url: &str,
strict: bool,
@@ -4030,6 +4169,81 @@ mod tests {
);
}
#[tokio::test]
async fn ollama_cloud_uses_authenticated_openai_compatible_v1_wire() {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/chat/completions"))
.and(header(
"authorization",
"Bearer ollama-cloud-request-boundary-key",
))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"id": "chatcmpl-ollama-cloud-request-boundary",
"object": "chat.completion",
"model": "gpt-oss:120b",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "ok"},
"finish_reason": "stop"
}],
"usage": {
"prompt_tokens": 1,
"completion_tokens": 1,
"total_tokens": 2
}
})))
.expect(5)
.mount(&server)
.await;
let client = ollama_cloud_request_boundary_client(server.uri());
for requested in ["off", "low", "medium", "high", "max"] {
client
.create_message(MessageRequest {
model: "gpt-oss:120b".to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: "Ollama Cloud request boundary".to_string(),
cache_control: None,
}],
}],
max_tokens: 64,
system: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: Some(requested.to_string()),
stream: Some(false),
temperature: None,
top_p: None,
})
.await
.expect("Ollama Cloud request succeeds");
}
let requests = server.received_requests().await.expect("recorded request");
assert_eq!(requests.len(), 5);
for (request, expected) in requests
.iter()
.zip(["none", "low", "medium", "high", "max"])
{
let body: Value = serde_json::from_slice(&request.body).expect("captured request JSON");
assert_eq!(body["model"], "gpt-oss:120b");
assert_eq!(body["reasoning_effort"], expected);
assert!(
body.get("think").is_none(),
"native Ollama field leaked: {body}"
);
assert!(
body.get("thinking").is_none(),
"foreign field leaked: {body}"
);
}
}
// This synchronous guard deliberately spans every await: the assertions
// require exclusive access to process-global retry state for the full call.
#[allow(clippy::await_holding_lock)]
@@ -7772,6 +7986,15 @@ mod tests {
}
}
#[test]
fn reasoning_effort_edenai_does_not_guess_a_model_dialect() {
for effort in ["off", "low", "medium", "high", "max", "xhigh"] {
let mut body = json!({});
apply_reasoning_effort(&mut body, Some(effort), ApiProvider::Edenai);
assert_eq!(body, json!({}), "unexpected Eden AI fields for {effort}");
}
}
#[test]
fn moonshot_uses_codewhale_user_agent_not_kimi_cli_identity() {
let user_agent = client_user_agent(ApiProvider::Moonshot);
@@ -7792,6 +8015,25 @@ mod tests {
assert_eq!(body, json!({ "think": false }));
}
#[test]
fn reasoning_effort_ollama_cloud_uses_openai_compatible_field() {
for (effort, expected) in [
("off", "none"),
("low", "low"),
("medium", "medium"),
("high", "high"),
("max", "max"),
] {
let mut body = json!({});
apply_reasoning_effort(&mut body, Some(effort), ApiProvider::OllamaCloud);
assert_eq!(body, json!({ "reasoning_effort": expected }));
}
let mut local = json!({});
apply_reasoning_effort(&mut local, Some("high"), ApiProvider::Ollama);
assert_eq!(local, json!({ "think": true }));
}
#[test]
fn reasoning_effort_uses_nvidia_nim_chat_template_kwargs() {
let mut body = json!({});
@@ -8693,6 +8935,23 @@ mod tests {
.expect("TelecomJS client")
}
fn edenai_client_for(server: &MockServer) -> DeepSeekClient {
let _ = rustls::crypto::ring::default_provider().install_default();
DeepSeekClient::new(&Config {
provider: Some("edenai".to_string()),
providers: Some(ProvidersConfig {
edenai: ProviderConfig {
api_key: Some("test-key".to_string()),
base_url: Some(format!("{}/v3", server.uri())),
..ProviderConfig::default()
},
..ProvidersConfig::default()
}),
..Config::default()
})
.expect("Eden AI client")
}
async fn mount_models_json(server: &MockServer, status: u16, body: serde_json::Value) {
Mock::given(method("GET"))
.and(path("/v1/models"))
@@ -8871,6 +9130,46 @@ mod tests {
assert!(default.default_for_provider);
}
#[tokio::test]
async fn edenai_live_catalog_marks_the_default_and_keeps_unknowns_unclaimed() {
let server = MockServer::start().await;
Mock::given(method("GET"))
.and(path("/v3/models"))
.and(header("authorization", "Bearer test-key"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"data": [
{"id": "synthetic/vendor-model"},
{"id": DEFAULT_EDENAI_MODEL}
]
})))
.mount(&server)
.await;
let delta = edenai_client_for(&server)
.fetch_catalog_delta()
.await
.expect("Eden AI catalog delta");
assert_eq!(delta.provider, "edenai");
assert_eq!(delta.offerings.len(), 2);
let unknown = delta
.offerings
.iter()
.find(|offering| offering.wire_model_id == "synthetic/vendor-model")
.expect("synthetic Eden AI row");
assert_eq!(unknown.canonical_model, None);
assert_eq!(unknown.reasoning, None);
assert_eq!(unknown.tool_call, None);
assert!(!unknown.default_for_provider);
let default = delta
.offerings
.iter()
.find(|offering| offering.wire_model_id == DEFAULT_EDENAI_MODEL)
.expect("Eden AI default row");
assert!(default.default_for_provider);
}
#[tokio::test]
async fn fetch_catalog_delta_success_builds_scoped_secret_free_live_delta() {
let server = MockServer::start().await;
@@ -9811,10 +10110,12 @@ mod tests {
let mut buffer: Vec<u8> = Vec::new();
// First read: no complete line yet.
buffer.extend_from_slice(&bytes[..split]);
assert_eq!(take_sse_line(&mut buffer), None);
assert_eq!(take_sse_line(&mut buffer).expect("utf-8"), None);
// Second read completes the line; '好' must be intact, not U+FFFD.
buffer.extend_from_slice(&bytes[split..]);
let line = take_sse_line(&mut buffer).expect("a complete line");
let line = take_sse_line(&mut buffer)
.expect("utf-8")
.expect("a complete line");
assert_eq!(line, "data: 你好");
assert!(!line.contains('\u{FFFD}'), "multibyte char was corrupted");
assert_eq!(extract_sse_data_value(&line), Some("你好"));
@@ -9825,10 +10126,40 @@ mod tests {
#[test]
fn take_sse_line_returns_none_without_newline() {
let mut buffer = b"data: partial".to_vec();
assert_eq!(take_sse_line(&mut buffer), None);
assert_eq!(take_sse_line(&mut buffer).expect("utf-8"), None);
assert_eq!(buffer, b"data: partial");
}
#[test]
fn take_sse_line_rejects_invalid_bytes_without_replacement() {
let mut buffer = b"data: ok".to_vec();
buffer.push(0xFF);
buffer.extend_from_slice(b"\n");
let err = take_sse_line(&mut buffer).expect_err("0xFF is not UTF-8");
assert_eq!(err.error_len, Some(1));
assert!(buffer.is_empty(), "invalid line must be drained");
}
#[test]
fn flush_sse_line_preserves_unterminated_cjk() {
let mut buffer = "data: 你好".as_bytes().to_vec();
let line = flush_sse_line(&mut buffer)
.expect("utf-8")
.expect("residual line");
assert_eq!(line, "data: 你好");
assert!(!line.contains('\u{FFFD}'));
assert!(buffer.is_empty());
}
#[test]
fn flush_sse_line_rejects_truncated_multibyte_sequence() {
let mut buffer = "data: ".as_bytes().to_vec();
buffer.extend_from_slice(&"".as_bytes()[..2]);
let err = flush_sse_line(&mut buffer).expect_err("truncated UTF-8");
assert!(err.error_len.is_none());
assert!(buffer.is_empty());
}
#[test]
fn extract_sse_data_value_accepts_optional_space() {
assert_eq!(
+9 -1
View File
@@ -336,7 +336,15 @@ impl DeepSeekClient {
last_chunk_at = std::time::Instant::now();
buffer.extend_from_slice(&chunk);
while let Some(line) = super::take_sse_line(&mut buffer) {
loop {
let line = match super::take_sse_line(&mut buffer) {
Ok(Some(line)) => line,
Ok(None) => break,
Err(err) => {
yield Err(anyhow::anyhow!("{err}"));
return;
}
};
// `event:` lines are redundant (the data payload carries
// `type`) and comment/heartbeat lines are ignorable.
+48 -34
View File
@@ -172,7 +172,7 @@ fn apply_inkling_reasoning_effort(
"low" => "low",
"medium" | "mid" | "" => "medium",
"high" => "high",
"max" | "xhigh" | "highest" | "ultracode" => "max",
"max" | "xhigh" | "highest" | "ultra" | "ultracode" => "max",
_ => return,
};
body["reasoning_effort"] = json!(wire_effort);
@@ -292,7 +292,7 @@ fn apply_zai_route_reasoning_controls(
.as_deref()
{
Some("high") => body["reasoning_effort"] = json!("high"),
Some("xhigh") | Some("max") | Some("highest") | Some("ultracode") => {
Some("xhigh") | Some("max") | Some("highest") | Some("ultra") | Some("ultracode") => {
body["reasoning_effort"] = json!("max");
}
// Off, lower tiers, omitted effort, and unknown legacy values retain
@@ -331,7 +331,7 @@ fn apply_minimax_route_reasoning_controls(
body["thinking"] = json!({ "type": "disabled" });
}
Some(
"low" | "minimal" | "medium" | "mid" | "high" | "xhigh" | "max" | "highest"
"low" | "minimal" | "medium" | "mid" | "high" | "xhigh" | "max" | "highest" | "ultra"
| "ultracode" | "",
) => {
body["thinking"] = json!({ "type": "adaptive" });
@@ -530,7 +530,7 @@ fn modelstudio_reasoning_effort_for_model(effort: &str) -> Option<&'static str>
match effort.trim().to_ascii_lowercase().as_str() {
// Model Studio documents low and medium as aliases for high.
"minimal" | "low" | "medium" | "mid" | "high" | "" => Some("high"),
"xhigh" | "max" | "highest" | "ultracode" => Some("max"),
"xhigh" | "max" | "highest" | "ultra" | "ultracode" => Some("max"),
_ => None,
}
}
@@ -685,12 +685,12 @@ fn strip_google_tool_call_extra_content(messages: &mut [Value]) {
continue;
};
for call in tool_calls {
if let Some(extra) = call.get_mut("extra_content") {
if let Some(obj) = extra.as_object_mut() {
obj.remove("google");
if obj.is_empty() {
call.as_object_mut().map(|c| c.remove("extra_content"));
}
if let Some(extra) = call.get_mut("extra_content")
&& let Some(obj) = extra.as_object_mut()
{
obj.remove("google");
if obj.is_empty() {
call.as_object_mut().map(|c| c.remove("extra_content"));
}
}
}
@@ -713,7 +713,7 @@ fn mistral_model_supports_reasoning(model: &str) -> bool {
fn mistral_reasoning_effort_wire_value(effort: &str) -> Option<&'static str> {
match effort.trim().to_ascii_lowercase().as_str() {
"off" | "disabled" | "none" | "false" => Some("none"),
"high" | "xhigh" | "max" | "highest" | "ultracode" => Some("high"),
"high" | "xhigh" | "max" | "highest" | "ultra" | "ultracode" => Some("high"),
_ => None,
}
}
@@ -874,8 +874,8 @@ fn openai_compatible_reasoning_effort(
"medium" | "mid" | "" => Some("medium"),
"high" => Some("high"),
"xhigh" => Some("xhigh"),
"max" | "highest" | "ultracode" if supports_max => Some("max"),
"max" | "highest" | "ultracode" => Some("xhigh"),
"max" | "highest" | "ultra" | "ultracode" if supports_max => Some("max"),
"max" | "highest" | "ultra" | "ultracode" => Some("xhigh"),
_ => None,
}
}
@@ -1349,15 +1349,20 @@ impl DeepSeekClient {
tokio::time::sleep(Duration::from_millis(SSE_BACKPRESSURE_SLEEP_MS)).await;
}
// Process complete SSE lines from the buffer
// Process complete SSE lines from the buffer. Strict UTF-8:
// never `from_utf8_lossy` here — a mid-character TCP split
// stays in `byte_buf` until `\n`, and a genuinely invalid
// line fails closed instead of injecting U+FFFD (#5374).
let mut lines_processed = 0usize;
while let Some(newline_pos) = byte_buf.iter().position(|&b| b == b'\n') {
let mut end = newline_pos;
if end > 0 && byte_buf[end - 1] == b'\r' {
end -= 1;
}
let line = String::from_utf8_lossy(&byte_buf[..end]).into_owned();
byte_buf.drain(..newline_pos + 1);
loop {
let line = match super::take_sse_line(&mut byte_buf) {
Ok(Some(line)) => line,
Ok(None) => break,
Err(err) => {
yield Err(anyhow::anyhow!("{err}"));
break 'stream;
}
};
if line.is_empty() {
// Empty line = event boundary, process accumulated data
@@ -1426,18 +1431,20 @@ impl DeepSeekClient {
// — last tokens, finish_reason, and usage — is silently dropped.
// Skipped after `[DONE]`, whose frame was already processed.
if !saw_done {
if !byte_buf.is_empty() {
let mut end = byte_buf.len();
if end > 0 && byte_buf[end - 1] == b'\r' {
end -= 1;
let residual = match super::flush_sse_line(&mut byte_buf) {
Ok(line) => line,
Err(err) => {
yield Err(anyhow::anyhow!("{err}"));
None
}
let line = String::from_utf8_lossy(&byte_buf[..end]).into_owned();
if let Some(data) = super::extract_sse_data_value(&line) {
if !line_buf.is_empty() {
line_buf.push('\n');
}
line_buf.push_str(data);
};
if let Some(line) = residual
&& let Some(data) = super::extract_sse_data_value(&line)
{
if !line_buf.is_empty() {
line_buf.push('\n');
}
line_buf.push_str(data);
}
if !line_buf.is_empty() {
let data = std::mem::take(&mut line_buf);
@@ -1740,6 +1747,12 @@ fn push_text_part(parts: &mut Vec<Value>, text: &str) {
pub(crate) const CACHE_WARMUP_USER_TAIL: &str = "请只回复 OK";
pub(crate) const CACHE_WARMUP_MAX_TOKENS: u32 = 8;
const TOOL_RESULT_SENT_CHAR_BUDGET: usize = 12_000;
fn tool_result_sent_char_budget() -> usize {
crate::tools::large_output_router::WorkshopConfig::active_tool_result_max_bytes()
.map(|bytes| bytes.clamp(TOOL_RESULT_SENT_CHAR_BUDGET, 2 * 1024 * 1024))
.unwrap_or(TOOL_RESULT_SENT_CHAR_BUDGET)
}
const TOOL_RESULT_HEAD_CHARS: usize = 4_000;
const TOOL_RESULT_TAIL_CHARS: usize = 4_000;
/// Tool results shorter than this stay inline even when repeated. The
@@ -2257,8 +2270,8 @@ fn compact_tool_result_for_wire(
// Only medium, non-mutation results can point back to a full earlier
// message in this one request. Oversized results are already excerpts, so
// a back-reference would falsely imply the exact bytes remain available.
let dedup_eligible = (TOOL_RESULT_DEDUP_MIN_CHARS..=TOOL_RESULT_SENT_CHAR_BUDGET)
.contains(&original_chars)
let sent_budget = tool_result_sent_char_budget();
let dedup_eligible = (TOOL_RESULT_DEDUP_MIN_CHARS..=sent_budget).contains(&original_chars)
&& !is_mutation_tool(tool_name);
if dedup_eligible && let Some(previous) = seen_tool_results.get(&sha) {
@@ -2288,7 +2301,7 @@ fn compact_tool_result_for_wire(
);
}
if original_chars <= TOOL_RESULT_SENT_CHAR_BUDGET {
if original_chars <= sent_budget {
return WireToolResult {
content: content.to_string(),
original_chars,
@@ -6051,6 +6064,7 @@ mod mistral_reasoning_tests {
assert_eq!(mistral_reasoning_effort_wire_value("high"), Some("high"));
assert_eq!(mistral_reasoning_effort_wire_value("xhigh"), Some("high"));
assert_eq!(mistral_reasoning_effort_wire_value("max"), Some("high"));
assert_eq!(mistral_reasoning_effort_wire_value("ultra"), Some("high"));
assert_eq!(
mistral_reasoning_effort_wire_value("ultracode"),
Some("high")
+435
View File
@@ -0,0 +1,435 @@
//! Google Antigravity / `agy` cloud-code wire (`/v1internal`).
//!
//! This is not OpenAI-compat. The official `agy` CLI speaks
//! `POST {base}:streamGenerateContent?alt=sse` with a GenerateContent JSON
//! body. Anything we have not seen on the wire fails closed.
use anyhow::{Context, Result, bail};
use futures_util::StreamExt;
use serde_json::{Value, json};
use crate::llm_client::StreamEventBox;
use crate::models::{
ContentBlock, ContentBlockStart, Delta, MessageRequest, MessageResponse, StreamEvent,
SystemPrompt,
};
use super::PreparedOutboundRequest;
use super::stream_entry;
/// Model id advertised only after a live cloud-code turn succeeds.
#[cfg(test)]
pub const GEMINI_37_FLASH: &str = "gemini-3.7-flash";
/// Semantic request-shape failures that must remain typed until the host
/// chooses localized user-facing prose.
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub(crate) enum CloudCodeRequestError {
#[error("cloud-code request would omit non-empty system instructions")]
SystemPromptUnsupported,
}
/// Build the cloud-code streaming URL from the configured `/v1internal` base.
#[must_use]
pub fn stream_generate_content_url(base_url: &str) -> String {
let base = base_url.trim_end_matches('/');
format!("{base}:streamGenerateContent?alt=sse")
}
/// Minimum GenerateContent JSON body. Tools, images, and unknown roles fail
/// closed — those shapes are unproven on this wire.
pub fn build_generate_content_body(request: &MessageRequest) -> Result<Value> {
let has_system_text = match request.system.as_ref() {
Some(SystemPrompt::Text(text)) => !text.trim().is_empty(),
Some(SystemPrompt::Blocks(blocks)) => {
blocks.iter().any(|block| !block.text.trim().is_empty())
}
None => false,
};
if has_system_text {
return Err(CloudCodeRequestError::SystemPromptUnsupported.into());
}
if request
.tools
.as_ref()
.is_some_and(|tools| !tools.is_empty())
{
bail!(
"Antigravity cloud-code tools are not implemented yet; send a text-only turn or use the google provider"
);
}
let mut contents = Vec::new();
for message in &request.messages {
let role = match message.role.as_str() {
"user" => "user",
"assistant" | "model" => "model",
other => bail!("Antigravity cloud-code does not accept role {other:?}"),
};
let mut parts = Vec::new();
for block in &message.content {
match block {
ContentBlock::Text { text, .. } if !text.trim().is_empty() => {
parts.push(json!({ "text": text }));
}
ContentBlock::Text { .. } => {}
_ => bail!(
"Antigravity cloud-code accepts text parts only; non-text content fails closed"
),
}
}
if !parts.is_empty() {
contents.push(json!({ "role": role, "parts": parts }));
}
}
if contents.is_empty() {
bail!("Antigravity cloud-code request has no text contents");
}
let model = request.model.trim();
if model.is_empty() {
bail!("Antigravity cloud-code request is missing a model id");
}
Ok(json!({
"model": model,
"userAgent": "codewhale",
"request": {
"contents": contents,
}
}))
}
/// Pull visible text out of a cloud-code SSE JSON object. Unknown shapes
/// return `None` so the caller can fail closed instead of guessing.
pub fn extract_cloud_code_text(value: &Value) -> Option<String> {
if let Some(text) = value.pointer("/response/candidates/0/content/parts/0/text") {
return text.as_str().filter(|s| !s.is_empty()).map(str::to_string);
}
if let Some(text) = value.pointer("/candidates/0/content/parts/0/text") {
return text.as_str().filter(|s| !s.is_empty()).map(str::to_string);
}
if let Some(text) = value.get("text").and_then(Value::as_str) {
return (!text.is_empty()).then(|| text.to_string());
}
None
}
impl super::DeepSeekClient {
pub(super) async fn handle_cloud_code_stream(
&self,
prepared: &PreparedOutboundRequest,
) -> Result<StreamEventBox> {
let url = prepared.endpoint.url.clone();
let body = prepared.body.clone();
let open_req = stream_entry::StreamOpenRequest::new(
stream_entry::stream_open_timeout(),
self.stream_idle_timeout,
);
let opened = stream_entry::open_sse_response(&open_req, |policy| {
let url = url.clone();
let body = body.clone();
async move {
self.wait_for_rate_limit().await;
let client = stream_entry::client_for_policy(
&self.http_client,
self.http1_fallback_client(),
policy,
);
client
.post(&url)
.header("Accept", "text/event-stream")
.json(&body)
.send()
.await
.context("Antigravity cloud-code request failed")
}
})
.await;
let response = match opened {
Ok(response) => response,
Err(err) => {
self.mark_request_failure(&format!("cloud-code stream open: {err}"))
.await;
return Err(err);
}
};
if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
let redacted = crate::llm_client::sanitize_http_error_body(
Some("antigravity"),
status.as_u16(),
&body,
);
bail!("Antigravity cloud-code HTTP {status}: {redacted}");
}
let stream_idle_timeout = self.stream_idle_timeout;
let byte_stream = response.bytes_stream();
let stream = async_stream::stream! {
let mut buffer: Vec<u8> = Vec::new();
let stream_start = std::time::Instant::now();
let mut last_chunk_at = std::time::Instant::now();
let mut bytes_received: usize = 0;
let mut started = false;
tokio::pin!(byte_stream);
loop {
let chunk = match tokio::time::timeout(stream_idle_timeout, byte_stream.next()).await {
Ok(Some(Ok(chunk))) => chunk,
Ok(Some(Err(e))) => {
yield Err(anyhow::anyhow!("Stream read error: {e}"));
return;
}
Ok(None) => break,
Err(_) => {
yield Err(anyhow::anyhow!(stream_entry::idle_timeout_message(
stream_idle_timeout,
bytes_received,
stream_start.elapsed(),
last_chunk_at.elapsed(),
)));
return;
}
};
bytes_received += chunk.len();
last_chunk_at = std::time::Instant::now();
buffer.extend_from_slice(&chunk);
loop {
let line = match super::take_sse_line(&mut buffer) {
Ok(Some(line)) => line,
Ok(None) => break,
Err(err) => {
yield Err(anyhow::anyhow!("{err}"));
return;
}
};
if line.is_empty() || line.starts_with(':') {
continue;
}
let Some(data) = super::extract_sse_data_value(&line) else {
continue;
};
if data == "[DONE]" {
break;
}
let value: Value = match serde_json::from_str(data) {
Ok(value) => value,
Err(err) => {
yield Err(anyhow::anyhow!(
"Antigravity cloud-code SSE is not JSON: {err}"
));
return;
}
};
if let Some(error) = value.get("error") {
yield Ok(StreamEvent::Error {
error: error.clone(),
});
return;
}
let Some(text) = extract_cloud_code_text(&value) else {
if value.get("response").is_some() || value.get("candidates").is_some() {
continue;
}
yield Err(anyhow::anyhow!(
"Antigravity cloud-code SSE shape is unproven; failing closed"
));
return;
};
if !started {
started = true;
yield Ok(StreamEvent::MessageStart {
message: MessageResponse {
id: "agy".to_string(),
r#type: "message".to_string(),
role: "assistant".to_string(),
content: Vec::new(),
model: String::new(),
stop_reason: None,
stop_sequence: None,
container: None,
usage: crate::models::Usage::default(),
},
});
yield Ok(StreamEvent::ContentBlockStart {
index: 0,
content_block: ContentBlockStart::Text {
text: String::new(),
},
});
}
yield Ok(StreamEvent::ContentBlockDelta {
index: 0,
delta: Delta::TextDelta { text },
});
}
}
if started {
yield Ok(StreamEvent::ContentBlockStop { index: 0 });
yield Ok(StreamEvent::MessageStop);
} else {
yield Err(anyhow::anyhow!(
"Antigravity cloud-code stream ended without a text part"
));
}
};
Ok(Box::pin(stream))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::models::{Message, MessageRequest, SystemBlock, SystemPrompt};
fn text_request(model: &str, prompt: &str) -> MessageRequest {
MessageRequest {
model: model.to_string(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: prompt.to_string(),
cache_control: None,
}],
}],
max_tokens: 32,
system: None,
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: None,
stream: Some(true),
temperature: None,
top_p: None,
}
}
#[test]
fn stream_url_uses_v1internal_colon_rpc() {
assert_eq!(
stream_generate_content_url("https://cloudcode-pa.googleapis.com/v1internal"),
"https://cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
);
}
#[test]
fn generate_content_body_is_text_only() {
let body = build_generate_content_body(&text_request(GEMINI_37_FLASH, "ping")).unwrap();
assert_eq!(body["model"], GEMINI_37_FLASH);
assert_eq!(body["request"]["contents"][0]["parts"][0]["text"], "ping");
}
#[tokio::test]
#[ignore = "live Antigravity cloud-code; run with --ignored"]
async fn live_gemini_37_flash_one_turn() {
let mut config = crate::config::Config::load(None, None).expect("load config");
config.provider = Some("antigravity".to_string());
config.default_text_model = Some(GEMINI_37_FLASH.to_string());
eprintln!(
"agy live creds: ANTIGRAVITY_API_KEY={} AGY_ADC_AUTH={}",
if std::env::var("ANTIGRAVITY_API_KEY").is_ok_and(|v| !v.trim().is_empty()) {
"set"
} else {
"unset"
},
if std::env::var("AGY_ADC_AUTH").is_ok_and(|v| !v.trim().is_empty()) {
"set"
} else {
"unset"
}
);
let client = match crate::client::DeepSeekClient::new(&config) {
Ok(client) => client,
Err(err) => {
panic!("antigravity client did not resolve a sendable credential: {err}");
}
};
let request = text_request(GEMINI_37_FLASH, "Reply with the single word pong.");
let mut stream = crate::llm_client::LlmClient::create_message_stream(&client, request)
.await
.expect("cloud-code stream opened");
let mut text = String::new();
while let Some(event) = futures_util::StreamExt::next(&mut stream).await {
match event.expect("stream event") {
StreamEvent::ContentBlockDelta {
delta: Delta::TextDelta { text: chunk },
..
} => text.push_str(&chunk),
StreamEvent::Error { error } => {
panic!("cloud-code error object (redacted shape): {error}");
}
StreamEvent::MessageStop => break,
_ => {}
}
}
assert!(
!text.trim().is_empty(),
"live Gemini 3.7 Flash turn returned no text"
);
eprintln!(
"agy live turn ok: {} chars, first word {:?}",
text.chars().count(),
text.split_whitespace().next()
);
}
#[test]
fn generate_content_body_rejects_tools() {
let mut request = text_request(GEMINI_37_FLASH, "ping");
request.tools = Some(vec![crate::models::Tool {
tool_type: None,
name: "read".to_string(),
description: "read".to_string(),
input_schema: json!({"type": "object"}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}]);
assert!(build_generate_content_body(&request).is_err());
}
#[test]
fn generate_content_body_rejects_text_system_prompt_instead_of_dropping_it() {
let mut request = text_request(GEMINI_37_FLASH, "ping");
request.system = Some(SystemPrompt::Text("Keep this instruction".to_string()));
let error = build_generate_content_body(&request).unwrap_err();
assert!(matches!(
error.downcast_ref::<CloudCodeRequestError>(),
Some(CloudCodeRequestError::SystemPromptUnsupported)
));
}
#[test]
fn generate_content_body_rejects_block_system_prompt_instead_of_dropping_it() {
let mut request = text_request(GEMINI_37_FLASH, "ping");
request.system = Some(SystemPrompt::Blocks(vec![SystemBlock {
block_type: "text".to_string(),
text: "Keep this structured instruction".to_string(),
cache_control: None,
}]));
let error = build_generate_content_body(&request).unwrap_err();
assert!(matches!(
error.downcast_ref::<CloudCodeRequestError>(),
Some(CloudCodeRequestError::SystemPromptUnsupported)
));
}
#[test]
fn generate_content_body_accepts_semantically_empty_system_prompt() {
let mut request = text_request(GEMINI_37_FLASH, "ping");
request.system = Some(SystemPrompt::Blocks(vec![SystemBlock {
block_type: "text".to_string(),
text: " \n\t".to_string(),
cache_control: None,
}]));
assert!(build_generate_content_body(&request).is_ok());
}
}
+14
View File
@@ -95,6 +95,7 @@ pub(super) const DEEPSEEK_EFFORT_ALIASES: &[(&str, DeepseekEffortTier)] = &[
("max", DeepseekEffortTier::Max),
("maximum", DeepseekEffortTier::Max),
("highest", DeepseekEffortTier::Max),
("ultra", DeepseekEffortTier::Max),
("ultracode", DeepseekEffortTier::Max),
];
@@ -151,6 +152,19 @@ mod tests {
);
}
#[test]
fn ultra_and_legacy_ultracode_alias_resolve_to_max() {
assert_eq!(deepseek_effort_tier("ultra"), Some(DeepseekEffortTier::Max));
assert_eq!(
deepseek_effort_tier("ultracode"),
Some(DeepseekEffortTier::Max)
);
assert_eq!(
deepseek_effort_tier_or_default("ultra").responses_effort(),
"max"
);
}
#[test]
fn every_tier_has_both_wire_spellings() {
for tier in [
+10
View File
@@ -44,6 +44,8 @@ pub(crate) enum WireDialect {
AnthropicMessages,
/// OpenAI-style `POST /responses`.
OpenAiResponses,
/// Google Antigravity / `agy` cloud-code (`POST /v1internal:streamGenerateContent`).
GoogleCloudCode,
}
impl WireDialect {
@@ -61,6 +63,7 @@ impl WireDialect {
Self::ChatCompletions => "chat-completions",
Self::AnthropicMessages => "anthropic-messages",
Self::OpenAiResponses => "openai-responses",
Self::GoogleCloudCode => "google-cloud-code",
}
}
}
@@ -89,6 +92,8 @@ pub(crate) enum RouteShape {
OpencodeZen,
/// A user-configured custom/compatible endpoint on a standard dialect.
CustomCompatible,
/// Google Antigravity / `agy` `/v1internal:streamGenerateContent`.
CloudCode,
}
impl RouteShape {
@@ -101,6 +106,7 @@ impl RouteShape {
Self::CodexResponses => "codex-responses",
Self::OpencodeZen => "opencode-zen",
Self::CustomCompatible => "custom-compatible",
Self::CloudCode => "cloud-code",
}
}
}
@@ -163,6 +169,7 @@ impl ReasoningReceipt {
],
WireDialect::AnthropicMessages => &["thinking", "output_config"],
WireDialect::OpenAiResponses => &["reasoning", "include"],
WireDialect::GoogleCloudCode => &[],
}
}
@@ -527,6 +534,7 @@ impl<'a> WireBodyView<'a> {
WireDialect::ChatCompletions => (None, "messages"),
WireDialect::AnthropicMessages => (Some("system"), "messages"),
WireDialect::OpenAiResponses => (Some("instructions"), "input"),
WireDialect::GoogleCloudCode => (None, "request"),
};
// The system region is accumulated as canonical text so it can be
@@ -619,6 +627,7 @@ fn is_tool_result_item(dialect: WireDialect, item: &Value) -> bool {
WireDialect::OpenAiResponses => {
item.get("type").and_then(Value::as_str) == Some("function_call_output")
}
WireDialect::GoogleCloudCode => false,
}
}
@@ -642,6 +651,7 @@ fn count_attachments(dialect: WireDialect, item: &Value) -> (usize, usize) {
WireDialect::OpenAiResponses => {
matches!(part_type, Some("input_image" | "input_file"))
}
WireDialect::GoogleCloudCode => false,
};
if !is_attachment {
continue;
+10 -2
View File
@@ -256,7 +256,15 @@ impl DeepSeekClient {
buffer.extend_from_slice(&chunk);
// Process complete SSE lines.
while let Some(line) = super::take_sse_line(&mut buffer) {
loop {
let line = match super::take_sse_line(&mut buffer) {
Ok(Some(line)) => line,
Ok(None) => break,
Err(err) => {
yield Err(anyhow::anyhow!("{err}"));
return;
}
};
if line.is_empty() || line.starts_with(':') {
continue;
@@ -833,7 +841,7 @@ fn codex_responses_reasoning_effort(raw: &str) -> Option<&'static str> {
"minimal" => Some("low"),
"low" => Some("low"),
"high" => Some("high"),
"xhigh" | "max" | "maximum" | "ultracode" => Some("xhigh"),
"xhigh" | "max" | "maximum" | "ultra" | "ultracode" => Some("xhigh"),
_ => Some("medium"),
}
}
+1
View File
@@ -513,6 +513,7 @@ fn codex_reasoning_effort_uses_responses_labels() {
assert_eq!(codex_responses_reasoning_effort("max"), Some("xhigh"));
assert_eq!(codex_responses_reasoning_effort("maximum"), Some("xhigh"));
assert_eq!(codex_responses_reasoning_effort("xhigh"), Some("xhigh"));
assert_eq!(codex_responses_reasoning_effort("ultra"), Some("xhigh"));
assert_eq!(codex_responses_reasoning_effort("ultracode"), Some("xhigh"));
assert_eq!(codex_responses_reasoning_effort("high"), Some("high"));
assert_eq!(codex_responses_reasoning_effort("medium"), Some("medium"));
+284
View File
@@ -434,6 +434,14 @@ pub fn is_parallel_readonly_command(command: &str) -> bool {
return false;
}
readonly_tokens_admitted(trimmed)
}
/// The token-level decision shared by every machine-authority read-only
/// classifier: the charset filters above have already run for the caller's
/// posture. Keys on the arity-aware canonical form, the literal-program
/// hardener, the env-prefix rejection, and the per-command option tables.
fn readonly_tokens_admitted(trimmed: &str) -> bool {
let tokens = shell_words(trimmed);
let Some(start) = primary_token_index(&tokens) else {
return false;
@@ -486,6 +494,168 @@ pub fn is_parallel_readonly_command(command: &str) -> bool {
.any(|prefix| *prefix == canonical)
}
/// Read-only shell surface for `ShellPolicy::ReadOnly` agents (fleet scouts
/// and reviewers, #5356 follow-up): the parallel auto-approve table widened by
/// exactly the shapes real repo reconnaissance needs, still
/// mutation-proof-by-construction.
///
/// Relaxations relative to [`is_parallel_readonly_command`] (which stays
/// untouched for the parent's parallel auto-approve chunks, where its
/// tightness is load-bearing):
///
/// - pipelines `a | b`, where **every** segment must itself be an admitted
/// read-only command (an empty segment — including `||` — rejects);
/// - glob `*` arguments, expanded by the shell only against workspace paths
/// the operand gate already confines;
/// - `git -C <dir> <subcommand>` and `git --no-pager <subcommand>`, whose
/// remainder re-enters the existing per-subcommand option tables;
/// - `find` without any mutating primary (`-delete`, `-exec`, `-execdir`,
/// `-ok`, `-okdir`, `-fprintf`, `-fls`, `-fprint`, `-fprint0`);
/// - `sed -n '<range>p` — numeric line-range print only, no script verbs
/// (`w`/`r`/`e`/`s`) can appear in a two-token range script;
/// - `npm view|show|info <pkg>` — registry reads, matching the scout role's
/// network-capable read-only posture;
/// - pure text filters `sort`, `uniq`, `cut`, `tr`, `comm` as pipeline
/// stages.
///
/// Everything else keeps the parallel classifier's posture: no separators,
/// redirects, backgrounding, command/parameter expansion, subshells, or
/// env-assignment prefixes.
pub fn is_agent_readonly_shell_command(command: &str) -> bool {
let trimmed = command.trim();
if trimmed.is_empty() {
return false;
}
if trimmed.chars().any(|ch| {
matches!(
ch,
'\n' | '\r'
| ';'
| '&'
| '>'
| '<'
| '`'
| '$'
| '?'
| '['
| ']'
| '{'
| '}'
| '('
| ')'
)
}) {
return false;
}
// A pipeline is admitted only when every segment is: `a | b` is two
// read-only commands, while `a | | b`, `a |`, and `||` all carry an empty
// segment and reject. Quoted pipes inside an argument mis-split here,
// which only ever makes a segment fail classification (fail closed).
trimmed.split('|').all(is_agent_readonly_segment)
}
fn is_agent_readonly_segment(segment: &str) -> bool {
let segment = segment.trim();
if segment.is_empty() {
return false;
}
let tokens = shell_words(segment);
let Some(program) = tokens.first() else {
return false;
};
// No `env ...`/`KEY=value ...` prefix — same rule as the parallel table.
if primary_token_index(&tokens) != Some(0) || program.contains('=') {
return false;
}
match program.as_str() {
"git" => is_agent_readonly_git(&tokens),
"find" => is_agent_readonly_find(&tokens),
"sed" => is_agent_readonly_sed(&tokens),
"npm" => is_agent_readonly_npm(&tokens),
"sort" | "uniq" | "cut" | "tr" | "comm" => true,
// Everything else re-uses the parallel table verbatim (including the
// gh families and per-command option allowlists); its glob-free
// charset is enforced by the caller having already rejected every
// metacharacter this classifier permits except `|` and `*`, and the
// shared token logic re-checks the rest.
_ => readonly_tokens_admitted(segment),
}
}
fn is_agent_readonly_git(tokens: &[String]) -> bool {
// Skip the two safe global preambles; anything else before the
// subcommand (e.g. `--git-dir`, `-c`) leaves it unclassified and
// rejected, exactly like the parallel table.
let mut rest = &tokens[1..];
loop {
match rest.first().map(String::as_str) {
Some("--no-pager") => rest = &rest[1..],
Some("-C") if rest.len() >= 2 => rest = &rest[2..],
_ => break,
}
}
let Some(subcommand) = rest.first().map(String::as_str) else {
return false;
};
if !matches!(
subcommand,
"status" | "log" | "diff" | "show" | "ls-files" | "blame" | "grep"
) {
return false;
}
// Re-enter the parallel option tables with the preamble stripped so
// `git -C dir log --oneline -n 5` is judged as `git log --oneline -n 5`.
let mut reduced = vec![tokens[0].clone()];
reduced.extend(rest.iter().cloned());
readonly_tokens_admitted(&reduced.join(" "))
}
fn is_agent_readonly_find(tokens: &[String]) -> bool {
const MUTATING_PRIMARIES: &[&str] = &[
"-delete",
"-exec",
"-execdir",
"-ok",
"-okdir",
"-fprintf",
"-fls",
"-fprint",
"-fprint0",
"-truncate",
];
tokens
.iter()
.skip(1)
.all(|token| !MUTATING_PRIMARIES.contains(&token.as_str()))
}
fn is_agent_readonly_sed(tokens: &[String]) -> bool {
if tokens.len() < 3 || tokens[1] != "-n" {
return false;
}
// Numeric line-range print scripts only: `10p`, `1,5p`, `p`. Script
// verbs that write or execute (`w`, `r`, `e`, `s///w`) cannot appear in
// a two-token range script, and separators like `;` were already
// rejected at the charset gate.
let script = tokens[2].as_str();
let Some(head) = script.strip_suffix(['p', 'P']) else {
return false;
};
let numeric = |part: &str| !part.is_empty() && part.chars().all(|ch| ch.is_ascii_digit());
head.is_empty()
|| numeric(head)
|| head
.split_once(',')
.is_some_and(|(a, b)| numeric(a) && numeric(b))
}
fn is_agent_readonly_npm(tokens: &[String]) -> bool {
matches!(
tokens.get(1).map(String::as_str),
Some("view" | "show" | "info")
)
}
/// Return `true` only for the networked GitHub CLI subset admitted by
/// [`is_parallel_readonly_command`].
///
@@ -558,6 +728,13 @@ fn readonly_options_are_allowed(canonical: &str, tokens: &[&str]) -> bool {
&& (!canonical.starts_with("gh ") || !github_command_targets_unsupported_host(tokens))
}
fn is_numeric_count_shorthand(token: &str) -> bool {
let Some(digits) = token.strip_prefix('-') else {
return false;
};
!digits.is_empty() && digits.bytes().all(|byte| byte.is_ascii_digit())
}
fn options_match_allowlist(tokens: &[&str], switches: &str, values: &str) -> bool {
let mut index = 0;
let mut options = true;
@@ -568,6 +745,15 @@ fn options_match_allowlist(tokens: &[&str], switches: &str, values: &str) -> boo
} else if options && token.starts_with('-') && token != "-" {
if switches.split_ascii_whitespace().any(|name| name == token) {
// exact, no-value switch
} else if is_numeric_count_shorthand(token)
&& values
.split_ascii_whitespace()
.any(|name| name == "-n" || name == "--lines")
{
// `head -5` / `tail -20` are the ubiquitous shorthand for
// `-n 5` / `-n 20`; only commands whose value flags include a
// line-count accept them, and the digit-only form can carry no
// attached path or value injection.
} else if values.split_ascii_whitespace().any(|name| name == token) {
index += 1;
if index >= tokens.len() || tokens[index].starts_with('-') {
@@ -1322,6 +1508,104 @@ pub fn extract_primary_command(command: &str) -> Option<&str> {
mod tests {
use super::*;
#[test]
fn agent_readonly_shell_admits_real_reconnaissance_shapes() {
for command in [
"git log",
"git -C crates/tui log --oneline -n 5",
"git --no-pager log --stat",
"git -C ../sibling status --short",
"grep TODO crates/ | head -5",
"git log --oneline | head -20",
"cat Cargo.toml | wc -l",
"rg enum crates/ | sort | uniq -c | head",
"find . -name *.rs -maxdepth 3",
"find crates -type f -name *.toml | head",
"sed -n 10p Cargo.toml",
"sed -n 1,5p README.md",
"npm view codewhale version",
"sort deps.txt | uniq -c",
"ls -la *.md",
] {
assert!(
is_agent_readonly_shell_command(command),
"{command} should be agent read-only"
);
}
}
#[test]
fn agent_readonly_shell_rejects_mutation_and_injection() {
for command in [
"git log; rm -rf /",
"git log && rm -rf /",
"git log | rm -rf /",
"git log | | head",
"git log |",
"|| head",
"cat a > b",
"cat a >> b",
"echo hi < a",
"cat $(which sh)",
"cat `which sh`",
"echo ${IFS}",
"find . -delete",
"find . -exec rm {} +",
"find . -execdir sh -c true ;",
"sed -n 1,5w /tmp/out Cargo.toml",
"sed -i s/a/b/ file",
"sed -n e true Cargo.toml",
"npm install left-pad",
"npm run build",
"FOO=1 git log",
"env PAGER=cat git log",
"git --git-dir=/tmp/x.git log",
"git -c core.fsmonitor=./hook log",
"git log (modified)",
"git log | (head)",
"git push origin main",
"awk BEGIN{system(rm)} file",
"python3 -c print(1)",
] {
assert!(
!is_agent_readonly_shell_command(command),
"{command} must stay denied for agents"
);
}
}
#[test]
fn agent_readonly_pipeline_needs_every_segment_readonly() {
// The final segment is the classifier-rejected one in each pair.
assert!(!is_agent_readonly_shell_command("git log | tee out"));
assert!(!is_agent_readonly_shell_command("cat f | xargs rm"));
assert!(!is_agent_readonly_shell_command("sort f | tail -1 | sh"));
// A denied segment anywhere in the chain denies the whole pipeline.
assert!(!is_agent_readonly_shell_command(
"head f | rm -rf / | wc -l"
));
}
#[test]
fn parallel_classifier_stays_unchanged_for_parent_auto_approve() {
// The relaxations belong to the agent surface only; the parent's
// parallel auto-approve chunks keep rejecting them.
for command in [
"git log | head -5",
"grep TODO crates/ | head",
"find . -name *.rs",
"git -C crates/tui log",
"sed -n 10p Cargo.toml",
"npm view codewhale version",
] {
assert!(
!is_parallel_readonly_command(command),
"{command} must stay parallel-strict"
);
assert!(is_agent_readonly_shell_command(command));
}
}
#[test]
fn test_safe_commands() {
assert_eq!(analyze_command("ls -la").level, SafetyLevel::Safe);
@@ -73,6 +73,15 @@ pub fn config_command(app: &mut App, arg: Option<&str>) -> CommandResult {
let rest = raw_words.next().unwrap_or("").trim();
return super::permissions::permissions_command(app, Some(rest));
}
if first_word.is_some_and(|token| {
token.eq_ignore_ascii_case("workflow") || token.eq_ignore_ascii_case("goal")
}) && raw_words
.clone()
.next()
.is_none_or(|rest| rest.trim().is_empty())
{
return super::workflow_settings(app);
}
if first_word.is_some_and(|token| token.eq_ignore_ascii_case("subagents")) {
let rest = raw_words.next().unwrap_or("").trim();
return subagents_config_command(app, rest);
@@ -2023,14 +2032,14 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
"keep_footer" => app.mini_window.keep_footer = value,
_ => unreachable!("mini_window field matched above"),
}
if persist {
if let Err(err) = crate::config_persistence::persist_mini_window_bool_key(
if persist
&& let Err(err) = crate::config_persistence::persist_mini_window_bool_key(
app.config_path.as_deref(),
field,
value,
) {
return CommandResult::error(format!("Failed to persist: {err}"));
}
)
{
return CommandResult::error(format!("Failed to persist: {err}"));
}
app.needs_redraw = true;
}
@@ -2545,6 +2554,24 @@ mod tests {
create_test_app_with_config(&Config::default())
}
#[test]
fn config_workflow_and_goal_explain_the_effective_tables() {
let mut app = create_test_app();
for token in ["workflow", "goal"] {
let result = config_command(&mut app, Some(token));
assert!(
result.action.is_none(),
"{token} must not spend a model turn"
);
let text = result.message.as_deref().unwrap_or_default();
assert!(
text.contains("require_approval_for_writes"),
"{token}: {text}"
);
assert!(text.contains("max_continuations"), "{token}: {text}");
}
}
/// The shipped preset must survive its own preflight, or `/config preset
/// calm` would be refused for a reason the user cannot act on.
#[test]
@@ -180,3 +180,57 @@ pub(in crate::commands) fn dispatch(
};
Some(result)
}
/// `/workflow settings` and `/config workflow`: the effective `[workflow]`
/// and `[goal]` tables with what each value does, read from the refreshed
/// session table after a config.toml reload (no model turn). The workflow
/// tool reads the same table, so the two surfaces cannot disagree. This
/// surface explains, it does not edit.
pub(in crate::commands) fn workflow_settings(app: &App) -> CommandResult {
let refreshed = crate::tools::workflow::session_workflow_config(&app.workspace);
let cfg = refreshed.as_ref().unwrap_or(&app.workflow_config);
let on = |value: bool| if value { "on" } else { "off" };
let lines = [
"[workflow] — config.toml".to_string(),
format!(
"automatic = {} · the agent may start a workflow itself for broad or staged work; off means only /workflow starts one",
on(cfg.automatic)
),
format!(
"auto_start_read_only = {} · read-only plans start without an approval card",
on(cfg.auto_start_read_only)
),
format!(
"require_approval_for_writes = {} · plans that write, use shell/network, or elevate show an approval card first",
on(cfg.require_approval_for_writes)
),
format!(
"auto_start_child_limit = {} · larger automatic plans ask first or use /workflow",
cfg.auto_start_child_limit
),
format!(
"max_children = {} · max_concurrent = {} · max_depth = {} · hard ceilings for one run",
cfg.max_children, cfg.max_concurrent, cfg.max_depth
),
format!(
"default_token_budget = {} · shared admission hint for a run and its children",
cfg.default_token_budget
),
format!(
"max_parallel_writes_without_worktree = {} · 0 forces worktree isolation for parallel writes",
cfg.max_parallel_writes_without_worktree
),
format!(
"persist_completed_activity = {} · persist_completed_across_restarts = {} · keep finished runs visible / across restarts (journal: .codewhale/workflow-runs.jsonl)",
on(cfg.persist_completed_activity),
on(cfg.persist_completed_across_restarts)
),
String::new(),
"[goal] — config.toml".to_string(),
format!(
"max_continuations = {} · automatic continuation passes before a goal pauses; 0 = unlimited (completion, blocked, or you stop it)",
app.goal_max_continuations
),
];
CommandResult::message(lines.join("\n"))
}
@@ -95,16 +95,45 @@ fn format_snapshot(app: &App, snapshot: &PermissionsSnapshot) -> String {
if snapshot.rules().is_empty() {
output.push('\n');
output.push_str(&tr(app.ui_locale, MessageId::PermissionsNoRules));
return output;
}
for (index, rule) in snapshot.rules().iter().enumerate() {
output.push_str("\n\n");
output.push_str(&format_rule(app, index + 1, rule));
} else {
for (index, rule) in snapshot.rules().iter().enumerate() {
output.push_str("\n\n");
output.push_str(&format_rule(app, index + 1, rule));
}
}
output.push_str("\n\n");
output.push_str(&format_posture_explainer(app));
output
}
/// What the active permission posture decides on its own and what it never
/// decides, so a person can predict Auto-Review without reading the policy
/// engine. Rules above are the durable allow/ask/deny surface; the posture is
/// the session-only layer that decides everything the rules did not.
fn format_posture_explainer(app: &App) -> String {
let posture = app.approval_mode;
let mut text = tr(app.ui_locale, MessageId::PermissionsPostureHeader)
.replace("{posture}", posture.permission_chip_label());
text.push('\n');
text.push_str(&tr(
app.ui_locale,
match posture {
crate::tui::approval::ApprovalMode::Suggest => MessageId::PermissionsPostureAsk,
crate::tui::approval::ApprovalMode::Auto => MessageId::PermissionsPostureAuto,
crate::tui::approval::ApprovalMode::Bypass => MessageId::PermissionsPostureBypass,
crate::tui::approval::ApprovalMode::Never => MessageId::PermissionsPostureNever,
},
));
text.push('\n');
let audit_path = crate::audit::audit_log_path()
.map(|path| codewhale_config::quote_os_path(&path))
.unwrap_or_else(|| "$CODEWHALE_HOME/audit.log".to_string());
text.push_str(
&tr(app.ui_locale, MessageId::PermissionsReceiptsNote).replace("{audit_path}", &audit_path),
);
text
}
fn format_rule(app: &App, display_index: usize, rule: &ToolAskRule) -> String {
let scope = rule.workspace.as_deref().map_or_else(
|| tr(app.ui_locale, MessageId::PermissionsScopeGlobal).into_owned(),
@@ -84,6 +84,22 @@ fn format_status(app: &App) -> String {
)
};
push_row(&mut out, "Session cache:", &session_cache);
// The full, untrimmed session metrics strip (the footer sheds groups to
// fit; here every group that has evidence is printed).
let metrics = crate::tui::session_metrics::full_text(
crate::tui::session_metrics::snapshot_from_app(app),
app.ui_locale,
crate::tui::color_compat::ascii_safe_enabled(),
);
let _ = writeln!(
out,
" {}",
crate::localization::tr(
app.ui_locale,
crate::localization::MessageId::SessionMetricsStatusLine
)
.replace("{metrics}", &metrics)
);
push_row(
&mut out,
"Session output:",
+1 -1
View File
@@ -386,7 +386,7 @@ pub fn models(_app: &mut App) -> CommandResult {
pub fn subagents(app: &mut App) -> CommandResult {
if app.view_stack.top_kind() != Some(ModalKind::SubAgents) {
let agents = subagent_view_agents(app, &app.subagent_cache);
app.view_stack.push(SubAgentsView::new(agents));
app.view_stack.push(SubAgentsView::for_app(app, agents));
}
app.status_message = Some(tr(app.ui_locale, MessageId::SubagentsFetching).to_string());
CommandResult::action(AppAction::ListSubAgents)
+275 -55
View File
@@ -4,8 +4,9 @@
//! the model to synthesize the objective from the conversation context and
//! orchestrate it through the `workflow` tool (the same contract as goal-mode
//! `/goal`: context-dependent, no argument required). `/workflow <objective>`
//! narrows the run to an explicit objective, and `/workflow status` relays
//! typed run receipts without starting anything new.
//! narrows the run to an explicit objective. Control verbs (`status`,
//! `cancel`, `settings`, `help`) are answered by the host from the run
//! journal and live state — they never spend a model turn.
use crate::commands::traits::{CommandInfo, RegisterCommand};
use crate::localization::MessageId;
@@ -16,7 +17,7 @@ use super::CommandResult;
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "workflow",
aliases: &["workflows", "wf"],
usage: "/workflow [objective|status|cancel <run_id>]",
usage: "/workflow [objective|run <path>|status [run_id]|cancel [run_id]|settings]",
description_id: MessageId::CmdWorkflowDescription,
};
@@ -45,10 +46,11 @@ const ORCHESTRATION_CONTRACT: &str = "Author a workflow script for the `workflow
narrate phases as they complete, verify findings before reporting them as facts, \
and end with a compact receipt summary: run_id, status, and per-leaf outcomes.";
pub fn workflow(_app: &mut App, arg: Option<&str>) -> CommandResult {
pub fn workflow(app: &mut App, arg: Option<&str>) -> CommandResult {
let _app: &App = app;
let arg = arg.map(str::trim).filter(|value| !value.is_empty());
if let Some(action) = parse_workflow_control_action(arg) {
if let Some(action) = parse_workflow_control_action(_app, arg) {
return action;
}
@@ -84,47 +86,33 @@ pub fn workflow(_app: &mut App, arg: Option<&str>) -> CommandResult {
}
}
/// Route `status`/`cancel` through the `workflow` tool without starting a run.
fn parse_workflow_control_action(arg: Option<&str>) -> Option<CommandResult> {
/// Host-side `status` / `runs` / `cancel` / `settings`: read the run journal and
/// live run state directly and answer without a model turn, so a status
/// check is free and a cancel lands even while the model is busy.
fn parse_workflow_control_action(app: &App, arg: Option<&str>) -> Option<CommandResult> {
let arg = arg?;
let (verb, rest) = match arg.split_once(char::is_whitespace) {
Some((verb, rest)) => (verb, rest.trim()),
None => (arg, ""),
};
match verb {
"status" | "runs" | "list" | "inspect" => {
let target = if rest.is_empty() {
"all runs".to_string()
} else {
format!("run_id `{rest}`")
};
"status" | "runs" | "list" | "inspect" => Some(workflow_status(app, rest)),
"cancel" | "stop" | "abort" => Some(workflow_cancel(app, rest)),
"settings" | "config" => Some(super::super::config::workflow_settings(app)),
"help" | "?" => Some(CommandResult::message(WORKFLOW_USAGE)),
// `/workflow run <path>` — the form the checked-in examples document.
// The run itself needs the tool's runtime, so the model is asked to
// launch exactly this source path (no re-authoring, no new plan).
"run" if !rest.is_empty() && !rest.contains(char::is_whitespace) => {
let message = format!(
"Call the `workflow` tool with action `status`{} and summarize the receipts for \
the user: run_id, status, phase progress, per-leaf outcomes, and any errors. \
Keep it compact. Do not start a new workflow.",
if rest.is_empty() {
String::new()
} else {
format!(" and run_id `{rest}`")
}
"The user invoked /workflow run with the checked-in source path {rest:?} — this is \
authorization to launch it as-is. Call the `workflow` tool with `source_path` set \
to that path (action `run` to wait, or `start` then `status` if it is long), do not \
rewrite or replace the script, narrate phases as they complete, and end with a \
compact receipt: run_id, status, and per-leaf outcomes."
);
Some(CommandResult::with_message_and_action(
format!("Fetching workflow status for {target}..."),
AppAction::SendMessage(message),
))
}
"cancel" | "stop" | "abort" => {
if rest.is_empty() || rest.contains(char::is_whitespace) {
return Some(CommandResult::error(
"Usage: /workflow cancel <run_id>\n\nUse /workflow status to list run ids.",
));
}
let message = format!(
"Call the `workflow` tool with action `cancel` and run_id `{rest}`, then report \
the final run status to the user. Do not start a new workflow."
);
Some(CommandResult::with_message_and_action(
format!("Cancelling workflow {rest}..."),
format!("Running workflow {rest}..."),
AppAction::SendMessage(message),
))
}
@@ -132,6 +120,109 @@ fn parse_workflow_control_action(arg: Option<&str>) -> Option<CommandResult> {
}
}
const WORKFLOW_USAGE: &str =
"/workflow <objective> — orchestrate the objective with the workflow tool
/workflow orchestrate the current work
/workflow status [run_id] runs known to this workspace (no model turn)
/workflow cancel [run_id] stop a running workflow (no model turn)
/workflow settings the effective [workflow] configuration";
fn describe_run(line: &crate::tools::workflow::HostWorkflowRunLine, now_ms: u64) -> String {
let elapsed = line
.completed_at_ms
.unwrap_or(now_ms)
.saturating_sub(line.started_at_ms)
/ 1000;
let mut text = format!(
"{} {} {} {} {} children",
line.run_id,
line.status,
line.label,
crate::elapsed::format_elapsed_secs(elapsed),
line.child_count
);
if let Some(progress) = line.last_progress.as_deref() {
text.push_str(" · ");
text.push_str(progress);
}
if let Some(error) = line.error.as_deref() {
text.push_str(" · ");
text.push_str(error);
}
text
}
fn workflow_status(app: &App, run_id: &str) -> CommandResult {
let runs = crate::tools::workflow::host_workflow_runs(&app.workspace);
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or_default();
if !run_id.is_empty() {
return match runs.iter().find(|line| line.run_id == run_id) {
Some(line) => CommandResult::message(describe_run(line, now_ms)),
None => CommandResult::error(format!(
"Unknown workflow run '{run_id}'. /workflow status lists the runs this workspace knows."
)),
};
}
if runs.is_empty() {
return CommandResult::message(
"No workflow runs in this workspace yet. /workflow <objective> starts one.",
);
}
let running = runs.iter().filter(|line| line.status == "running").count();
let mut lines = vec![format!(
"{} workflow run{} · {running} running",
runs.len(),
if runs.len() == 1 { "" } else { "s" }
)];
// Newest first; the journal can hold every run the workspace ever made.
for line in runs.iter().rev().take(20) {
lines.push(describe_run(line, now_ms));
}
if runs.len() > 20 {
lines.push(format!(
"… {} older runs in .codewhale/workflow-runs.jsonl",
runs.len() - 20
));
}
CommandResult::message(lines.join("\n"))
}
fn workflow_cancel(app: &App, run_id: &str) -> CommandResult {
if run_id.contains(char::is_whitespace) {
return CommandResult::error("Usage: /workflow cancel [run_id]");
}
let target = if run_id.is_empty() {
let running: Vec<_> = crate::tools::workflow::host_workflow_runs(&app.workspace)
.into_iter()
.filter(|line| line.status == "running")
.collect();
match running.as_slice() {
[] => return CommandResult::message("No workflow is running."),
[only] => only.run_id.clone(),
many => {
let ids: Vec<&str> = many.iter().map(|line| line.run_id.as_str()).collect();
return CommandResult::error(format!(
"{} workflows are running; name one: {}",
many.len(),
ids.join(", ")
));
}
}
} else {
run_id.to_string()
};
match crate::tools::workflow::host_cancel_workflow(&app.workspace, &target) {
Ok(line) => CommandResult::message(format!(
"Workflow {} {} · {}",
line.run_id, line.status, line.label
)),
Err(reason) => CommandResult::error(reason),
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -177,29 +268,158 @@ mod tests {
}
#[test]
fn workflow_status_and_cancel_route_to_tool_without_new_runs() {
fn workflow_status_and_cancel_answer_from_the_host_without_a_model_turn() {
let dir = tempfile::tempdir().expect("tempdir");
let mut app = test_app();
app.workspace = dir.path().to_path_buf();
// Nothing has run in this workspace: status is a plain answer, and it
// must not create the run journal just to say so.
let result = workflow(&mut app, Some("status"));
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("action `status`"));
assert!(message.contains("Do not start a new workflow"));
assert!(!result.is_error);
assert!(
result.action.is_none(),
"status must not send a model message"
);
assert!(
result
.message
.as_deref()
.unwrap()
.contains("No workflow runs")
);
assert!(!dir.path().join(".codewhale/workflow-runs.jsonl").exists());
let result = workflow(&mut app, Some("status wf_run_1"));
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("run_id `wf_run_1`"));
let result = workflow(&mut app, Some("status wf_missing"));
assert!(result.is_error);
assert!(result.action.is_none());
let result = workflow(&mut app, Some("cancel wf_run_1"));
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("action `cancel`"));
assert!(message.contains("run_id `wf_run_1`"));
// A seeded run is listed and described from host state.
crate::tools::workflow::structcopy_test_seed_run(dir.path(), "workflow_seed");
let result = workflow(&mut app, Some("runs"));
let text = result.message.unwrap();
assert!(text.contains("workflow_seed"), "{text}");
assert!(text.contains("running"), "{text}");
assert!(result.action.is_none());
// Cancel with one running run needs no id and never asks the model.
// The seeded record has no live controller (no VM ran); cancel still
// marks the journal cancelled with an honest nothing-live receipt.
let result = workflow(&mut app, Some("cancel"));
assert!(result.is_error, "cancel without a run id is a usage error");
assert!(result.action.is_none());
assert!(!result.is_error, "{:?}", result.message);
let text = result.message.as_deref().unwrap();
assert!(text.contains("workflow_seed"), "{text}");
assert!(text.contains("cancelled"), "{text}");
let after = crate::tools::workflow::host_workflow_runs(&app.workspace);
assert_eq!(
after
.iter()
.find(|line| line.run_id == "workflow_seed")
.map(|line| line.status),
Some("cancelled")
);
let result = workflow(&mut app, Some("cancel with spaces"));
assert!(result.is_error);
let result = workflow(&mut app, Some("help"));
assert!(result.message.unwrap().contains("/workflow status"));
// `/workflow run <path>` launches exactly that source through the tool.
let result = workflow(&mut app, Some("run workflows/tiny.workflow.js"));
let Some(AppAction::SendMessage(message)) = result.action else {
panic!("expected SendMessage action");
};
assert!(message.contains("`source_path`"), "{message}");
assert!(message.contains("workflows/tiny.workflow.js"), "{message}");
assert!(message.contains("do not"), "{message}");
}
#[test]
fn workflow_settings_explains_the_session_table() {
let mut app = test_app();
app.workflow_config.automatic = false;
app.workflow_config.require_approval_for_writes = false;
app.goal_max_continuations = 25;
let result = workflow(&mut app, Some("settings"));
assert!(result.action.is_none());
let text = result.message.unwrap();
assert!(text.contains("automatic = off"), "{text}");
assert!(text.contains("require_approval_for_writes = off"), "{text}");
assert!(text.contains("max_continuations = 25"), "{text}");
}
#[test]
fn workflow_settings_and_tool_share_a_refreshed_session_table() {
use crate::tools::spec::{ApprovalRequirement, ToolContext, ToolSpec};
use crate::tools::subagent::{SubAgentRuntime, new_shared_subagent_manager};
use crate::tools::workflow::WorkflowTool;
use serde_json::json;
let dir = tempfile::tempdir().expect("tempdir");
let mut app = test_app();
app.workspace = dir.path().to_path_buf();
let mut table = app.workflow_config.clone();
table.automatic = false;
table.require_approval_for_writes = false;
table.auto_start_read_only = false;
crate::tools::workflow::set_session_workflow_config(&app.workspace, table.clone());
app.workflow_config = table;
let result = workflow(&mut app, Some("settings"));
assert!(result.action.is_none());
let text = result.message.unwrap();
assert!(text.contains("automatic = off"), "{text}");
assert!(text.contains("require_approval_for_writes = off"), "{text}");
assert!(text.contains("auto_start_read_only = off"), "{text}");
let ctx = ToolContext::new(dir.path().to_path_buf());
let manager = new_shared_subagent_manager(dir.path().to_path_buf(), 2);
let _ = rustls::crypto::ring::default_provider().install_default();
let client = crate::client::DeepSeekClient::new(&crate::config::Config {
api_key: Some("test-key".to_string()),
..crate::config::Config::default()
})
.expect("stub client");
let mut runtime = SubAgentRuntime::new(
client,
"deepseek-v4-flash".to_string(),
ctx,
true,
None,
manager.clone(),
);
// Stale snapshot: product defaults still require write approval.
runtime.api_config = Some(std::sync::Arc::new(crate::config::Config::default()));
let tool = WorkflowTool::new(manager, runtime);
let write_plan = json!({
"action": "start",
"plan": {
"goal": "write freely",
"risk": "writes",
"children": [{ "prompt": "edit", "type": "implementer" }]
}
});
let read_only = json!({
"action": "start",
"plan": {
"goal": "scout crates",
"risk": "read_only",
"children": [{ "prompt": "look", "type": "explore" }]
}
});
assert_eq!(
tool.approval_requirement_for(&write_plan),
ApprovalRequirement::Auto,
"refreshed require_approval_for_writes = false must win over the stale runtime snapshot"
);
assert_eq!(
tool.approval_requirement_for(&read_only),
ApprovalRequirement::Required,
"refreshed auto_start_read_only = false must still ask"
);
}
}
+44 -4
View File
@@ -301,17 +301,45 @@ fn format_cache_stats(app: &App) -> String {
let changes = app.prefix_change_count;
let stable_checks = checks.saturating_sub(changes);
let drift = app.prefix_drift_count;
if changes == 0 {
out.push_str(&format!(
" Stability: {pct}% ({stable_checks}/{checks} checks)\n"
));
out.push_str(" Status: stable (no prefix changes this session)\n");
if app.prefix_context_updates > 0 {
out.push_str(&format!(
" Context updates: {} (workspace drift delivered as history, header unchanged)\n",
app.prefix_context_updates
));
}
} else {
out.push_str(&format!(
" Stability: {pct}% ({stable_checks}/{checks} checks, {changes} change{})\n",
if changes == 1 { "" } else { "s" }
));
out.push_str(" Status: WARNING — prefix has changed\n");
if drift == 0 {
out.push_str(
" Status: stable (all changes were declared header changes)\n",
);
} else {
out.push_str(&format!(
" Status: WARNING — {drift} undeclared drift{}\n",
if drift == 1 { "" } else { "s" }
));
}
if let Some(ref reason) = app.prefix_pin_reason {
out.push_str(&format!(" Pin reason: {reason}\n"));
}
if app.prefix_context_updates > 0 {
out.push_str(&format!(
" Context updates: {} (workspace drift delivered as history, header unchanged)\n",
app.prefix_context_updates
));
}
if let Some(ref reason) = app.prefix_last_miss_reason {
out.push_str(&format!(" Last miss: {reason}\n"));
}
if let Some(ref desc) = app.last_prefix_change_desc {
out.push_str(&format!(" Last change: {desc}\n"));
}
@@ -330,8 +358,20 @@ fn format_cache_stats(app: &App) -> String {
out.push_str(&format!(" Pinned hash: {hash}\n"));
let short = if hash.len() >= 12 { &hash[..12] } else { hash };
out.push_str(&format!(" Short id: {short}\n"));
if app.prefix_change_count > 0 {
out.push_str(" Drift: WARNING — hash has changed during this session\n");
if app.prefix_drift_count > 0 {
out.push_str(" Drift: WARNING — undeclared hash change this session\n");
out.push_str(&format!(
" ({change} change{plural} detected, {drift} undeclared)\n",
change = app.prefix_change_count,
plural = if app.prefix_change_count == 1 {
""
} else {
"s"
},
drift = app.prefix_drift_count,
));
} else if app.prefix_change_count > 0 {
out.push_str(" Drift: none (all changes were declared)\n");
out.push_str(&format!(
" ({change} change{plural} detected)\n",
change = app.prefix_change_count,
@@ -339,7 +379,7 @@ fn format_cache_stats(app: &App) -> String {
""
} else {
"s"
}
},
));
} else {
out.push_str(" Drift: none (hash stable)\n");
+32 -3
View File
@@ -1748,8 +1748,12 @@ fn cache_stats_warns_on_prefix_change() {
app.prefix_stability_pct = Some(67);
app.prefix_checks_total = 3;
app.prefix_change_count = 1;
// The one change was an undeclared drift — the case worth warning about.
app.prefix_drift_count = 1;
app.prefix_pin_reason = Some("initial".to_string());
app.prefix_last_miss_reason = Some("drift:sys".to_string());
app.last_prefix_change_desc =
Some("prefix cache invalidated: system prompt changed".to_string());
Some("drift — prefix cache invalidated: system prompt changed".to_string());
app.last_pinned_prefix_hash =
Some("deadbeef0000deadbeef0000deadbeef0000deadbeef0000deadbeef0000deadbeef".to_string());
@@ -1757,12 +1761,37 @@ fn cache_stats_warns_on_prefix_change() {
let msg = result.message.expect("cache stats produces a message");
assert!(msg.contains("Stability: 67%"), "got: {msg}");
assert!(msg.contains("WARNING — prefix has changed"), "got: {msg}");
assert!(msg.contains("WARNING — 1 undeclared drift"), "got: {msg}");
assert!(msg.contains("Last miss: drift:sys"), "got: {msg}");
assert!(msg.contains("system prompt changed"), "got: {msg}");
assert!(msg.contains("Drift: WARNING"), "got: {msg}");
assert!(msg.contains("1 change detected"), "got: {msg}");
}
#[test]
fn cache_stats_does_not_warn_on_declared_header_change() {
let mut app = create_test_app();
app.prefix_stability_pct = Some(67);
app.prefix_checks_total = 3;
app.prefix_change_count = 1;
// A declared header change (e.g. /model): expected, not drift.
app.prefix_drift_count = 0;
app.prefix_pin_reason = Some("change:model".to_string());
app.prefix_last_miss_reason = Some("change:model".to_string());
app.last_prefix_change_desc = Some("change:model — tool set changed".to_string());
app.last_pinned_prefix_hash =
Some("deadbeef0000deadbeef0000deadbeef0000deadbeef0000deadbeef0000deadbeef".to_string());
let result = cache(&mut app, Some("stats"));
let msg = result.message.expect("cache stats produces a message");
assert!(
msg.contains("stable (all changes were declared header changes)"),
"got: {msg}"
);
assert!(!msg.contains("WARNING — "), "got: {msg}");
assert!(msg.contains("Pin reason: change:model"), "got: {msg}");
}
#[test]
fn cache_stats_shows_cache_hit_summary() {
let mut app = create_test_app();
@@ -0,0 +1,386 @@
//! Explicit local import bridge for Kimi-managed plugins.
//!
//! Listing is read-only and only considers immediate, canonical child
//! directories of `~/.kimi-code/plugins/managed`. Import requires the exact
//! content hash shown by listing, then routes the local directory through the
//! ordinary reviewed installer. The resulting Codewhale plugin still starts
//! disabled and untrusted; this module never launches or probes an external
//! Kimi application, daemon, MCP binary, or permission grant.
use std::fmt::Write as _;
use std::fs;
use std::path::{Path, PathBuf};
use super::render::{escape_review_path, escape_review_text};
use crate::commands::CommandResult;
use crate::localization::{Locale, MessageId, tr};
use crate::plugins::agent_plugin::KIMI_PLUGIN_JSON_NAME;
use crate::plugins::manifest::PluginManifest;
use crate::plugins::metadata_is_link_or_reparse;
use crate::tui::app::App;
const MAX_MANAGED_CHILDREN: usize = 128;
const LIST_COMMAND: &str = "/plugin import kimi [list]";
const APPROVE_COMMAND: &str = "/plugin import kimi approve <name> <content-hash>";
#[derive(Debug)]
struct Candidate {
name: String,
version: String,
license: Option<String>,
canonical_path: PathBuf,
content_hash: String,
capability_hash: String,
inventory: String,
applicable: bool,
}
#[derive(Debug)]
struct Scan {
root: PathBuf,
candidates: Vec<Candidate>,
rejected: Vec<String>,
}
fn message(locale: Locale, id: MessageId, replacements: &[(&str, &str)]) -> String {
let mut rendered = tr(locale, id).into_owned();
for (placeholder, value) in replacements {
rendered = rendered.replace(placeholder, value);
}
rendered
}
pub(super) fn usage(locale: Locale) -> String {
message(
locale,
MessageId::PluginKimiUsage,
&[
("{list_command}", LIST_COMMAND),
("{approve_command}", APPROVE_COMMAND),
],
)
}
pub(super) fn dispatch(
app: &mut App,
words: &[&str],
home_override: Option<&Path>,
) -> CommandResult {
match words {
[] | ["list"] => list(app.ui_locale, home_override),
["approve", name, content_hash] => approve(app, name, content_hash, home_override),
_ => CommandResult::error(usage(app.ui_locale)),
}
}
fn list(locale: Locale, home_override: Option<&Path>) -> CommandResult {
let scan = match scan_managed_plugins(locale, home_override) {
Ok(scan) => scan,
Err(error) => return CommandResult::error(error),
};
let root = escape_review_path(&scan.root);
let mut output = message(
locale,
MessageId::PluginKimiManagedRootHeading,
&[("{root}", &root)],
);
output.push('\n');
if scan.candidates.is_empty() {
output.push_str(" ");
output.push_str(&tr(locale, MessageId::PluginKimiNoneFound));
output.push('\n');
}
for candidate in &scan.candidates {
let name = escape_review_text(&candidate.name);
let version = escape_review_text(&candidate.version);
let license = candidate
.license
.as_deref()
.map(escape_review_text)
.unwrap_or_else(|| tr(locale, MessageId::PluginKimiLicenseUnspecified).into_owned());
let applicability = tr(
locale,
if candidate.applicable {
MessageId::PluginKimiApplicable
} else {
MessageId::PluginKimiNotApplicable
},
);
let inventory = escape_review_text(&candidate.inventory);
let summary = message(
locale,
MessageId::PluginKimiCandidateSummary,
&[
("{name}", &name),
("{version}", &version),
("{license}", &license),
("{applicability}", &applicability),
("{inventory}", &inventory),
],
);
let _ = writeln!(output, "\n{summary}");
let path = escape_review_path(&candidate.canonical_path);
let approve_command = format!(
"/plugin import kimi approve {} {}",
candidate.name, candidate.content_hash
);
let details = message(
locale,
MessageId::PluginKimiCandidateDetails,
&[
("{path}", &path),
("{content_hash}", &candidate.content_hash),
("{capability_hash}", &candidate.capability_hash),
("{approve_command}", &approve_command),
],
);
let _ = writeln!(output, "{details}");
}
if !scan.rejected.is_empty() {
output.push('\n');
output.push_str(&tr(locale, MessageId::PluginKimiRejectedHeading));
output.push('\n');
for rejection in &scan.rejected {
let _ = writeln!(output, " - {rejection}");
}
}
output.push('\n');
output.push_str(&tr(locale, MessageId::PluginKimiInspectionFooter));
CommandResult::message(output)
}
fn approve(
app: &mut App,
name: &str,
expected_hash: &str,
home_override: Option<&Path>,
) -> CommandResult {
let scan = match scan_managed_plugins(app.ui_locale, home_override) {
Ok(scan) => scan,
Err(error) => return CommandResult::error(error),
};
let Some(candidate) = scan
.candidates
.into_iter()
.find(|candidate| candidate.name == name)
else {
let name = escape_review_text(name);
return CommandResult::error(message(
app.ui_locale,
MessageId::PluginKimiCandidateMissing,
&[("{name}", &name), ("{list_command}", "/plugin import kimi")],
));
};
if candidate.content_hash != expected_hash {
let name = escape_review_text(name);
let expected = escape_review_text(expected_hash);
return CommandResult::error(message(
app.ui_locale,
MessageId::PluginKimiCandidateChanged,
&[
("{name}", &name),
("{expected}", &expected),
("{actual}", &candidate.content_hash),
("{list_command}", "/plugin import kimi"),
],
));
}
// `install_bundle` revalidates and copies the source through the ordinary
// local installer. Its result is always rediscovered disabled/untrusted
// and presents the post-copy authority review before any activation.
super::install_bundle_with_expected_hash(app, &candidate.canonical_path, expected_hash)
}
fn scan_managed_plugins(locale: Locale, home_override: Option<&Path>) -> Result<Scan, String> {
let home = match home_override {
Some(home) => home.to_path_buf(),
None => crate::config::effective_home_dir()
.ok_or_else(|| tr(locale, MessageId::PluginKimiHomeMissing).into_owned())?,
};
let configured_root = home.join(".kimi-code/plugins/managed");
let metadata = match fs::symlink_metadata(&configured_root) {
Ok(metadata) => metadata,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {
return Ok(Scan {
root: configured_root,
candidates: Vec::new(),
rejected: Vec::new(),
});
}
Err(error) => {
let root = escape_review_path(&configured_root);
let error = escape_review_text(&error.to_string());
return Err(message(
locale,
MessageId::PluginKimiRootInspectFailed,
&[("{root}", &root), ("{error}", &error)],
));
}
};
if metadata_is_link_or_reparse(&metadata) || !metadata.is_dir() {
let root = escape_review_path(&configured_root);
return Err(message(
locale,
MessageId::PluginKimiRootMustBeDirectory,
&[("{root}", &root)],
));
}
let canonical_root = configured_root.canonicalize().map_err(|error| {
let root = escape_review_path(&configured_root);
let error = escape_review_text(&error.to_string());
message(
locale,
MessageId::PluginKimiRootCanonicalizeFailed,
&[("{root}", &root), ("{error}", &error)],
)
})?;
let mut entries = fs::read_dir(&canonical_root)
.map_err(|error| {
let root = escape_review_path(&canonical_root);
let error = escape_review_text(&error.to_string());
message(
locale,
MessageId::PluginKimiRootListFailed,
&[("{root}", &root), ("{error}", &error)],
)
})?
.collect::<Result<Vec<_>, _>>()
.map_err(|error| {
let error = escape_review_text(&error.to_string());
message(
locale,
MessageId::PluginKimiEntryReadFailed,
&[("{error}", &error)],
)
})?;
if entries.len() > MAX_MANAGED_CHILDREN {
let count = entries.len().to_string();
let max = MAX_MANAGED_CHILDREN.to_string();
return Err(message(
locale,
MessageId::PluginKimiEntryLimit,
&[("{count}", &count), ("{max}", &max)],
));
}
entries.sort_by_key(fs::DirEntry::file_name);
let mut candidates = Vec::new();
let mut rejected = Vec::new();
for entry in entries {
let path = entry.path();
let metadata = match fs::symlink_metadata(&path) {
Ok(metadata) => metadata,
Err(error) => {
let path = escape_review_path(&path);
let error = escape_review_text(&error.to_string());
rejected.push(message(
locale,
MessageId::PluginKimiEntryInspectFailed,
&[("{path}", &path), ("{error}", &error)],
));
continue;
}
};
if metadata_is_link_or_reparse(&metadata) {
let path = escape_review_path(&path);
rejected.push(message(
locale,
MessageId::PluginKimiEntryLinksRefused,
&[("{path}", &path)],
));
continue;
}
if !metadata.is_dir() {
continue;
}
let canonical_path = match path.canonicalize() {
Ok(path) if path.parent() == Some(canonical_root.as_path()) => path,
Ok(canonical_path) => {
let path = escape_review_path(&entry.path());
let canonical_path = escape_review_path(&canonical_path);
rejected.push(message(
locale,
MessageId::PluginKimiEntryOutsideRoot,
&[("{path}", &path), ("{canonical_path}", &canonical_path)],
));
continue;
}
Err(error) => {
let path = escape_review_path(&path);
let error = escape_review_text(&error.to_string());
rejected.push(message(
locale,
MessageId::PluginKimiEntryCanonicalizeFailed,
&[("{path}", &path), ("{error}", &error)],
));
continue;
}
};
match inspect_candidate(locale, &canonical_path) {
Ok(candidate) => candidates.push(candidate),
Err(error) => rejected.push(error),
}
}
candidates.sort_by(|left, right| left.name.cmp(&right.name));
Ok(Scan {
root: canonical_root,
candidates,
rejected,
})
}
fn inspect_candidate(locale: Locale, canonical_path: &Path) -> Result<Candidate, String> {
let manifest_path = canonical_path.join(KIMI_PLUGIN_JSON_NAME);
let metadata = fs::symlink_metadata(&manifest_path).map_err(|error| {
let path = escape_review_path(canonical_path);
let error = escape_review_text(&error.to_string());
message(
locale,
MessageId::PluginKimiManifestUnreadable,
&[
("{path}", &path),
("{manifest}", KIMI_PLUGIN_JSON_NAME),
("{error}", &error),
],
)
})?;
if metadata_is_link_or_reparse(&metadata) || !metadata.is_file() {
let path = escape_review_path(canonical_path);
return Err(message(
locale,
MessageId::PluginKimiManifestMustBeFile,
&[("{path}", &path), ("{manifest}", KIMI_PLUGIN_JSON_NAME)],
));
}
let validated = PluginManifest::validate_from_path(&manifest_path).map_err(|error| {
let path = escape_review_path(canonical_path);
let error = escape_review_text(&error.to_string());
message(
locale,
MessageId::PluginKimiManifestInvalid,
&[("{path}", &path), ("{error}", &error)],
)
})?;
let name = validated.manifest.plugin.name.clone();
if canonical_path.file_name().and_then(|part| part.to_str()) != Some(name.as_str()) {
let path = escape_review_path(canonical_path);
let escaped_name = escape_review_text(&name);
return Err(message(
locale,
MessageId::PluginKimiDirectoryNameMismatch,
&[("{path}", &path), ("{name}", &escaped_name)],
));
}
Ok(Candidate {
name,
version: validated.manifest.plugin.version.clone(),
license: validated.manifest.plugin.license.clone(),
canonical_path: validated.canonical_root,
content_hash: validated.content_hash,
capability_hash: validated.capability_hash,
inventory: validated.inventory.summary(),
applicable: validated.applicable,
})
}
@@ -17,7 +17,11 @@ use std::path::{Path, PathBuf};
use super::render::{escape_review_path, escape_review_text};
use crate::commands::CommandResult;
use crate::localization::{Locale, MessageId, tr};
use crate::plugins::marketplace::parsers::MarketplaceDocument;
use crate::plugins::marketplace::parsers::kimi::{
KIMI_GZIP_TARBALL_SOURCE_KIND, KIMI_REMOTE_UNSUPPORTED_REASON, KIMI_ZIP_UNSUPPORTED_REASON,
};
use crate::plugins::marketplace::store::{MarketplaceStore, StoredMarketplaceCatalog};
use crate::plugins::marketplace::types::{
MarketplaceCatalog, MarketplaceFormat, MarketplaceInstallPlan, MarketplaceSourceSpec,
@@ -162,7 +166,7 @@ fn list(app: &mut App) -> CommandResult {
for (name, entry) in state.catalogs() {
output.push('\n');
output.push_str(&render_catalog_summary(name, &entry.catalog));
output.push_str(&render_candidates(&entry.catalog, false));
output.push_str(&render_candidates(app.ui_locale, &entry.catalog, false));
}
output.push_str(
"\nTiers and provenance are display-only. Install with /plugin marketplace install <catalog> <candidate>; \
@@ -197,7 +201,7 @@ fn show(app: &mut App, name: &str) -> CommandResult {
"{}",
escape_review_path(Path::new(&entry.source_path))
);
output.push_str(&render_candidates(&entry.catalog, true));
output.push_str(&render_candidates(app.ui_locale, &entry.catalog, true));
CommandResult::message(output)
}
@@ -259,7 +263,7 @@ fn install(app: &mut App, catalog_name: &str, candidate_name: &str) -> CommandRe
return CommandResult::error(format!(
"Candidate `{}` cannot be installed by Codewhale: {}",
escape_review_text(candidate_name),
escape_review_text(reason)
escape_review_text(&localized_marketplace_plan_text(app.ui_locale, reason))
));
};
let spec = spec.as_str();
@@ -270,12 +274,11 @@ fn install(app: &mut App, catalog_name: &str, candidate_name: &str) -> CommandRe
}
fn resolve_spec(source_path: &str, source: &MarketplaceSourceSpec, spec: &str) -> String {
if let MarketplaceSourceSpec::LocalPath { path } = source {
if path.is_relative() {
if let Some(dir) = Path::new(source_path).parent() {
return format!("path:{}", dir.join(path).display());
}
}
if let MarketplaceSourceSpec::LocalPath { path } = source
&& path.is_relative()
&& let Some(dir) = Path::new(source_path).parent()
{
return format!("path:{}", dir.join(path).display());
}
spec.to_string()
}
@@ -352,7 +355,18 @@ fn render_catalog_summary(name: &str, catalog: &MarketplaceCatalog) -> String {
out
}
fn render_candidates(catalog: &MarketplaceCatalog, detailed: bool) -> String {
fn localized_marketplace_plan_text(locale: Locale, value: &str) -> std::borrow::Cow<'_, str> {
match value {
KIMI_ZIP_UNSUPPORTED_REASON => tr(locale, MessageId::PluginKimiMarketplaceZipUnsupported),
KIMI_REMOTE_UNSUPPORTED_REASON => {
tr(locale, MessageId::PluginKimiMarketplaceRemoteUnsupported)
}
KIMI_GZIP_TARBALL_SOURCE_KIND => tr(locale, MessageId::PluginKimiMarketplaceGzipTarball),
_ => std::borrow::Cow::Borrowed(value),
}
}
fn render_candidates(locale: Locale, catalog: &MarketplaceCatalog, detailed: bool) -> String {
let mut out = String::new();
for candidate in &catalog.candidates {
let status = if candidate.has_errors() {
@@ -385,6 +399,7 @@ fn render_candidates(catalog: &MarketplaceCatalog, detailed: bool) -> String {
let _ = writeln!(out, " compatibility: {compatibility}");
match &candidate.install_plan {
MarketplaceInstallPlan::Supported { source_kind, .. } => {
let source_kind = localized_marketplace_plan_text(locale, source_kind);
let _ = writeln!(
out,
" installable via {source_kind}: /plugin marketplace install {} {}",
@@ -393,7 +408,8 @@ fn render_candidates(catalog: &MarketplaceCatalog, detailed: bool) -> String {
);
}
MarketplaceInstallPlan::Unsupported { reason, .. } => {
let _ = writeln!(out, " not installable: {}", escape_review_text(reason));
let reason = localized_marketplace_plan_text(locale, reason);
let _ = writeln!(out, " not installable: {}", escape_review_text(&reason));
}
}
if detailed {
@@ -457,3 +473,18 @@ fn render_diagnostics_inline(
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod localized_plan_tests {
use super::*;
#[test]
fn kimi_plan_codes_resolve_at_render_time() {
let zip = localized_marketplace_plan_text(Locale::Es419, KIMI_ZIP_UNSUPPORTED_REASON);
let remote = localized_marketplace_plan_text(Locale::Es419, KIMI_REMOTE_UNSUPPORTED_REASON);
let gzip = localized_marketplace_plan_text(Locale::Es419, KIMI_GZIP_TARBALL_SOURCE_KIND);
assert!(zip.contains("no admite paquetes ZIP"), "{zip}");
assert!(remote.contains("deben terminar en .tar.gz"), "{remote}");
assert_eq!(gzip, "URL de tarball gzip");
}
}
@@ -51,7 +51,7 @@ fn write_kimi_catalog(dir: &Path) -> std::path::PathBuf {
},
{
"id": "remote-thing",
"source": "https://example.invalid/remote-thing",
"source": "https://example.invalid/remote-thing.zip",
"tier": "curated",
"displayName": "Remote Thing"
}
@@ -115,6 +115,17 @@ fn marketplace_add_list_show_remove_roundtrip() {
assert!(list.contains("tier=official"), "{list}");
assert!(list.contains("tier=curated"), "{list}");
// Stored plans keep stable codes; rendering resolves the current locale.
app.ui_locale = Locale::Es419;
let localized = plugins(&mut app, Some("marketplace list")).message.unwrap();
assert!(localized.contains("no admite paquetes ZIP"), "{localized}");
assert!(!localized.contains("kimi_zip_unsupported"), "{localized}");
assert!(
!localized.contains("ZIP bundles are not supported"),
"{localized}"
);
app.ui_locale = Locale::En;
let show = plugins(&mut app, Some("marketplace show kimi"))
.message
.unwrap();
+135 -11
View File
@@ -19,7 +19,7 @@
use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write as _;
use std::path::PathBuf;
use std::path::{Path, PathBuf};
use crate::commands::CommandResult;
use crate::commands::traits::{
@@ -29,6 +29,7 @@ use crate::localization::{MessageId, tr};
use crate::plugins::types::{LoadedPlugin, PluginDiagnosticLevel};
use crate::tui::app::{App, AppAction};
mod kimi_import;
mod legacy;
mod marketplace;
#[cfg(test)]
@@ -57,7 +58,7 @@ impl CommandGroup for PluginsCommands {
pub(in crate::commands) const PLUGINS_INFO: CommandInfo = CommandInfo {
name: "plugin",
aliases: &["plugins"],
usage: "/plugin [list|show|suggest|validate|export|install|update|uninstall|trust|enable|disable|revoke|reload|tools|marketplace]",
usage: "/plugin [list|show|suggest|validate|export|install|import|update|uninstall|trust|enable|disable|revoke|reload|tools|marketplace]",
description_id: MessageId::CmdPluginDescription,
};
@@ -74,14 +75,32 @@ impl RegisterCommand for PluginsCmd {
}
fn plugins(app: &mut App, arg: Option<&str>) -> CommandResult {
plugins_with_kimi_home_override(app, arg, None)
}
#[cfg(test)]
fn plugins_with_kimi_home(app: &mut App, arg: Option<&str>, home: &Path) -> CommandResult {
plugins_with_kimi_home_override(app, arg, Some(home))
}
fn plugins_with_kimi_home_override(
app: &mut App,
arg: Option<&str>,
kimi_home: Option<&Path>,
) -> CommandResult {
let words = arg
.unwrap_or_default()
.split_whitespace()
.collect::<Vec<_>>();
match words.as_slice() {
[] | ["list"] => list_bundles_and_legacy_tools(app),
["help"] => CommandResult::message(tr(app.ui_locale, MessageId::CmdPluginBundleUsage)),
["help"] => CommandResult::message(format!(
"{}\n\n/plugin import kimi [list]\n/plugin import kimi approve <name> <content-hash>",
tr(app.ui_locale, MessageId::CmdPluginBundleUsage)
)),
["marketplace", rest @ ..] => marketplace::dispatch(app, rest),
["import", "kimi", rest @ ..] => kimi_import::dispatch(app, rest, kimi_home),
["import", ..] => CommandResult::error(kimi_import::usage(app.ui_locale)),
["show", selector] => show_bundle(app, selector),
["suggest"] | ["recommend"] => CommandResult::error("Usage: /plugin suggest <task>"),
["suggest", task @ ..] | ["recommend", task @ ..] => suggest_bundles(app, &task.join(" ")),
@@ -421,10 +440,6 @@ fn validate_bundles(app: &App, selector: Option<&str>) -> CommandResult {
// bits are always disabled and untrusted until the hash-bound trust flow runs.
fn install_bundle(app: &mut App, spec: &str) -> CommandResult {
use crate::plugins::mutation::{
PluginMutationContext, PluginMutationOutcome, PluginMutationRequest,
};
let source = match crate::plugins::install::PluginInstallSource::parse(spec) {
Ok(source) => source,
Err(error) => {
@@ -434,28 +449,89 @@ fn install_bundle(app: &mut App, spec: &str) -> CommandResult {
));
}
};
install_bundle_source(app, source, None)
}
fn install_bundle_with_expected_hash(
app: &mut App,
path: &std::path::Path,
expected_content_hash: &str,
) -> CommandResult {
install_bundle_source(
app,
crate::plugins::install::PluginInstallSource::LocalPath(path.to_path_buf()),
Some(expected_content_hash),
)
}
fn install_bundle_source(
app: &mut App,
source: crate::plugins::install::PluginInstallSource,
expected_content_hash: Option<&str>,
) -> CommandResult {
use crate::plugins::mutation::{
PluginMutationContext, PluginMutationOutcome, PluginMutationRequest,
};
let network = plugin_network_policy();
let expected_content_hash = expected_content_hash.map(str::to_string);
let expected_for_request = expected_content_hash.clone();
let registry = std::sync::Arc::make_mut(&mut app.plugin_registry);
let outcome = run_async(async move {
let ctx = PluginMutationContext {
network: &network,
max_size: crate::plugins::install::DEFAULT_MAX_SIZE_BYTES,
};
crate::plugins::mutation::execute(PluginMutationRequest::Install { source }, &ctx, registry)
.await
let request = match expected_for_request {
Some(expected_content_hash) => PluginMutationRequest::InstallExact {
source,
expected_content_hash,
},
None => PluginMutationRequest::Install { source },
};
crate::plugins::mutation::execute(request, &ctx, registry).await
});
match outcome {
Ok(receipt) => match receipt.outcome {
PluginMutationOutcome::Installed => {
let name = receipt.name.clone();
let path = receipt
.path
let installed_path = receipt.path.clone();
let installed_content_hash = receipt.installed_content_hash.clone();
let path = installed_path
.as_deref()
.map(|path| path.display().to_string())
.unwrap_or_default();
if let Some(expected) = expected_content_hash.as_deref()
&& receipt.content_hash.as_deref() != Some(expected)
{
return rollback_hash_mismatch(
app,
&name,
installed_path.as_deref(),
expected,
receipt.content_hash.as_deref(),
);
}
app.plugin_registry = app.plugin_registry.rediscover_for_workspace(&app.workspace);
app.refresh_skill_cache();
if expected_content_hash.is_some() {
let post_copy_hash = app
.plugin_registry
.get(&name)
.map(|plugin| plugin.content_hash.clone());
if installed_content_hash.is_none()
|| post_copy_hash.as_deref() != installed_content_hash.as_deref()
{
return rollback_hash_mismatch(
app,
&name,
installed_path.as_deref(),
installed_content_hash.as_deref().unwrap_or("unavailable"),
post_copy_hash.as_deref(),
);
}
}
let mut output = format!(
"Installed plugin '{name}' to {path}.\n\
It is disabled and untrusted. Review its requested authority below, then trust and enable it.\n"
@@ -478,6 +554,54 @@ fn install_bundle(app: &mut App, spec: &str) -> CommandResult {
}
}
fn rollback_hash_mismatch(
app: &mut App,
name: &str,
installed_path: Option<&std::path::Path>,
expected: &str,
actual: Option<&str>,
) -> CommandResult {
let locale = app.ui_locale;
let missing_destination =
tr(locale, MessageId::PluginKimiRollbackDestinationMissing).into_owned();
let rollback = installed_path
.and_then(std::path::Path::parent)
.ok_or_else(|| anyhow::anyhow!(missing_destination))
.and_then(|plugins_dir| crate::plugins::install::uninstall(name, plugins_dir));
app.plugin_registry = app.plugin_registry.rediscover_for_workspace(&app.workspace);
app.refresh_skill_cache();
let actual = actual
.map(escape_review_text)
.unwrap_or_else(|| tr(locale, MessageId::PluginKimiHashUnavailable).into_owned());
let name = escape_review_text(name);
let expected = escape_review_text(expected);
match rollback {
Ok(()) => CommandResult::error(
tr(locale, MessageId::PluginKimiMismatchRemoved)
.replace("{name}", &name)
.replace("{expected}", &expected)
.replace("{actual}", &actual),
),
Err(error) => CommandResult {
message: Some(
tr(locale, MessageId::PluginKimiMismatchRollbackFailed)
.replace("{name}", &name)
.replace("{expected}", &expected)
.replace("{actual}", &actual)
.replace("{error}", &escape_review_text(&format!("{error:#}")))
.replace(
"{path}",
&installed_path.map(escape_review_path).unwrap_or_else(|| {
tr(locale, MessageId::PluginKimiUserPluginDirectory).into_owned()
}),
),
),
action: Some(AppAction::PluginRegistryChanged),
is_error: true,
},
}
}
fn update_bundle(app: &mut App, selector: &str) -> CommandResult {
use crate::plugins::mutation::{
PluginMutationContext, PluginMutationOutcome, PluginMutationRequest,
@@ -410,6 +410,144 @@ fn install_update_uninstall_verbs_drive_the_guided_trust_flow() {
});
}
#[test]
fn kimi_managed_import_is_read_only_until_hash_bound_approval() {
let _lock = crate::test_support::lock_test_env();
let root = TempDir::new().unwrap();
let codewhale_home = root.path().join("codewhale-home");
let _codewhale_home = crate::test_support::EnvVarGuard::set("CODEWHALE_HOME", &codewhale_home);
let managed = root.path().join(".kimi-code/plugins/managed/kimi-demo");
fs::create_dir_all(managed.join("skills/kimi-demo")).unwrap();
fs::write(
managed.join("kimi.plugin.json"),
r#"{
"name": "kimi-demo",
"version": "1.0.0",
"license": "Proprietary",
"skills": "./skills/",
"interface": {
"displayName": "Kimi Demo",
"hostKind": "local",
"platforms": ["macos"]
}
}"#,
)
.unwrap();
fs::write(
managed.join("skills/kimi-demo/SKILL.md"),
"---\nname: kimi-demo\ndescription: local managed fixture\n---\n",
)
.unwrap();
let (mut app, _temp) = create_test_app(root.path());
let help = plugins(&mut app, Some("help")).message.unwrap();
assert!(help.contains("/plugin import kimi [list]"), "{help}");
let listed = plugins_with_kimi_home(&mut app, Some("import kimi"), root.path());
assert!(!listed.is_error, "{:?}", listed.message);
let message = listed.message.unwrap();
assert!(
message.contains("Kimi Demo") || message.contains("kimi-demo"),
"{message}"
);
assert!(message.contains("license=Proprietary"), "{message}");
assert!(message.contains("content hash:"), "{message}");
assert!(message.contains("External Kimi apps"), "{message}");
let approval = message
.lines()
.find_map(|line| line.trim().strip_prefix("approve: /plugin "))
.expect("listing must render an exact approval command")
.to_string();
assert!(app.plugin_registry.get("kimi-demo").is_none());
assert!(!codewhale_home.join("plugins/kimi-demo").exists());
assert!(!codewhale_home.join("plugins/state.json").exists());
// The approval token is tied to the bytes that were inspected.
fs::write(
managed.join("skills/kimi-demo/SKILL.md"),
"---\nname: kimi-demo\ndescription: changed fixture\n---\n",
)
.unwrap();
let changed = plugins_with_kimi_home(&mut app, Some(&approval), root.path());
assert!(changed.is_error);
assert!(changed.message.unwrap().contains("changed since review"));
assert!(!codewhale_home.join("plugins/kimi-demo").exists());
let refreshed = plugins_with_kimi_home(&mut app, Some("import kimi"), root.path())
.message
.unwrap();
let approval = refreshed
.lines()
.find_map(|line| line.trim().strip_prefix("approve: /plugin "))
.unwrap()
.to_string();
let runtime = tokio::runtime::Builder::new_multi_thread()
.worker_threads(2)
.enable_all()
.build()
.unwrap();
runtime.block_on(async {
let installed = plugins_with_kimi_home(&mut app, Some(&approval), root.path());
assert!(!installed.is_error, "{:?}", installed.message);
assert!(
installed
.message
.as_deref()
.is_some_and(|message| message.contains("disabled and untrusted"))
);
});
let plugin = app.plugin_registry.get("kimi-demo").unwrap();
assert!(!plugin.enabled && !plugin.trusted());
assert!(
codewhale_home
.join("plugins/kimi-demo/kimi.plugin.json")
.is_file()
);
}
#[test]
fn kimi_managed_import_renders_in_the_selected_non_english_locale() {
let root = TempDir::new().unwrap();
let (mut app, _temp) = create_test_app(root.path());
app.ui_locale = Locale::Es419;
let message = plugins_with_kimi_home(&mut app, Some("import kimi"), root.path())
.message
.expect("localized Kimi listing");
assert!(
message.contains("Plugins gestionados por Kimi"),
"{message}"
);
assert!(
message.contains("No se encontraron plugins gestionados válidos"),
"{message}"
);
assert!(
!message.contains("No valid managed plugins found"),
"{message}"
);
}
#[cfg(unix)]
#[test]
fn kimi_managed_import_refuses_linked_children() {
use std::os::unix::fs::symlink;
let root = TempDir::new().unwrap();
let managed_root = root.path().join(".kimi-code/plugins/managed");
let outside = root.path().join("outside");
fs::create_dir_all(&managed_root).unwrap();
fs::create_dir_all(&outside).unwrap();
symlink(&outside, managed_root.join("linked-plugin")).unwrap();
let (mut app, _temp) = create_test_app(root.path());
let result = plugins_with_kimi_home(&mut app, Some("import kimi"), root.path());
assert!(!result.is_error);
let message = result.message.unwrap();
assert!(message.contains("Rejected entries"), "{message}");
assert!(message.contains("links and reparse points are refused"));
assert!(!message.contains("approve: /plugin import kimi approve linked-plugin"));
}
#[test]
fn export_verb_writes_agent_plugins_bundle() {
let _lock = crate::test_support::lock_test_env();
+136 -56
View File
@@ -41,6 +41,8 @@ fn hunt(app: &mut App, arg: Option<&str>) -> CommandResult {
close_hunt(app, HuntVerdict::Wounded, GoalStatus::Paused)
}
Some("resume") | Some("continue") => resume_hunt(app),
Some("help") | Some("?") | Some("usage") => CommandResult::message(goal_usage()),
Some("status") | Some("show") => goal_status(app),
Some("block") | Some("blocked") | Some("escape") | Some("escaped") => {
close_hunt(app, HuntVerdict::Escaped, GoalStatus::Blocked)
}
@@ -66,40 +68,13 @@ fn hunt(app: &mut App, arg: Option<&str>) -> CommandResult {
)
}
_ => {
if let Some(ref obj) = app.hunt.quarry {
let elapsed = app
.hunt
.time_used_seconds
.gt(&0)
.then(|| crate::elapsed::format_elapsed_secs(app.hunt.time_used_seconds))
.or_else(|| {
app.hunt
.started_at
.map(|t| crate::elapsed::format_elapsed_secs(t.elapsed().as_secs()))
})
.unwrap_or_else(|| "unknown".to_string());
let budget_str = app
.hunt
.token_budget
.map(|b| {
let used = if app.hunt.tokens_used > 0 {
app.hunt.tokens_used
} else {
u64::from(app.session.total_conversation_tokens)
};
let pct = if b > 0 {
(used as f64 / f64::from(b) * 100.0).min(100.0)
} else {
0.0
};
format!(" | tokens: {used}/{b} ({pct:.0}%)")
})
.unwrap_or_default();
let verdict_label = hunt_verdict_label(app.hunt.verdict);
CommandResult::message(format!(
"Goal {verdict_label}: \"{obj}\" - elapsed: {elapsed}{budget_str} | continuations: {}",
app.hunt.continuation_count
))
if app.hunt.quarry.is_some() {
goal_status(app)
} else if app.api_messages.is_empty() {
// Nothing has happened yet: there is no context to derive an
// objective from, so answer with usage instead of spending a
// model turn on a question we already know the answer to.
CommandResult::message(goal_usage())
} else {
// Context-dependent bare /goal: with no active goal, the
// invocation itself is the ask — derive the objective from
@@ -122,6 +97,55 @@ fn hunt(app: &mut App, arg: Option<&str>) -> CommandResult {
}
}
/// Plain status line: objective, state, elapsed, budget, continuations, and
/// — for an active goal that no turn is driving right now — how to continue.
fn goal_status(app: &App) -> CommandResult {
let Some(obj) = app.hunt.quarry.as_deref() else {
return CommandResult::message(goal_usage());
};
let elapsed = app
.hunt
.time_used_seconds
.gt(&0)
.then(|| crate::elapsed::format_elapsed_secs(app.hunt.time_used_seconds))
.or_else(|| {
app.hunt
.started_at
.map(|t| crate::elapsed::format_elapsed_secs(t.elapsed().as_secs()))
})
.unwrap_or_else(|| "unknown".to_string());
let budget_str = app
.hunt
.token_budget
.map(|b| {
let used = if app.hunt.tokens_used > 0 {
app.hunt.tokens_used
} else {
u64::from(app.session.total_conversation_tokens)
};
let pct = if b > 0 {
(used as f64 / f64::from(b) * 100.0).min(100.0)
} else {
0.0
};
format!(" · tokens {used}/{b} ({pct:.0}%)")
})
.unwrap_or_default();
let mut state = hunt_verdict_label(app.hunt.verdict).to_string();
if let (HuntVerdict::Wounded, Some(reason)) = (app.hunt.verdict, app.hunt.pause_reason) {
state = format!("{state} ({})", reason.label());
}
let mut line = format!(
"Goal {state}: \"{obj}\" · elapsed {elapsed}{budget_str} · continuations {}",
app.hunt.continuation_count
);
if app.hunt.verdict == HuntVerdict::Hunting && !app.is_loading {
line.push_str(" · ");
line.push_str(&app.tr(MessageId::GoalStatusIdleHint));
}
CommandResult::message(line)
}
fn declare_hunted(app: &mut App) -> CommandResult {
let previous = app.hunt.verdict;
let result = close_hunt(app, HuntVerdict::Hunted, GoalStatus::Complete);
@@ -173,16 +197,16 @@ fn close_hunt(app: &mut App, verdict: HuntVerdict, status: GoalStatus) -> Comman
HuntVerdict::Hunted => {
let elapsed = goal_elapsed_at_close(&app.hunt);
CommandResult::with_message_and_action(
format!("Goal hunted. Elapsed: {elapsed}"),
format!("Goal complete. Elapsed: {elapsed}"),
action,
)
}
HuntVerdict::Wounded => CommandResult::with_message_and_action(
"Goal wounded. Progress is saved; use /goal resume to continue.",
"Goal paused. Progress is saved; use /goal resume to continue.",
action,
),
HuntVerdict::Escaped => CommandResult::with_message_and_action("Goal escaped.", action),
HuntVerdict::Hunting => CommandResult::with_message_and_action("Goal hunting.", action),
HuntVerdict::Escaped => CommandResult::with_message_and_action("Goal blocked.", action),
HuntVerdict::Hunting => CommandResult::with_message_and_action("Goal active.", action),
}
}
@@ -209,20 +233,22 @@ fn resume_hunt(app: &mut App) -> CommandResult {
}
fn goal_usage() -> &'static str {
"No goal set. Use /goal <objective> [budget: N] to set one.\n\
/goal declare-hunted - override verification and mark hunted\n\
/goal wounded - pause without continuing\n\
"No goal set. /goal <objective> [budget: N] sets one; the agent works toward it \
across turns until it is verified complete, blocked, or you stop it.\n\
/goal - progress of the current goal\n\
/goal pause - pause without continuing\n\
/goal resume - resume and continue\n\
/goal escaped - mark escaped\n\
/goal done - mark complete (declare-hunted skips verification)\n\
/goal blocked - mark blocked\n\
/goal clear - remove the current goal."
}
fn hunt_verdict_label(verdict: HuntVerdict) -> &'static str {
match verdict {
HuntVerdict::Hunting => "[HUNTING]",
HuntVerdict::Hunted => "[HUNTED]",
HuntVerdict::Wounded => "[WOUNDED]",
HuntVerdict::Escaped => "[ESCAPED]",
HuntVerdict::Hunting => "active",
HuntVerdict::Hunted => "complete",
HuntVerdict::Wounded => "paused",
HuntVerdict::Escaped => "blocked",
}
}
@@ -366,7 +392,7 @@ fn write_trophy_card_contents(mut f: impl Write, card: TrophyCard<'_>) -> std::i
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "goal",
aliases: &["hunt", "mubiao", "狩猎"],
usage: "/goal [objective|clear|wounded|resume|declare-hunted|escaped] [budget: N]",
usage: "/goal [objective|status|pause|resume|done|blocked|clear] [budget: N]",
description_id: MessageId::CmdGoalDescription,
};
@@ -417,6 +443,13 @@ mod tests {
// derives the objective from the conversation and sets it via
// create_goal — it must not error with a usage demand.
let mut app = create_test_app();
app.api_messages.push(crate::models::Message {
role: "user".to_string(),
content: vec![crate::models::ContentBlock::Text {
text: "make the tests pass".to_string(),
cache_control: None,
}],
});
let result = hunt(&mut app, None);
assert!(!result.is_error);
let Some(AppAction::SendMessage(message)) = result.action else {
@@ -426,6 +459,44 @@ mod tests {
assert!(message.contains("`create_goal`"));
}
#[test]
fn bare_goal_on_an_empty_session_prints_usage_without_a_model_turn() {
// No conversation yet: there is nothing to derive an objective from,
// so the answer is usage — free, and not a question to the model.
let mut app = create_test_app();
let result = hunt(&mut app, None);
assert!(!result.is_error);
assert!(result.action.is_none());
assert!(result.message.unwrap().contains("/goal <objective>"));
let result = hunt(&mut app, Some("help"));
assert!(result.action.is_none());
assert!(result.message.unwrap().contains("/goal resume"));
assert!(
app.hunt.quarry.is_none(),
"help must not become an objective"
);
}
#[test]
fn goal_status_is_plain_and_says_how_to_continue_when_idle() {
let mut app = create_test_app();
let _ = hunt(&mut app, Some("ship the release notes"));
app.is_loading = false;
let result = hunt(&mut app, Some("status"));
let text = result.message.unwrap();
assert!(
text.starts_with("Goal active: \"ship the release notes\""),
"{text}"
);
assert!(text.contains("/goal resume"), "{text}");
assert!(!text.contains('['), "no bracket tags: {text}");
app.is_loading = true;
let text = hunt(&mut app, None).message.unwrap();
assert!(!text.contains("/goal resume"), "{text}");
}
#[test]
fn test_hunt_without_argument_shows_state_when_goal_active() {
// With an active goal, bare /goal stays a status readout.
@@ -443,10 +514,13 @@ mod tests {
}
#[test]
fn test_command_usage_mentions_hunt_verdicts() {
assert!(COMMAND_INFO.usage.contains("declare-hunted"));
assert!(COMMAND_INFO.usage.contains("wounded"));
assert!(COMMAND_INFO.usage.contains("escaped"));
fn test_command_usage_mentions_host_verbs() {
assert!(COMMAND_INFO.usage.contains("status"));
assert!(COMMAND_INFO.usage.contains("pause"));
assert!(COMMAND_INFO.usage.contains("resume"));
assert!(COMMAND_INFO.usage.contains("done"));
assert!(COMMAND_INFO.usage.contains("blocked"));
assert!(COMMAND_INFO.usage.contains("clear"));
}
#[test]
@@ -566,7 +640,7 @@ mod tests {
.message
.as_deref()
.unwrap_or_default()
.contains("Goal hunted. Elapsed:"),
.contains("Goal complete. Elapsed:"),
"close-out message should report a frozen elapsed; got {:?}",
result.message
);
@@ -594,8 +668,7 @@ mod tests {
let result = hunt(&mut app, None);
let message = result.message.as_deref().unwrap_or_default();
assert!(message.contains("Goal [ESCAPED]"));
assert!(!message.contains("[BLOCKED]"));
assert!(message.starts_with("Goal blocked:"), "{message}");
}
#[test]
@@ -666,9 +739,16 @@ mod tests {
#[test]
fn test_show_hunt_when_none() {
// Bare /goal with no active goal now declares one from context
// instead of printing usage.
// Bare /goal with no active goal but a live conversation declares one
// from context instead of printing usage.
let mut app = create_test_app();
app.api_messages.push(crate::models::Message {
role: "user".to_string(),
content: vec![crate::models::ContentBlock::Text {
text: "fix the flaky test".to_string(),
cache_control: None,
}],
});
let result = hunt(&mut app, None);
assert!(
result
@@ -20,6 +20,7 @@ mod resume;
mod save;
mod sessions;
mod structcopy;
mod title;
mod tree;
// This group dir intentionally has a `session.rs` child module with the same
// name. The module_inception allow is a permanent structure rationale, not
@@ -39,6 +40,10 @@ impl CommandGroup for SessionCommands {
rename::RenameCmd::info(),
rename::RenameCmd::execute,
)),
Box::new(FunctionCommand::new(
title::TitleCmd::info(),
title::TitleCmd::execute,
)),
Box::new(FunctionCommand::new(
save::SaveCmd::info(),
save::SaveCmd::execute,
@@ -35,7 +35,12 @@ impl RegisterCommand for RenameCmd {
/// The new title is persisted immediately to `~/.deepseek/sessions/<id>.json`
/// so the updated name is visible the next time the session picker is opened.
pub fn rename(app: &mut App, arg: Option<&str>) -> CommandResult {
let new_title = match arg.map(str::trim).filter(|s| !s.is_empty()) {
// Same character policy as the picker and Runtime API rename: controls
// and bidi/zero-width format characters never reach the persisted title.
let sanitized = arg
.map(crate::session_manager::sanitize_session_title)
.unwrap_or_default();
let new_title = match Some(sanitized.trim()).filter(|s| !s.is_empty()) {
Some(t) => t,
None => return CommandResult::error("Usage: /rename <new title>"),
};
@@ -67,6 +72,13 @@ pub(crate) fn rename_with_manager(
manager: &SessionManager,
app: &mut App,
) -> CommandResult {
// Same character policy as the picker and Runtime API rename: controls
// and bidi/zero-width format characters never reach the persisted title.
let sanitized = crate::session_manager::sanitize_session_title(new_title);
let new_title = sanitized.trim();
if new_title.is_empty() {
return CommandResult::error("Usage: /rename <new title>");
}
let mut session = match manager.load_session(session_id) {
Ok(s) => s,
Err(e) => return CommandResult::error(format!("Could not load session: {e}")),
@@ -108,7 +120,9 @@ pub(crate) fn rename_with_manager(
"Session renamed, but Work views were not published: {err}"
));
}
CommandResult::message(format!("Session renamed to \"{new_title}\""))
CommandResult::message(format!(
"Session and terminal tab renamed to \"{new_title}\""
))
}
Err(e) => CommandResult::error(format!("Could not save session: {e}")),
}
@@ -232,6 +246,33 @@ mod tests {
);
}
#[test]
fn rename_strips_terminal_controls_before_persisting() {
let tmp = TempDir::new().unwrap();
let manager = make_session_manager(&tmp);
let mut app = make_app(&tmp);
let session =
create_saved_session_with_mode(&[], "deepseek-v4-pro", tmp.path(), 0, None, None);
let session_id = session.metadata.id.clone();
manager.save_session(&session).unwrap();
app.current_session_id = Some(session_id.clone());
let result = rename_with_manager(
"Ev\u{1b}]0;PWNED\u{7}il\u{202e} Beta",
&session_id,
&manager,
&mut app,
);
assert!(!result.is_error, "{result:?}");
let reloaded = manager.load_session(&session_id).unwrap();
assert_eq!(reloaded.metadata.title, "Ev]0;PWNEDil Beta");
assert_eq!(app.session_title.as_deref(), Some("Ev]0;PWNEDil Beta"));
// Controls alone are the same as no title at all.
let result = rename_with_manager("\u{1b}\u{7}\u{200b}", &session_id, &manager, &mut app);
assert!(result.is_error);
}
#[test]
fn rename_title_at_max_length_succeeds() {
let tmp = TempDir::new().unwrap();
@@ -0,0 +1,29 @@
//! Discoverable terminal-tab name command backed by the existing session name.
use crate::commands::traits::{CommandInfo, RegisterCommand};
use crate::localization::MessageId;
use crate::tui::app::App;
use super::CommandResult;
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "title",
aliases: &["tabtitle", "window-title"],
usage: "/title <name>",
description_id: MessageId::CmdTitleDescription,
};
pub(in crate::commands) struct TitleCmd;
impl RegisterCommand for TitleCmd {
fn info() -> &'static CommandInfo {
&COMMAND_INFO
}
fn execute(app: &mut App, arg: Option<&str>) -> CommandResult {
let Some(title) = arg.map(str::trim).filter(|title| !title.is_empty()) else {
return CommandResult::error("Usage: /title <name>");
};
super::rename::rename(app, Some(title))
}
}
+107 -19
View File
@@ -1,11 +1,14 @@
//! In-TUI MCP manager command parser.
use crate::commands::traits::{CommandInfo, RegisterCommand};
use crate::localization::MessageId;
use crate::localization::{Locale, MessageId, tr};
use crate::tui::app::{App, AppAction, McpUiAction};
use crate::commands::CommandResult;
const GITHUB_MCP_URL: &str = "https://api.githubcopilot.com/mcp/";
const CHROME_DEVTOOLS_MCP_PACKAGE: &str = "chrome-devtools-mcp@1.7.0";
pub(in crate::commands) const COMMAND_INFO: CommandInfo = CommandInfo {
name: "mcp",
aliases: &[],
@@ -25,7 +28,7 @@ impl RegisterCommand for McpCmd {
}
}
fn mcp(_app: &mut App, args: Option<&str>) -> CommandResult {
fn mcp(app: &mut App, args: Option<&str>) -> CommandResult {
let raw = args.unwrap_or("").trim();
if raw.is_empty() || raw.eq_ignore_ascii_case("status") || raw.eq_ignore_ascii_case("list") {
return CommandResult::action(AppAction::Mcp(McpUiAction::Show));
@@ -38,9 +41,9 @@ fn mcp(_app: &mut App, args: Option<&str>) -> CommandResult {
force: parts.any(|part| part == "--force" || part == "-f"),
})),
"recommend" | "recommended" | "recommendations" => {
CommandResult::message(recommended_mcp_text())
CommandResult::message(recommended_mcp_text(app.ui_locale))
}
"add" => parse_add(parts.collect()),
"add" => parse_add(app.ui_locale, parts.collect()),
"enable" => match parse_name(parts.next(), "Usage: /mcp enable <name>") {
Ok(name) => CommandResult::action(AppAction::Mcp(McpUiAction::Enable { name })),
Err(msg) => CommandResult::error(msg),
@@ -112,7 +115,7 @@ fn parse_name(name: Option<&str>, usage: &str) -> Result<String, String> {
}
}
fn parse_add(parts: Vec<&str>) -> CommandResult {
fn parse_add(locale: Locale, parts: Vec<&str>) -> CommandResult {
if parts
.first()
.is_some_and(|part| part.eq_ignore_ascii_case("recommended"))
@@ -125,8 +128,26 @@ fn parse_add(parts: Vec<&str>) -> CommandResult {
transport: None,
}))
}
[_, id] if id.eq_ignore_ascii_case("github") || id.eq_ignore_ascii_case("gh") => {
CommandResult::action(AppAction::Mcp(McpUiAction::AddHttp {
name: "github".to_string(),
url: GITHUB_MCP_URL.to_string(),
transport: None,
}))
}
[_, id]
if id.eq_ignore_ascii_case("chrome-devtools")
|| id.eq_ignore_ascii_case("chrome") =>
{
CommandResult::action(AppAction::Mcp(McpUiAction::AddStdio {
name: "chrome-devtools".to_string(),
command: recommended_npx_command().to_string(),
args: vec!["-y".to_string(), CHROME_DEVTOOLS_MCP_PACKAGE.to_string()],
}))
}
[_, _] => CommandResult::error(
"Unknown recommended MCP id. Run /mcp recommendations to inspect the curated list.",
tr(locale, MessageId::McpRecommendedUnknownId)
.replace("{recommendations_command}", "/mcp recommendations"),
),
_ => CommandResult::error("Usage: /mcp add recommended <id>"),
};
@@ -158,19 +179,45 @@ fn parse_add(parts: Vec<&str>) -> CommandResult {
}
}
fn recommended_mcp_text() -> &'static str {
"Recommended MCP servers (suggestions only; nothing is installed automatically)\n\
\n\
hugging-face remote Hugging Face MCP endpoint\n\
provenance: bundled Codewhale recommendation\n\
add explicitly: /mcp add recommended hugging-face\n\
then inspect: /mcp doctor · reload all configured servers: /mcp restart\n\
\n\
External sources (~/.claude.json, .mcp.json, marketplace manifests):\n\
/mcp import list candidates with provenance (keyboard/mouse status)\n\
/mcp import approve <name> create managed connector after consent\n\
/mcp import decline <name> durable decline until source content changes\n\
enabled=false is a hard block and will never import. Nothing is auto-imported."
fn recommended_mcp_text(locale: Locale) -> String {
let safety = tr(locale, MessageId::McpRecommendationsSafety)
.replace("{restart_command}", "/mcp restart");
let github = tr(locale, MessageId::McpRecommendationGithub)
.replace("{endpoint}", GITHUB_MCP_URL)
.replace("{login_command}", "/mcp login github")
.replace("{add_command}", "/mcp add recommended github");
let chrome = tr(locale, MessageId::McpRecommendationChrome)
.replace("{package}", CHROME_DEVTOOLS_MCP_PACKAGE)
.replace("{launcher}", "npx/npx.cmd")
.replace("{restart_command}", "/mcp restart")
.replace("{add_command}", "/mcp add recommended chrome-devtools");
format!(
"Recommended MCP servers (suggestions only; nothing is installed automatically)\n\
{safety}\n\
\n\
hugging-face remote Hugging Face MCP endpoint\n\
provenance: bundled Codewhale recommendation\n\
add explicitly: /mcp add recommended hugging-face\n\
then inspect: /mcp doctor · reload all configured servers: /mcp restart\n\
\n\
{github}\n\
\n\
{chrome}\n\
\n\
External sources (~/.claude.json, .mcp.json, marketplace manifests):\n\
/mcp import list candidates with provenance (keyboard/mouse status)\n\
/mcp import approve <name> create managed connector after consent\n\
/mcp import decline <name> durable decline until source content changes\n\
enabled=false is a hard block and will never import. Nothing is auto-imported."
)
}
fn recommended_npx_command() -> &'static str {
recommended_npx_command_for(cfg!(windows))
}
fn recommended_npx_command_for(windows: bool) -> &'static str {
if windows { "npx.cmd" } else { "npx" }
}
fn parse_scopes(parts: Vec<&str>) -> Vec<String> {
@@ -259,6 +306,25 @@ mod tests {
.expect("recommendations text");
assert!(recommended.contains("nothing is installed automatically"));
assert!(recommended.contains("provenance:"));
assert!(recommended.contains("https://api.githubcopilot.com/mcp/"));
assert!(recommended.contains("chrome-devtools-mcp@1.7.0"));
assert!(recommended.contains("least-privilege PAT outside command history"));
assert!(recommended.contains("read authenticated pages"));
app.ui_locale = Locale::Es419;
let localized = mcp(&mut app, Some("recommendations"))
.message
.expect("localized recommendations text");
assert!(localized.contains("páginas autenticadas"), "{localized}");
assert!(
!localized.contains("read authenticated pages"),
"{localized}"
);
let unknown = mcp(&mut app, Some("add recommended unknown"))
.message
.expect("localized unknown recommendation error");
assert!(unknown.contains("ID de MCP recomendado"), "{unknown}");
app.ui_locale = Locale::En;
let add_recommended = mcp(&mut app, Some("add recommended hugging-face"));
assert!(matches!(
@@ -267,6 +333,22 @@ mod tests {
if name == "hugging-face" && url == "https://huggingface.co/mcp"
));
let add_github = mcp(&mut app, Some("add recommended github"));
assert!(matches!(
add_github.action,
Some(AppAction::Mcp(McpUiAction::AddHttp { name, url, transport: None }))
if name == "github" && url == GITHUB_MCP_URL
));
let add_chrome = mcp(&mut app, Some("add recommended chrome-devtools"));
assert!(matches!(
add_chrome.action,
Some(AppAction::Mcp(McpUiAction::AddStdio { name, command, args }))
if name == "chrome-devtools"
&& command == recommended_npx_command()
&& args == vec!["-y".to_string(), CHROME_DEVTOOLS_MCP_PACKAGE.to_string()]
));
let import_list = mcp(&mut app, Some("import"));
assert!(matches!(
import_list.action,
@@ -302,4 +384,10 @@ mod tests {
&& scopes == vec!["tools/read".to_string(), "tools/write".to_string()]
));
}
#[test]
fn recommended_chrome_launcher_is_native_on_unix_and_windows() {
assert_eq!(recommended_npx_command_for(false), "npx");
assert_eq!(recommended_npx_command_for(true), "npx.cmd");
}
}
+355 -9
View File
@@ -89,9 +89,11 @@ impl Default for CompactionConfig {
}
}
/// Minimum non-whitespace characters for a usable successor summary.
/// Below this (or missing required section headings), treat as degenerate and
/// retry once rather than shipping amnesia (compactionidea failure ladder).
/// A provider can return HTTP success with an empty, non-text, or known
/// placeholder response. Committing that response would discard the useful
/// history while leaving only a placeholder checkpoint. Keep this deliberately
/// conservative: it is a corruption guard, not a prose-length or language
/// scorer.
const COMPACTION_LANGUAGE_CONTRACT: &str = "Use the natural language of the most recent \
substantive user message for reasoning and user-facing prose. Keep code, identifiers, paths, \
commands, logs, tool payloads, quotations, and the English structural labels verbatim. English \
@@ -941,6 +943,7 @@ pub async fn compact_messages_safe(
};
let mut last_error: Option<anyhow::Error> = None;
let mut quality_retries = 0u32;
for attempt in 0..MAX_RETRIES {
if attempt > 0 {
@@ -949,13 +952,15 @@ pub async fn compact_messages_safe(
tokio::time::sleep(delay).await;
}
match compact_messages(client, compaction_input, config).await {
match compact_messages_with_metadata(client, compaction_input, config, &mut quality_retries)
.await
{
Ok((msgs, prompt, removed)) => {
drop(removed);
return Ok(CompactionResult {
messages: sanitize_retained_messages(msgs),
summary_prompt: prompt,
retries_used: attempt,
retries_used: attempt.saturating_add(quality_retries),
});
}
Err(e) => {
@@ -1043,16 +1048,29 @@ fn user_anchors_section(workspace: Option<&std::path::Path>) -> String {
}
}
pub async fn compact_messages(
#[cfg(test)]
async fn compact_messages(
client: &dyn ModelClient,
messages: &[Message],
config: &CompactionConfig,
) -> Result<(Vec<Message>, Option<SystemPrompt>, Vec<Message>)> {
let mut quality_retries = 0;
let (messages, summary_prompt, removed) =
compact_messages_with_metadata(client, messages, config, &mut quality_retries).await?;
Ok((messages, summary_prompt, removed))
}
async fn compact_messages_with_metadata(
client: &dyn ModelClient,
messages: &[Message],
config: &CompactionConfig,
quality_retries: &mut u32,
) -> Result<(Vec<Message>, Option<SystemPrompt>, Vec<Message>)> {
if messages.is_empty() {
return Ok((Vec::new(), None, Vec::new()));
}
let summary = create_summary(client, messages, config).await?;
let summary = create_summary(client, messages, config, quality_retries).await?;
let anchors = user_anchors_section(config.workspace.as_deref());
let checkpoint_text = build_compaction_summary_block_text(&summary, &anchors);
let summary_block = SystemBlock {
@@ -1085,6 +1103,58 @@ fn compact_prompt(focus: Option<&str>) -> String {
prompt
}
fn compact_quality_retry_prompt(focus: Option<&str>) -> String {
let mut prompt = format!(
"The previous handoff response was empty or a placeholder. Return a substantive factual \
continuation handoff. State the user objective, completed and current work, hard constraints, verified \
evidence, unresolved failures, and the single next action. Do not refuse, call tools, discuss \
checkpoint machinery, or return a placeholder. {COMPACTION_LANGUAGE_CONTRACT}"
);
if let Some(focus) = focus.map(str::trim).filter(|focus| !focus.is_empty()) {
let _ = write!(
prompt,
"\n\nThe user asked this compaction to focus on: {focus}"
);
}
prompt
}
fn validate_compaction_summary(summary: &str) -> Result<()> {
let trimmed = summary.trim();
if trimmed.is_empty() {
anyhow::bail!("Compaction summary response was unusable: no text was returned.");
}
// Strip every non-word edge, not just ASCII punctuation. Providers can
// return visually non-empty Unicode punctuation or emoji-only payloads;
// neither is a usable continuation checkpoint. `is_alphanumeric` keeps
// this language-neutral for CJK and other scripts without imposing a
// prose-length heuristic.
let normalized = trimmed
.trim_matches(|ch: char| !ch.is_alphanumeric())
.to_ascii_lowercase();
if normalized.is_empty() {
anyhow::bail!(
"Compaction summary response was unusable: only whitespace or punctuation was returned."
);
}
if matches!(
normalized.as_str(),
"no summary available"
| "summary unavailable"
| "no summary"
| "n/a"
| "na"
| "not available"
| "i cannot provide a summary"
| "i can't provide a summary"
| "unable to provide a summary"
) {
anyhow::bail!("Compaction summary response was unusable: a placeholder was returned.");
}
Ok(())
}
/// Drop the oldest history message before retrying an over-window summary
/// request (Codex parity: `history.remove_first_item()`), plus any tool
/// results the removal orphans — strict providers reject unpaired results.
@@ -1107,6 +1177,7 @@ async fn create_summary(
client: &dyn ModelClient,
messages: &[Message],
config: &CompactionConfig,
quality_retries: &mut u32,
) -> Result<String> {
// The summarization request IS the live conversation plus one final user
// message asking for the handoff summary, so the provider's prefix cache
@@ -1130,6 +1201,7 @@ async fn create_summary(
}],
});
let mut quality_retry_used = false;
loop {
// Codex compaction is a normal model generation over the existing
// cached prefix. Do the same here: the resolved route decides how
@@ -1204,7 +1276,7 @@ async fn create_summary(
);
}
return Ok(response
let summary = response
.content
.iter()
.filter_map(|block| match block {
@@ -1212,7 +1284,34 @@ async fn create_summary(
_ => None,
})
.collect::<Vec<_>>()
.join("\n"));
.join("\n");
if let Err(error) = validate_compaction_summary(&summary) {
if quality_retry_used {
return Err(error.context(
"Compaction summary remained unusable after one conservative retry; \
no replacement checkpoint was committed",
));
}
quality_retry_used = true;
*quality_retries = (*quality_retries).saturating_add(1);
logging::warn(
"Compaction provider returned an unusable successful response; retrying once with the conservative handoff prompt",
);
let Some(instruction) = request_messages.last_mut() else {
return Err(error.context(
"Compaction summary validation failed and the retry instruction was missing",
));
};
instruction.content = vec![ContentBlock::Text {
text: compact_quality_retry_prompt(config.focus.as_deref()),
cache_control: None,
}];
continue;
}
return Ok(summary);
}
}
@@ -1568,6 +1667,74 @@ mod tests {
2. Key technical concepts sqlite. 7. Pending tasks finish the fixed clock. \
8. Current work rerunning the session tests.";
struct ScriptedSummaryClient {
responses: std::sync::Mutex<std::collections::VecDeque<anyhow::Result<Vec<ContentBlock>>>>,
requests: std::sync::Mutex<Vec<MessageRequest>>,
}
impl ScriptedSummaryClient {
fn new(responses: Vec<Vec<ContentBlock>>) -> Self {
Self::with_outcomes(responses.into_iter().map(Ok).collect())
}
fn with_outcomes(responses: Vec<anyhow::Result<Vec<ContentBlock>>>) -> Self {
Self {
responses: std::sync::Mutex::new(responses.into()),
requests: std::sync::Mutex::new(Vec::new()),
}
}
}
#[async_trait::async_trait]
impl crate::core::model_client::ModelClient for ScriptedSummaryClient {
fn provider_name(&self) -> &str {
"test"
}
fn model(&self) -> &str {
"test-model"
}
async fn create_message(
&self,
request: MessageRequest,
) -> anyhow::Result<crate::models::MessageResponse> {
self.requests
.lock()
.expect("capture scripted summary request")
.push(request);
let outcome = self
.responses
.lock()
.expect("read scripted summary response")
.pop_front()
.ok_or_else(|| anyhow::anyhow!("scripted summary responses exhausted"))?;
let content = outcome?;
Ok(crate::models::MessageResponse {
id: "summary-scripted".to_string(),
r#type: "message".to_string(),
role: "assistant".to_string(),
content,
model: "test-model".to_string(),
stop_reason: None,
stop_sequence: None,
container: None,
usage: crate::models::Usage::default(),
})
}
async fn create_message_stream(
&self,
_request: MessageRequest,
) -> anyhow::Result<crate::llm_client::StreamEventBox> {
anyhow::bail!("streaming is unused by compaction")
}
async fn health_check(&self) -> anyhow::Result<bool> {
Ok(true)
}
}
#[async_trait::async_trait]
impl crate::core::model_client::ModelClient for FixedSummaryClient {
fn provider_name(&self) -> &str {
@@ -1685,6 +1852,185 @@ mod tests {
assert_eq!(user_text_of(&retained[2]).as_deref(), Some(text.as_str()));
}
#[test]
fn summary_quality_gate_rejects_empty_and_known_placeholder_text() {
for summary in [
"",
" \n\t ",
"...",
"。。。",
"🫧",
"N/A",
"(no summary available)",
"I cannot provide a summary.",
] {
let error = validate_compaction_summary(summary)
.expect_err("degenerate summary must fail closed");
assert!(error.to_string().contains("unusable"), "{error}");
}
validate_compaction_summary(FIXED_SUMMARY)
.expect("a substantive continuation handoff must be accepted");
validate_compaction_summary(
"目的: #4394の空要約を防止。完了: 検証と実装。制約: 履歴を変更しない。次: テスト実行。",
)
.expect("a concise multilingual handoff must not be rejected by prose length");
}
#[tokio::test]
async fn empty_successful_summary_retries_once_without_replacing_history() {
let original = vec![
msg(
"user",
"Keep the migration transactional and preserve existing sessions.",
),
msg(
"assistant",
"I am updating the session store and its fixtures.",
),
];
let client = ScriptedSummaryClient::new(vec![
vec![ContentBlock::Text {
text: " \n\t ".to_string(),
cache_control: None,
}],
vec![ContentBlock::Text {
text: FIXED_SUMMARY.to_string(),
cache_control: None,
}],
]);
let config = CompactionConfig {
model: "test-model".to_string(),
cache_summary: false,
..Default::default()
};
let result = compact_messages_safe(&client, &original, None, &prepared(&config))
.await
.expect("the conservative retry should recover a usable summary");
let requests = client
.requests
.lock()
.expect("read scripted summary requests");
assert_eq!(requests.len(), 2, "quality failure retries exactly once");
let ContentBlock::Text { text, .. } = &requests[1]
.messages
.last()
.expect("retry instruction")
.content[0]
else {
panic!("retry instruction must be text");
};
assert!(text.contains("previous handoff response was empty"));
drop(requests);
assert_eq!(
result.retries_used, 1,
"quality retry must reach diagnostics"
);
assert_eq!(original[0].role, "user", "source history remains untouched");
assert!(result.messages.iter().any(is_compaction_checkpoint_message));
let Some(SystemPrompt::Blocks(blocks)) = result.summary_prompt else {
panic!("recovered summary must be committed");
};
assert!(blocks[0].text.contains(FIXED_SUMMARY));
assert!(!blocks[0].text.contains("(no summary available)"));
}
#[tokio::test]
async fn quality_retry_count_survives_a_later_transient_failure() {
let client = ScriptedSummaryClient::with_outcomes(vec![
Ok(vec![ContentBlock::Text {
text: "...".to_string(),
cache_control: None,
}]),
Err(anyhow::anyhow!("request timed out")),
Ok(vec![ContentBlock::Text {
text: FIXED_SUMMARY.to_string(),
cache_control: None,
}]),
]);
let config = CompactionConfig {
model: "test-model".to_string(),
cache_summary: false,
..Default::default()
};
let result = compact_messages_safe(
&client,
&[msg("user", "Preserve the current migration state.")],
None,
&prepared(&config),
)
.await
.expect("the outer retry should recover after the transient failure");
assert_eq!(
result.retries_used, 2,
"one quality retry plus one outer transient retry must be reported"
);
assert_eq!(
client
.requests
.lock()
.expect("read scripted summary requests")
.len(),
3,
"the diagnostic count must match the two calls after the initial request"
);
}
#[tokio::test]
async fn non_text_summary_failure_preserves_history_after_one_retry() {
let original = vec![
msg(
"user",
"Do not lose the current branch or the failing test name.",
),
msg("assistant", "The failing test is session_store::roundtrip."),
];
let client = ScriptedSummaryClient::new(vec![
vec![ContentBlock::thinking("internal-only response")],
vec![ContentBlock::thinking("still no user-visible handoff")],
]);
let config = CompactionConfig {
model: "test-model".to_string(),
cache_summary: false,
..Default::default()
};
let error = compact_messages_safe(&client, &original, None, &prepared(&config))
.await
.expect_err("two non-text responses must not replace history");
assert!(
error
.to_string()
.contains("remained unusable after one conservative retry"),
"{error}"
);
assert_eq!(
client
.requests
.lock()
.expect("read scripted summary requests")
.len(),
2,
"quality failure gets one retry, not the transient retry ladder"
);
assert_eq!(
original,
vec![
msg(
"user",
"Do not lose the current branch or the failing test name."
),
msg("assistant", "The failing test is session_store::roundtrip."),
],
"borrowed source history must remain byte-for-byte unchanged"
);
}
#[tokio::test]
async fn compaction_uses_the_resolved_route_output_allowance() {
for (route_label, provider, model) in [
+311 -30
View File
@@ -69,6 +69,7 @@ pub enum ApiProvider {
Sglang,
Vllm,
Ollama,
OllamaCloud,
Huggingface,
Together,
Qianfan,
@@ -92,12 +93,15 @@ pub enum ApiProvider {
/// backend, not an OpenAI alias: thought signatures on tool calls are
/// captured and replayed per Google's contract.
Google,
/// Google Antigravity (`agy`). Credential plane only: consent-gated
/// read-only import of the official CLI's login. Sends fail closed
/// until the cloud-code wire protocol is implemented.
/// Google Antigravity (`agy`). Consent-gated read-only import of the
/// official CLI's login, then a text-only cloud-code stream
/// (`/v1internal:streamGenerateContent`). Tools and non-text parts
/// fail closed.
Antigravity,
/// Jiangsu Telecom TokenHub — OpenAI-compatible AI gateway.
Telecomjs,
/// Eden AI — OpenAI-compatible AI gateway (aggregator).
Edenai,
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
ModelstudioTokenPlan,
/// Alibaba Cloud Model Studio — Token Plan Anthropic-compatible endpoint.
@@ -130,6 +134,11 @@ pub(crate) struct ProviderIdentity {
/// root-level `provider = "custom"` route and must never be upgraded to an
/// exact `[providers.custom]` table merely because one exists later.
pub(crate) exact_id: Option<String>,
/// Runtime provenance for the released `ollama` + exact Cloud route.
/// Persistence writes the canonical Cloud kind plus the original `ollama`
/// id, then reconstructs this flag on resume; the flag itself is not
/// serialized.
pub(crate) migrated_legacy_ollama_cloud_route: bool,
}
impl ProviderIdentity {
@@ -325,7 +334,7 @@ impl ApiProvider {
/// `ApiProvider` discriminant → `ProviderKind` lookup.
/// Index 1 is `None` for the legacy `DeepseekCN` variant.
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 46] = [
const KIND_LOOKUP: [Option<codewhale_config::ProviderKind>; 48] = [
Some(codewhale_config::ProviderKind::Deepseek),
None, // DeepseekCN
Some(codewhale_config::ProviderKind::DeepseekAnthropic),
@@ -346,6 +355,7 @@ impl ApiProvider {
Some(codewhale_config::ProviderKind::Sglang),
Some(codewhale_config::ProviderKind::Vllm),
Some(codewhale_config::ProviderKind::Ollama),
Some(codewhale_config::ProviderKind::OllamaCloud),
Some(codewhale_config::ProviderKind::Huggingface),
Some(codewhale_config::ProviderKind::Together),
Some(codewhale_config::ProviderKind::Qianfan),
@@ -367,6 +377,7 @@ impl ApiProvider {
Some(codewhale_config::ProviderKind::Google),
Some(codewhale_config::ProviderKind::Antigravity),
Some(codewhale_config::ProviderKind::Telecomjs),
Some(codewhale_config::ProviderKind::Edenai),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlan),
Some(codewhale_config::ProviderKind::ModelstudioTokenPlanAnthropic),
Some(codewhale_config::ProviderKind::ModelstudioCodingPlan),
@@ -375,7 +386,7 @@ impl ApiProvider {
];
/// `ProviderKind` discriminant → `ApiProvider` lookup.
const FROM_KIND_LOOKUP: [Self; 45] = [
const FROM_KIND_LOOKUP: [Self; 47] = [
Self::Deepseek,
Self::DeepseekAnthropic,
Self::NvidiaNim,
@@ -395,6 +406,7 @@ impl ApiProvider {
Self::Sglang,
Self::Vllm,
Self::Ollama,
Self::OllamaCloud,
Self::Huggingface,
Self::Together,
Self::Qianfan,
@@ -420,6 +432,7 @@ impl ApiProvider {
Self::ModelstudioCodingPlanAnthropic,
Self::Antigravity,
Self::Google,
Self::Edenai,
Self::Custom,
];
@@ -487,6 +500,7 @@ fn subagent_provider_key_matches(key: &str, provider: ApiProvider) -> bool {
),
ApiProvider::Openrouter => matches!(normalized.as_str(), "openrouter" | "open_router"),
ApiProvider::Orcarouter => matches!(normalized.as_str(), "orcarouter" | "orca_router"),
ApiProvider::Edenai => matches!(normalized.as_str(), "edenai" | "eden_ai"),
ApiProvider::OpenaiCodex => matches!(
normalized.as_str(),
"openai_codex" | "codex" | "chatgpt" | "openai_chatgpt"
@@ -1164,10 +1178,10 @@ fn canonical_zai_model_id(model: &str) -> Option<&'static str> {
let normalized = normalized.replace(['_', ' '], "-");
match normalized.as_str() {
"glm-5.1" | "glm-5-1" | "zai-glm-5.1" | "zai-glm-5-1" => Some(ZAI_GLM_5_1_MODEL),
"glm-5.2" | "glm-5-2" | "zai-glm-5.2" | "zai-glm-5-2" => Some(DEFAULT_ZAI_MODEL),
// Resolves to its own constant, never to `DEFAULT_ZAI_MODEL`: adding a
// model must not silently re-point an explicit 5.3 request at the
// default (GLM-5.2).
// Each alias resolves to its own constant, never through
// `DEFAULT_ZAI_MODEL`: moving the default (now GLM-5.3) must not
// silently re-point an explicit GLM-5.2 request.
"glm-5.2" | "glm-5-2" | "zai-glm-5.2" | "zai-glm-5-2" => Some(ZAI_GLM_5_2_MODEL),
"glm-5.3" | "glm-5-3" | "zai-glm-5.3" | "zai-glm-5-3" => Some(ZAI_GLM_5_3_MODEL),
"glm-5-turbo" | "glm-5turbo" | "zai-glm-5-turbo" => Some(ZAI_GLM_5_TURBO_MODEL),
_ => None,
@@ -1476,7 +1490,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::Sglang => vec![DEFAULT_SGLANG_MODEL, DEFAULT_SGLANG_FLASH_MODEL],
ApiProvider::Vllm => vec![DEFAULT_VLLM_MODEL, DEFAULT_VLLM_FLASH_MODEL],
ApiProvider::Volcengine => vec![DEFAULT_VOLCENGINE_MODEL, DEFAULT_VOLCENGINE_FLASH_MODEL],
ApiProvider::Ollama => Vec::new(),
ApiProvider::Ollama | ApiProvider::OllamaCloud => Vec::new(),
ApiProvider::Openai | ApiProvider::Atlascloud => OFFICIAL_DEEPSEEK_MODELS.to_vec(),
ApiProvider::Together => vec![DEFAULT_TOGETHER_MODEL, DEFAULT_TOGETHER_FLASH_MODEL],
ApiProvider::Qianfan => vec![DEFAULT_QIANFAN_MODEL],
@@ -1484,7 +1498,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
ApiProvider::Openmodel => vec![DEFAULT_OPENMODEL_MODEL],
ApiProvider::Zai => vec![
DEFAULT_ZAI_MODEL,
ZAI_GLM_5_3_MODEL,
ZAI_GLM_5_2_MODEL,
ZAI_GLM_5_1_MODEL,
ZAI_GLM_5_TURBO_MODEL,
],
@@ -1573,6 +1587,7 @@ pub fn model_completion_names_for_provider(provider: ApiProvider) -> Vec<&'stati
// The cloud-code wire protocol is not implemented; no model is
// advertised for the credential-import-only route.
ApiProvider::Antigravity => Vec::new(),
ApiProvider::Edenai => vec![DEFAULT_EDENAI_MODEL],
// Custom endpoints expose no built-in completion names; the user
// supplies their own model id (#1519).
ApiProvider::Custom => Vec::new(),
@@ -2084,6 +2099,9 @@ pub enum StatusItem {
Tokens,
/// DeepSeek account balance, refreshed once per turn completion.
Balance,
/// Session metrics strip: turns · steps │ LLM · tools │ TTFT · tok/s │
/// cache │ in — sourced from engine timings and provider usage.
SessionMetrics,
}
impl StatusItem {
@@ -2103,6 +2121,7 @@ impl StatusItem {
StatusItem::Cache,
StatusItem::GitBranch,
StatusItem::Tokens,
StatusItem::SessionMetrics,
]
}
@@ -2124,6 +2143,7 @@ impl StatusItem {
StatusItem::RateLimit => "rate_limit",
StatusItem::Tokens => "tokens",
StatusItem::Balance => "balance",
StatusItem::SessionMetrics => "session_metrics",
}
}
@@ -2147,6 +2167,7 @@ impl StatusItem {
"rate_limit" => Some(Self::RateLimit),
"tokens" => Some(Self::Tokens),
"balance" => Some(Self::Balance),
"session_metrics" => Some(Self::SessionMetrics),
_ => None,
}
}
@@ -2169,6 +2190,7 @@ impl StatusItem {
StatusItem::RateLimit => "Rate-limit remaining",
StatusItem::Tokens => "Session tokens",
StatusItem::Balance => "Account balance",
StatusItem::SessionMetrics => "Session metrics",
}
}
@@ -2191,6 +2213,7 @@ impl StatusItem {
StatusItem::RateLimit => "remaining requests in the budget (reserved)",
StatusItem::Tokens => "input / cache-hit / output token totals",
StatusItem::Balance => "topped-up + granted balance from DeepSeek",
StatusItem::SessionMetrics => "turns · steps · LLM/tool time · TTFT · tok/s · input",
}
}
@@ -2212,6 +2235,7 @@ impl StatusItem {
StatusItem::LastToolElapsed,
StatusItem::RateLimit,
StatusItem::Tokens,
StatusItem::SessionMetrics,
]
}
@@ -2558,6 +2582,15 @@ pub struct Config {
/// model changed without persisting compatibility state back to config.
#[serde(skip)]
pub(crate) migrated_deepseek_model_alias: Option<String>,
/// Runtime-only receipt that the released `ollama` + exact
/// `https://ollama.com/v1` tuple was upgraded to `ollama-cloud` in memory.
///
/// This survives route-scoped config clones so old provider-table and
/// secret-slot reads remain available to that exact migrated route. It is
/// never serialized and is never set for an explicit `ollama-cloud`
/// selection.
#[serde(skip)]
pub(crate) migrated_legacy_ollama_cloud_route: bool,
/// Native tool catalog controls. This table controls built-in
/// tool loading policy.
#[serde(default)]
@@ -3381,6 +3414,8 @@ pub struct ProvidersConfig {
pub vllm: ProviderConfig,
#[serde(default)]
pub ollama: ProviderConfig,
#[serde(default, alias = "ollama-cloud", alias = "ollamaCloud")]
pub ollama_cloud: ProviderConfig,
#[serde(default, alias = "hugging-face", alias = "hf")]
pub huggingface: ProviderConfig,
#[serde(default, alias = "deep-infra", alias = "deep_infra")]
@@ -3483,6 +3518,9 @@ pub struct ProvidersConfig {
alias = "tokenhub"
)]
pub telecomjs: ProviderConfig,
/// Eden AI — OpenAI-compatible AI gateway (aggregator).
#[serde(default, alias = "eden-ai", alias = "eden_ai")]
pub edenai: ProviderConfig,
/// Alibaba Cloud Model Studio — Token Plan (OpenAI-compatible Chat Completions).
#[serde(default, alias = "modelstudio-token-plan")]
pub modelstudio_token_plan: ProviderConfig,
@@ -3534,6 +3572,7 @@ impl ProvidersConfig {
("providers.sglang", &self.sglang),
("providers.vllm", &self.vllm),
("providers.ollama", &self.ollama),
("providers.ollama_cloud", &self.ollama_cloud),
("providers.huggingface", &self.huggingface),
("providers.deepinfra", &self.deepinfra),
("providers.together", &self.together),
@@ -4436,6 +4475,9 @@ impl Config {
return ApiProvider::Custom;
}
if let Some(provider) = self.provider.as_deref().and_then(ApiProvider::parse) {
if provider == ApiProvider::Ollama && self.selects_legacy_ollama_cloud_route() {
return ApiProvider::OllamaCloud;
}
return provider;
}
self.base_url
@@ -4451,6 +4493,38 @@ impl Config {
.unwrap_or(ApiProvider::Deepseek)
}
/// Whether the live config uses the released route-sensitive Ollama Cloud
/// shape. This is a pure in-memory compatibility check: no config or
/// secret state is rewritten, and only the exact official `/v1` endpoint
/// upgrades from `ollama` to `ollama-cloud`.
fn selects_legacy_ollama_cloud_route(&self) -> bool {
if self.migrated_legacy_ollama_cloud_route {
return true;
}
if self.provider.as_deref().and_then(ApiProvider::parse) != Some(ApiProvider::Ollama) {
return false;
}
self.legacy_ollama_cloud_route_configured()
}
/// Whether the legacy Ollama table itself names the exact hosted route,
/// independent of which provider the parent session currently selects.
/// Fleet and subagent pins need this route-scoped form.
fn legacy_ollama_cloud_route_configured(&self) -> bool {
let base_url = self
.providers
.as_ref()
.and_then(|providers| providers.ollama.base_url.as_deref())
.map(str::to_string)
.or_else(|| first_nonempty_env(&["OLLAMA_BASE_URL"]));
base_url.is_some_and(|base_url| {
codewhale_config::provider::migrates_legacy_ollama_cloud_route(
codewhale_config::ProviderKind::Ollama,
&base_url,
)
})
}
/// Return the exact non-secret key for an active provider route.
#[must_use]
pub(crate) fn provider_identity_for(&self, provider: ApiProvider) -> String {
@@ -4478,6 +4552,14 @@ impl Config {
&self,
provider: ApiProvider,
) -> std::result::Result<ProviderIdentity, String> {
if provider == ApiProvider::OllamaCloud
&& (self.migrated_legacy_ollama_cloud_route
|| self.provider.as_deref().and_then(ApiProvider::parse)
== Some(ApiProvider::Ollama))
&& self.legacy_ollama_cloud_route_configured()
{
return self.resolve_provider_identity(ApiProvider::Ollama.as_str());
}
self.resolve_provider_identity(&self.provider_identity_for(provider))
}
@@ -4508,13 +4590,23 @@ impl Config {
.is_some();
if !has_exact_custom_table
&& let Some(provider) = ApiProvider::parse(key)
&& let Some(mut provider) = ApiProvider::parse(key)
&& provider != ApiProvider::Custom
{
let migrated_legacy_ollama_cloud_route =
provider == ApiProvider::Ollama && self.legacy_ollama_cloud_route_configured();
if provider == ApiProvider::Ollama && migrated_legacy_ollama_cloud_route {
provider = ApiProvider::OllamaCloud;
}
return Ok(ProviderIdentity {
provider,
key: provider.as_str().to_string(),
exact_id: Some(provider.as_str().to_string()),
exact_id: Some(if migrated_legacy_ollama_cloud_route {
ApiProvider::Ollama.as_str().to_string()
} else {
provider.as_str().to_string()
}),
migrated_legacy_ollama_cloud_route,
});
}
@@ -4532,6 +4624,7 @@ impl Config {
provider: ApiProvider::Custom,
key: ApiProvider::Custom.as_str().to_string(),
exact_id: None,
migrated_legacy_ollama_cloud_route: false,
});
}
}
@@ -4616,9 +4709,32 @@ impl Config {
provider: ApiProvider::Custom,
key: exact_key.to_string(),
exact_id: Some(exact_key.to_string()),
migrated_legacy_ollama_cloud_route: false,
})
}
/// Resolve a provider explicitly pinned by a current Fleet/subagent
/// declaration.
///
/// A scoped legacy Ollama Cloud config retains its migration marker so the
/// active client can keep reading `[providers.ollama]` and the old secret
/// slot. That marker is provenance for the active route, not an alias for a
/// newly declared `ollama-cloud` pin: the explicit pin must bind the
/// first-class table and credential slot even when it is declared by a
/// child of the migrated route.
pub(crate) fn resolve_provider_pin_identity(
&self,
provider_id: &str,
) -> std::result::Result<ProviderIdentity, String> {
let mut identity = self.resolve_provider_identity(provider_id)?;
if identity.provider == ApiProvider::OllamaCloud
&& ApiProvider::parse(provider_id.trim()) == Some(ApiProvider::OllamaCloud)
{
identity.migrated_legacy_ollama_cloud_route = false;
}
Ok(identity)
}
/// Resolve an additive exact provider id. Unlike raw selector resolution,
/// this never interprets the literal id `custom` as the legacy root route:
/// an id means the record requires that exact `[providers.<id>]` table.
@@ -4689,7 +4805,7 @@ impl Config {
);
};
let Some(provider) = ApiProvider::parse(kind) else {
let Some(mut provider) = ApiProvider::parse(kind) else {
// Pre-additive releases sometimes wrote an exact named custom key
// into `model_provider`. Preserve that shape, but reject a
// contradictory additive id instead of silently choosing one.
@@ -4705,6 +4821,14 @@ impl Config {
None => self.resolve_provider_identity(kind),
};
};
let migrated_legacy_ollama_cloud = (provider == ApiProvider::Ollama
&& self.legacy_ollama_cloud_route_configured())
|| (provider == ApiProvider::OllamaCloud
&& id.and_then(ApiProvider::parse) == Some(ApiProvider::Ollama)
&& self.legacy_ollama_cloud_route_configured());
if migrated_legacy_ollama_cloud {
provider = ApiProvider::OllamaCloud;
}
if provider == ApiProvider::Custom {
if let Some(id) = id {
@@ -4726,11 +4850,14 @@ impl Config {
provider: ApiProvider::Custom,
key: ApiProvider::Custom.as_str().to_string(),
exact_id: None,
migrated_legacy_ollama_cloud_route: false,
});
}
if let Some(id) = id
&& ApiProvider::parse(id) != Some(provider)
&& !(migrated_legacy_ollama_cloud
&& ApiProvider::parse(id) == Some(ApiProvider::Ollama))
{
return Err(format!(
"persisted provider route declares built-in kind '{}' but exact provider id '{id}' names a different route; repair the mismatched fields because Codewhale will not guess or fall back",
@@ -4759,7 +4886,12 @@ impl Config {
Ok(ProviderIdentity {
provider,
key: provider.as_str().to_string(),
exact_id: Some(provider.as_str().to_string()),
exact_id: Some(if migrated_legacy_ollama_cloud {
ApiProvider::Ollama.as_str().to_string()
} else {
provider.as_str().to_string()
}),
migrated_legacy_ollama_cloud_route: migrated_legacy_ollama_cloud,
})
}
@@ -4769,6 +4901,7 @@ impl Config {
/// otherwise capture the table. Removing it from the scoped clone keeps
/// the root endpoint authoritative without mutating the live registry.
pub(crate) fn scope_to_provider_identity(&mut self, identity: &ProviderIdentity) {
self.migrated_legacy_ollama_cloud_route = identity.migrated_legacy_ollama_cloud_route;
self.provider = Some(identity.key.clone());
if identity.provider == ApiProvider::Custom
&& identity.persisted_id().is_none()
@@ -4913,6 +5046,10 @@ impl Config {
ApiProvider::Sglang => &providers.sglang,
ApiProvider::Vllm => &providers.vllm,
ApiProvider::Ollama => &providers.ollama,
ApiProvider::OllamaCloud if self.selects_legacy_ollama_cloud_route() => {
&providers.ollama
}
ApiProvider::OllamaCloud => &providers.ollama_cloud,
ApiProvider::Volcengine => &providers.volcengine,
ApiProvider::Huggingface => &providers.huggingface,
ApiProvider::Deepinfra => &providers.deepinfra,
@@ -4935,6 +5072,7 @@ impl Config {
ApiProvider::Google => &providers.google,
ApiProvider::Antigravity => &providers.antigravity,
ApiProvider::Telecomjs => &providers.telecomjs,
ApiProvider::Edenai => &providers.edenai,
ApiProvider::ModelstudioTokenPlan => &providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
&providers.modelstudio_token_plan_anthropic
@@ -4968,6 +5106,7 @@ impl Config {
.clone()
.unwrap_or_else(|| "__custom__".to_string())
});
let legacy_ollama_cloud = self.selects_legacy_ollama_cloud_route();
let providers = self.providers.get_or_insert_with(ProvidersConfig::default);
if let Some(key) = custom_key {
return providers.custom.entry(key).or_default();
@@ -4992,6 +5131,8 @@ impl Config {
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
ApiProvider::Ollama => &mut providers.ollama,
ApiProvider::OllamaCloud if legacy_ollama_cloud => &mut providers.ollama,
ApiProvider::OllamaCloud => &mut providers.ollama_cloud,
ApiProvider::Volcengine => &mut providers.volcengine,
ApiProvider::Huggingface => &mut providers.huggingface,
ApiProvider::Deepinfra => &mut providers.deepinfra,
@@ -5014,6 +5155,7 @@ impl Config {
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::Edenai => &mut providers.edenai,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
&mut providers.modelstudio_token_plan_anthropic
@@ -5345,6 +5487,7 @@ impl Config {
ApiProvider::Sglang => DEFAULT_SGLANG_MODEL,
ApiProvider::Vllm => DEFAULT_VLLM_MODEL,
ApiProvider::Ollama => DEFAULT_OLLAMA_MODEL,
ApiProvider::OllamaCloud => DEFAULT_OLLAMA_CLOUD_MODEL,
ApiProvider::Volcengine => DEFAULT_VOLCENGINE_MODEL,
ApiProvider::Huggingface => DEFAULT_HUGGINGFACE_MODEL,
ApiProvider::Deepinfra => DEFAULT_DEEPINFRA_MODEL,
@@ -5378,6 +5521,7 @@ impl Config {
ApiProvider::Google => DEFAULT_GOOGLE_MODEL,
ApiProvider::Antigravity => DEFAULT_ANTIGRAVITY_MODEL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_MODEL,
ApiProvider::Edenai => DEFAULT_EDENAI_MODEL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
@@ -5480,6 +5624,7 @@ impl Config {
| ApiProvider::Sglang
| ApiProvider::Vllm
| ApiProvider::Ollama
| ApiProvider::OllamaCloud
| ApiProvider::Volcengine
| ApiProvider::Huggingface
| ApiProvider::Deepinfra
@@ -5499,6 +5644,7 @@ impl Config {
| ApiProvider::Google
| ApiProvider::Antigravity
| ApiProvider::Telecomjs
| ApiProvider::Edenai
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
@@ -5585,6 +5731,7 @@ impl Config {
ApiProvider::Sglang => DEFAULT_SGLANG_BASE_URL,
ApiProvider::Vllm => DEFAULT_VLLM_BASE_URL,
ApiProvider::Ollama => DEFAULT_OLLAMA_BASE_URL,
ApiProvider::OllamaCloud => DEFAULT_OLLAMA_CLOUD_BASE_URL,
ApiProvider::Volcengine => DEFAULT_VOLCENGINE_BASE_URL,
ApiProvider::Huggingface => DEFAULT_HUGGINGFACE_BASE_URL,
ApiProvider::Deepinfra => DEFAULT_DEEPINFRA_BASE_URL,
@@ -5607,6 +5754,7 @@ impl Config {
ApiProvider::Google => DEFAULT_GOOGLE_BASE_URL,
ApiProvider::Antigravity => DEFAULT_ANTIGRAVITY_BASE_URL,
ApiProvider::Telecomjs => DEFAULT_TELECOMJS_BASE_URL,
ApiProvider::Edenai => DEFAULT_EDENAI_BASE_URL,
ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
@@ -5902,7 +6050,7 @@ impl Config {
return false;
}
provider.is_self_hosted()
provider_route_is_keyless_self_hosted(provider, &self.base_url_for_route(provider))
|| (provider == self.api_provider()
&& base_url_uses_local_host(&self.deepseek_base_url()))
}
@@ -6151,9 +6299,8 @@ impl Config {
// Official Antigravity (`agy`) login. `ANTIGRAVITY_API_KEY` config
// and env slots were already checked above; here the process's own
// `AGY_ADC_AUTH` wins over the consented `state.vscdb`, which is
// imported read-only from the one pinned path. The token only ever
// reaches the credential plane — sends still fail closed in the
// client because the cloud-code wire protocol is unimplemented.
// imported read-only from the one pinned path. The token is then
// used on the cloud-code stream; it is never logged.
if provider == ApiProvider::Antigravity && !custom_endpoint {
let grant = self
.external_credential_read_grant(
@@ -6178,14 +6325,15 @@ impl Config {
tracing::debug!(
target: "config",
source = other.source_label(),
"antigravity credential plane resolved; sends remain fail-closed"
"antigravity credential plane did not yield a sendable token"
);
}
}
}
if !auth_mode_requires_api_key(auth_mode.as_deref())
&& (provider.is_self_hosted() || base_url_uses_local_host(&self.deepseek_base_url()))
&& (provider_route_is_keyless_self_hosted(provider, &self.deepseek_base_url())
|| base_url_uses_local_host(&self.deepseek_base_url()))
{
return Ok(String::new());
}
@@ -6279,7 +6427,20 @@ impl Config {
}
// Self-hosted deployments commonly run without auth on localhost.
// Return an empty key and let the client omit the Authorization header.
ApiProvider::Sglang | ApiProvider::Vllm | ApiProvider::Ollama => Ok(String::new()),
ApiProvider::Sglang | ApiProvider::Vllm => Ok(String::new()),
ApiProvider::Ollama
if provider_route_is_keyless_self_hosted(provider, &self.deepseek_base_url()) =>
{
Ok(String::new())
}
ApiProvider::Ollama => {
let help = credential_help_for_provider_route(provider, &self.deepseek_base_url());
anyhow::bail!(
"Ollama Cloud API key not found. Get a key: {}. Run 'codewhale auth set --provider ollama', set OLLAMA_API_KEY, or add [providers.ollama] api_key in ~/.codewhale/config.toml.",
help.credential_url
.unwrap_or(codewhale_config::provider::OLLAMA_CLOUD_API_KEY_URL)
)
}
// Custom OpenAI-compatible endpoints (#1519): the key comes from the
// env var named by `[providers.<name>] api_key_env`. If we reached
// here it is unset/empty (and the endpoint is not loopback).
@@ -7208,6 +7369,7 @@ fn provider_env_base_url_override(provider: ApiProvider) -> Option<String> {
ApiProvider::Sglang => &["SGLANG_BASE_URL"],
ApiProvider::Vllm => &["VLLM_BASE_URL"],
ApiProvider::Ollama => &["OLLAMA_BASE_URL"],
ApiProvider::OllamaCloud => &["OLLAMA_CLOUD_BASE_URL"],
ApiProvider::Huggingface => &["HUGGINGFACE_BASE_URL", "HF_BASE_URL"],
ApiProvider::Meta => &["META_MODEL_API_BASE_URL", "MODEL_API_BASE_URL"],
ApiProvider::Xai => &["XAI_BASE_URL"],
@@ -7215,6 +7377,7 @@ fn provider_env_base_url_override(provider: ApiProvider) -> Option<String> {
ApiProvider::Google => &["GOOGLE_BASE_URL", "GEMINI_BASE_URL"],
ApiProvider::Antigravity => &["ANTIGRAVITY_BASE_URL"],
ApiProvider::Telecomjs => &["TELECOMJS_BASE_URL"],
ApiProvider::Edenai => &["EDENAI_BASE_URL"],
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic => {
&["MODELSTUDIO_TOKEN_PLAN_BASE_URL"]
}
@@ -7289,7 +7452,9 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
config.provider = Some(value);
}
let active_base_url_from_env = env_base_url_override().is_some()
|| provider_env_base_url_override(config.api_provider()).is_some();
|| provider_env_base_url_override(config.api_provider()).is_some()
|| (config.selects_legacy_ollama_cloud_route()
&& first_nonempty_env(&["OLLAMA_BASE_URL"]).is_some());
if let Ok(value) = codewhale_env_var("CODEWHALE_BASE_URL", "DEEPSEEK_BASE_URL") {
match config.api_provider() {
ApiProvider::Deepseek | ApiProvider::DeepseekCN => {
@@ -7432,6 +7597,13 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.ollama
.base_url = Some(value);
}
ApiProvider::OllamaCloud => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.ollama_cloud
.base_url = Some(value);
}
ApiProvider::Volcengine => {
config
.providers
@@ -7579,6 +7751,13 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.telecomjs
.base_url = Some(value);
}
ApiProvider::Edenai => {
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.edenai
.base_url = Some(value);
}
ApiProvider::ModelstudioTokenPlan => {
config
.providers
@@ -7826,6 +8005,16 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.telecomjs
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Edenai)
&& let Ok(value) = std::env::var("EDENAI_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.edenai
.base_url = Some(value);
}
if matches!(
config.api_provider(),
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic
@@ -7914,6 +8103,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
ApiProvider::Ollama => &mut providers.ollama,
ApiProvider::OllamaCloud => &mut providers.ollama_cloud,
ApiProvider::Volcengine => &mut providers.volcengine,
ApiProvider::Huggingface => &mut providers.huggingface,
ApiProvider::Deepinfra => &mut providers.deepinfra,
@@ -7936,6 +8126,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::Edenai => &mut providers.edenai,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
&mut providers.modelstudio_token_plan_anthropic
@@ -7954,7 +8145,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
entry.http_headers = Some(provider_headers);
}
}
if matches!(config.api_provider(), ApiProvider::Ollama)
if config.provider.as_deref().and_then(ApiProvider::parse) == Some(ApiProvider::Ollama)
&& let Ok(value) = std::env::var("OLLAMA_BASE_URL")
&& !value.trim().is_empty()
{
@@ -7964,6 +8155,17 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.ollama
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::OllamaCloud)
&& config.provider.as_deref().and_then(ApiProvider::parse) == Some(ApiProvider::OllamaCloud)
&& let Ok(value) = std::env::var("OLLAMA_CLOUD_BASE_URL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.ollama_cloud
.base_url = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Sglang)
&& let Ok(value) = std::env::var("SGLANG_MODEL")
{
@@ -7974,8 +8176,15 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
{
config.default_text_model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Ollama)
&& let Ok(value) = std::env::var("OLLAMA_MODEL")
if matches!(
config.api_provider(),
ApiProvider::Ollama | ApiProvider::OllamaCloud
) && let Ok(value) = std::env::var("OLLAMA_MODEL")
{
config.default_text_model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::OllamaCloud)
&& let Ok(value) = std::env::var("OLLAMA_CLOUD_MODEL")
{
config.default_text_model = Some(value);
}
@@ -8148,6 +8357,16 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
.telecomjs
.model = Some(value);
}
if matches!(config.api_provider(), ApiProvider::Edenai)
&& let Ok(value) = std::env::var("EDENAI_MODEL")
&& !value.trim().is_empty()
{
config
.providers
.get_or_insert_with(ProvidersConfig::default)
.edenai
.model = Some(value);
}
if matches!(
config.api_provider(),
ApiProvider::ModelstudioTokenPlan | ApiProvider::ModelstudioTokenPlanAnthropic
@@ -8261,6 +8480,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Sglang => &mut providers.sglang,
ApiProvider::Vllm => &mut providers.vllm,
ApiProvider::Ollama => &mut providers.ollama,
ApiProvider::OllamaCloud => &mut providers.ollama_cloud,
ApiProvider::Volcengine => &mut providers.volcengine,
ApiProvider::Huggingface => &mut providers.huggingface,
ApiProvider::Deepinfra => &mut providers.deepinfra,
@@ -8283,6 +8503,7 @@ fn apply_env_overrides_unlocked(config: &mut Config, policy: ConfigEnvironmentPo
ApiProvider::Google => &mut providers.google,
ApiProvider::Antigravity => &mut providers.antigravity,
ApiProvider::Telecomjs => &mut providers.telecomjs,
ApiProvider::Edenai => &mut providers.edenai,
ApiProvider::ModelstudioTokenPlan => &mut providers.modelstudio_token_plan,
ApiProvider::ModelstudioTokenPlanAnthropic => {
&mut providers.modelstudio_token_plan_anthropic
@@ -8578,10 +8799,12 @@ pub(crate) fn provider_passes_model_through(provider: ApiProvider) -> bool {
| ApiProvider::Qianfan
| ApiProvider::Openmodel
| ApiProvider::Ollama
| ApiProvider::OllamaCloud
| ApiProvider::Huggingface
| ApiProvider::Meta
| ApiProvider::Xai
| ApiProvider::Telecomjs
| ApiProvider::Edenai
| ApiProvider::ModelstudioTokenPlan
| ApiProvider::ModelstudioTokenPlanAnthropic
| ApiProvider::ModelstudioCodingPlan
@@ -8830,6 +9053,20 @@ fn base_url_is_custom_for_provider(provider: ApiProvider, base_url: &str) -> boo
codewhale_config::provider_preserves_custom_base_url_model(kind, base_url)
}
/// Whether this concrete route is a self-hosted endpoint whose credentials
/// are optional by default.
///
/// Ollama is local; the released exact `ollama` + `https://ollama.com/v1`
/// tuple is upgraded to `OllamaCloud` before this helper runs. Cloud is never
/// self-hosted, while neighboring remote Ollama URLs remain custom and are
/// rejected before they can inherit ambient or saved credentials.
pub(crate) fn provider_route_is_keyless_self_hosted(provider: ApiProvider, base_url: &str) -> bool {
if provider == ApiProvider::Ollama {
return base_url_uses_local_host(base_url);
}
provider.is_self_hosted()
}
fn provider_preserves_custom_base_url_model(provider: ApiProvider, base_url: &str) -> bool {
base_url_is_custom_for_provider(provider, base_url)
}
@@ -9422,6 +9659,8 @@ fn merge_config(base: Config, override_cfg: Config) -> Config {
reasoning_effort_inferred_from_legacy_alias: override_cfg
.reasoning_effort_inferred_from_legacy_alias
|| base.reasoning_effort_inferred_from_legacy_alias,
migrated_legacy_ollama_cloud_route: override_cfg.migrated_legacy_ollama_cloud_route
|| base.migrated_legacy_ollama_cloud_route,
migrated_deepseek_model_alias: override_cfg
.migrated_deepseek_model_alias
.or(base.migrated_deepseek_model_alias),
@@ -9660,6 +9899,7 @@ fn merge_providers(
sglang: merge_provider_config(base.sglang, override_cfg.sglang),
vllm: merge_provider_config(base.vllm, override_cfg.vllm),
ollama: merge_provider_config(base.ollama, override_cfg.ollama),
ollama_cloud: merge_provider_config(base.ollama_cloud, override_cfg.ollama_cloud),
volcengine: merge_provider_config(base.volcengine, override_cfg.volcengine),
huggingface: merge_provider_config(base.huggingface, override_cfg.huggingface),
deepinfra: merge_provider_config(base.deepinfra, override_cfg.deepinfra),
@@ -9683,6 +9923,7 @@ fn merge_providers(
google: merge_provider_config(base.google, override_cfg.google),
antigravity: merge_provider_config(base.antigravity, override_cfg.antigravity),
telecomjs: merge_provider_config(base.telecomjs, override_cfg.telecomjs),
edenai: merge_provider_config(base.edenai, override_cfg.edenai),
modelstudio_token_plan: merge_provider_config(
base.modelstudio_token_plan,
override_cfg.modelstudio_token_plan,
@@ -10397,9 +10638,13 @@ fn user_global_config_api_key(provider: ApiProvider) -> Option<String> {
let text = std::fs::read_to_string(path).ok()?;
let doc: codewhale_config::ConfigToml = toml::from_str(&text).ok()?;
let json = serde_json::to_value(&doc).ok()?;
let provider_config_key = provider.metadata().map_or_else(
|| provider.as_str(),
|metadata| metadata.provider_config_key(),
);
let key = json
.get("providers")?
.get(provider.as_str())?
.get(provider_config_key)?
.get("api_key")?
.as_str()?;
let key = key.trim();
@@ -10462,6 +10707,24 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
// checks below instead of masking a configured key.
return true;
}
if provider == ApiProvider::Antigravity && !config.provider_uses_custom_endpoint(provider) {
let path = codewhale_config::default_agy_credentials_path();
if config
.external_credential_read_grant(
provider,
codewhale_config::ExternalCredentialSource::AgyCli,
&path,
)
.is_ok_and(|grant| {
crate::agy_credentials::antigravity_oauth_token_from_grant(&grant)
.ok()
.flatten()
.is_some()
})
{
return true;
}
}
if matches!(
provider,
ApiProvider::Deepseek | ApiProvider::DeepseekAnthropic
@@ -10486,7 +10749,7 @@ pub fn has_api_key_for(config: &Config, provider: ApiProvider) -> bool {
}
if !auth_mode_requires_api_key(auth_mode.as_deref())
&& (provider.is_self_hosted()
&& (provider_route_is_keyless_self_hosted(provider, &config.base_url_for_route(provider))
|| (provider == config.api_provider()
&& base_url_uses_local_host(&config.deepseek_base_url())))
{
@@ -10680,6 +10943,7 @@ pub fn save_api_key_for(provider: ApiProvider, api_key: &str) -> Result<PathBuf>
provider,
key: provider.as_str().to_string(),
exact_id: Some(provider.as_str().to_string()),
migrated_legacy_ollama_cloud_route: false,
},
&Config {
provider: Some(provider.as_str().to_string()),
@@ -11246,11 +11510,28 @@ fn provider_secret_store_api_key_with_mode(
} else {
codewhale_secrets::Secrets::auto_detect()
};
secrets
let primary = secrets
.get(provider_secret_store_slot(provider))
.ok()
.flatten()
.filter(|value| !value.trim().is_empty())
.filter(|value| !value.trim().is_empty());
if primary.is_some() {
return primary;
}
// The old local identity owned the hosted slot only when the live config
// selected the exact Ollama Cloud route. Never apply this fallback to a
// neighboring/custom endpoint or to an explicit new `ollama-cloud`
// selection, and never write/copy/delete either slot while resolving.
(provider == ApiProvider::OllamaCloud && config.selects_legacy_ollama_cloud_route())
.then(|| {
secrets
.get(ApiProvider::Ollama.as_str())
.ok()
.flatten()
.filter(|value| !value.trim().is_empty())
})
.flatten()
}
/// The shadowing warning for a config-file `api_key` that wins over a live
+11 -6
View File
@@ -132,6 +132,8 @@ pub const DEFAULT_VLLM_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
pub const DEFAULT_VLLM_BASE_URL: &str = "http://localhost:8000/v1";
pub const DEFAULT_OLLAMA_MODEL: &str = "deepseek-v4-flash";
pub const DEFAULT_OLLAMA_BASE_URL: &str = "http://localhost:11434/v1";
pub const DEFAULT_OLLAMA_CLOUD_MODEL: &str = "gpt-oss:120b";
pub const DEFAULT_OLLAMA_CLOUD_BASE_URL: &str = codewhale_config::provider::OLLAMA_CLOUD_BASE_URL;
pub const DEFAULT_HUGGINGFACE_MODEL: &str = "deepseek-ai/DeepSeek-V4-Pro";
pub const DEFAULT_HUGGINGFACE_FLASH_MODEL: &str = "deepseek-ai/DeepSeek-V4-Flash";
pub const DEFAULT_HUGGINGFACE_BASE_URL: &str = "https://router.huggingface.co/v1";
@@ -165,15 +167,16 @@ pub const COMMON_DEEPSEEK_MODELS: &[&str] = &[
"deepseek/deepseek-v4-flash",
];
pub const OFFICIAL_DEEPSEEK_MODELS: &[&str] = &["deepseek-v4-pro", "deepseek-v4-flash"];
pub const DEFAULT_ZAI_MODEL: &str = "GLM-5.2";
pub const ZAI_GLM_5_1_MODEL: &str = "GLM-5.1";
pub const ZAI_GLM_5_2_MODEL: &str = "GLM-5.2";
// GLM-5.3 is live on the Z.ai Coding Plan (2026-08-13). The id follows the
// family's naming convention. Limits and reasoning options still inherit
// from glm-5.2 until Z.ai publishes distinct 5.3 numbers; no USD price is
// GLM-5.3 is live on the Z.ai Coding Plan (2026-08-13) and is the default
// for new Z.ai routes. Limits and reasoning options still inherit from
// glm-5.2 until Z.ai publishes distinct 5.3 numbers; no USD price is
// claimed. Scope is first-party Z.ai plus its OpenRouter mirror only.
// Correct at crates/config/assets/models_dev.bundled.json
// `_meta.pending_release_metadata` when distinct 5.3 facts exist.
// Explicit GLM-5.2 selections keep their own id: only the default moved.
pub const DEFAULT_ZAI_MODEL: &str = ZAI_GLM_5_3_MODEL;
pub const ZAI_GLM_5_1_MODEL: &str = "GLM-5.1";
pub const ZAI_GLM_5_2_MODEL: &str = "GLM-5.2";
pub const ZAI_GLM_5_3_MODEL: &str = "GLM-5.3";
pub const ZAI_GLM_5_TURBO_MODEL: &str = "GLM-5-Turbo";
pub const DEFAULT_ZAI_BASE_URL: &str = "https://api.z.ai/api/coding/paas/v4";
@@ -229,6 +232,8 @@ pub const DEFAULT_GOOGLE_BASE_URL: &str =
"https://generativelanguage.googleapis.com/v1beta/openai/";
pub const DEFAULT_TELECOMJS_MODEL: &str = "deepseek-v4-pro";
pub const DEFAULT_TELECOMJS_BASE_URL: &str = "https://aigw.telecomjs.com/v1";
pub const DEFAULT_EDENAI_MODEL: &str = "deepseek/deepseek-v4-pro";
pub const DEFAULT_EDENAI_BASE_URL: &str = "https://api.edenai.run/v3";
// Alibaba Cloud Model Studio (DashScope) defaults
pub const DEFAULT_MODELSTUDIO_TOKEN_PLAN_MODEL: &str = "qwen3.8-max";
pub const DEFAULT_MODELSTUDIO_TOKEN_PLAN_BASE_URL: &str =

Some files were not shown because too many files have changed in this diff Show More