Compare commits

...
Author SHA1 Message Date
CodeWhale BotandClaude Sonnet 5.5 b3edc96bfb test(tui): real-terminal thinking stream, stop, restore and sign-in notice at four widths (CW-7876)
Adds a PTY scenario to the existing cucumber harness, compiled only on unix
with long-running-tests: stream thinking, stop, and restore at four terminal
widths, the one-time sign-in notice with Esc dismissal, and a saved choice to
hide thinking surviving the new default.

Checks: farm run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 tests run,
3 passed (1 slow), 60 skipped, exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 17:55:59 -08:00
CodeWhale BotandClaude Sonnet 5.5 c8ee444f88 fix(engine): a build with test-support loads the account only from a loopback account service (CW-7876)
CI builds the acceptance binary with `cargo build -p codewhale-cli --bin
codewhale --all-features`, but the workspace `cargo nextest run --workspace
--all-features` step that follows rebuilds target/debug/codewhale, and that
build unifies the engine test-support feature in (app-server's test-support
enables it, and the cli integration tests make cargo relink the binary). The
binary the real-terminal tests launch therefore has test-support on. To
reproduce: build that way, then run the cucumber test
account_preferences_notice_is_shown_once_per_sign_in_and_esc_dismisses_it.

Under test-support profile_constitution::capture never read the account, so the
loopback account service in the test was never asked and the one-time sign-in
notice never appeared; the test waited its full 80 seconds on Linux and macOS
and failed.

Under test-support the account is now loaded only when
CODEWHALE_CLOUD_API_BASE normalizes to a loopback host. An unset variable or a
non-loopback base (including the production base some secrets tests export for
the whole process) still skips the account, so a test-support build never sends
a stored bearer to a remote account service and a developer shell that exports
the variable cannot leak a real session into an engine turn. Builds without
test-support are unchanged. The comment that claimed unit tests never read
account credentials is removed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 17:55:45 -08:00
CodeWhale BotandClaude Sonnet 5.5 eef869d2a6 fix(tui): a paused stream shows the thinking it already received (CW-7876)
When the provider paused mid-stream, reasoning already received stayed
unrendered until the next token arrived. The owed revision now renders on a
later display beat (render_owed_revision), so the transcript shows what has
arrived. Covered by a_paused_stream_renders_the_reasoning_it_already_received
and the PTY scenario.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 17:53:41 -08:00
CodeWhale BotandClaude Sonnet 5.5 20f437f42c fix(tui): the footer clock says thinking, like the transcript (CW-7876)
The footer phase word and clock said reasoning while the transcript header
said thinking. Both now say thinking, in the 12 locale packs that carried the
old word and the localization library; uk and ru keep their own wording. The
ui tests assert the new clock text.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 17:53:16 -08:00
CodeWhale BotandClaude Sonnet 5.5 bab86cd3f0 feat(settings): model thinking shows by default; a saved choice is kept (CW-7876)
Thinking traces are first-class: show_thinking now defaults to true in the
runtime Settings and the config schema, while thinking_default_expanded stays
false (live while streaming, collapsed once the answer starts). Docs, the
features.toml row, the zh-Hans docs, the config panel goldens and the derived
web json follow. A show_thinking value already present in settings.toml is
read as an explicit choice and kept. Note that earlier saves wrote the old
default into that file, so only installs without the key get the new default;
whether to migrate those is a founder decision recorded in the review packet.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0, including feature_registry (2 passed); clippy run
20261112T061950Z-ENG-TUI-rebased-clippy-50d460 clean, exit 0; check-facts OK.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 17:52:57 -08:00
CodeWhale BotandClaude Sonnet 5.5 61144665b5 fix(tui): the sign-in notice is a one-time footer notice that Esc dismisses (CW-7876)
The "account preferences not loaded" notice was pushed into the transcript
on every refresh, so it repeated and scrolled away with the conversation.
It is now a standing footer notice shown once per sign-in
(account_preferences_notice_shown), and Esc dismisses it while idle. The
message names the remedy within 40 columns and carries meaning in words, not
colour. New string added to all 15 locale packs and the settings schema.

Checks: farm tests run 20261112T061929Z-ENG-TUI-rebased-tests-d67557, 7557
passed / 10 skipped, exit 0; clippy run 20261112T061950Z-ENG-TUI-rebased-clippy-50d460
clean, exit 0; PTY run 20261112T073916Z-ENG-TUI-rebased-pty4-b3fc94, 3 of 3
passed; locale parity PASS; vocabulary check exit 0.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-11-12 17:52:47 -08:00
35 changed files with 883 additions and 69 deletions

No files matched your search

+7 -1
View File
@@ -605,7 +605,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
def(
"show_thinking",
SettingKind::Bool(SHOW_THINKING),
"false",
"true",
ui(
TAB_APPEARANCE,
"display",
@@ -1546,6 +1546,12 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
"false",
None,
),
def(
"account_preferences_notice_shown",
SettingKind::String,
"",
None,
),
// Round 3 work-bar placement migration ran once (`top` → `bottom`).
def(
"work_surface_bottom_migrated",
+23 -7
View File
@@ -244,22 +244,38 @@ pub async fn capture(
if let Some(snapshot) = supplied {
return snapshot.render().map(Some);
}
// Unit tests never read an operator's account credentials. Transport tests
// exercise load_from with an explicitly isolated store instead.
#[cfg(not(any(test, feature = "test-support")))]
if let Some(snapshot) = load(profile).await? {
return snapshot.render().map(Some);
}
#[cfg(any(test, feature = "test-support"))]
{
let _ = profile;
if let Some(message) = TEST_ACCOUNT_LOAD_FAILURE.get() {
anyhow::bail!(message);
}
if !account_api_base_is_loopback() {
return Ok(crate::prompts::load_user_constitution_block());
}
}
if let Some(snapshot) = load(profile).await? {
return snapshot.render().map(Some);
}
Ok(crate::prompts::load_user_constitution_block())
}
#[cfg(any(test, feature = "test-support"))]
fn account_api_base_is_loopback() -> bool {
std::env::var(codewhale_secrets::account::ACCOUNT_API_BASE_ENV)
.ok()
.and_then(|value| normalize_runtime_account_api_base(&value))
.and_then(|base| reqwest::Url::parse(&base).ok())
.and_then(|url| url.host_str().map(str::to_owned))
.is_some_and(|host| {
host.eq_ignore_ascii_case("localhost")
|| host
.trim_start_matches('[')
.trim_end_matches(']')
.parse::<std::net::IpAddr>()
.is_ok_and(|address| address.is_loopback())
})
}
#[cfg(any(test, feature = "test-support"))]
thread_local! {
static TEST_ACCOUNT_LOAD_FAILURE: std::cell::Cell<Option<&'static str>> =
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "actiu",
"ExtensionPromptUnavailable": "Les instruccions de les extensions no estan disponibles; consulta /plugin show.",
"ProfileConstitutionUnavailableLocal": "No s'han pogut carregar les preferències del compte de Codewhale; aquest torn fa servir la configuració local. La clau del proveïdor continua funcionant; executa codewhale login per recuperar les preferències del compte.",
"AccountPreferencesNotLoaded": "Compte no carregat. Executa {command}. La clau del proveïdor continua funcionant.",
"ComposerOversizedSubmitHeld": "No s’ha enviat: el missatge supera els {limit} caràcters i no s’ha pogut desar en un fitxer de text enganxat ({error}). El text complet continua al camp d’entrada: escurça’l o habilita l’escriptura a .codewhale/pastes i torna a prémer Enter.",
"ComposerOversizedSubmitHeldShort": "No s’ha enviat: supera els {limit} caràcters; no s’ha desat el fitxer d’enganxat.",
"CmdPluginOwnerReport": "Amfitrió d’extensions:\n Estat: {state}\n Eines actives ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "inactiu",
"PhaseDraft": "esborrany",
"PhaseWorking": "treballant",
"PhaseReasoning": "raonant",
"PhaseReasoning": "pensant",
"PhaseReading": "llegint",
"PhaseUsingTool": "usant una eina",
"PhaseSubagents": "subagents en execució",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "aktiv",
"ExtensionPromptUnavailable": "Anweisungen von Erweiterungen sind nicht verfügbar; prüfe /plugin show.",
"ProfileConstitutionUnavailableLocal": "Die Codewhale-Kontoeinstellungen konnten nicht geladen werden; dieser Durchlauf nutzt deine lokalen Einstellungen. Dein Anbieter-Schlüssel funktioniert weiter; führe codewhale login aus, um die Kontoeinstellungen wiederherzustellen.",
"AccountPreferencesNotLoaded": "Konto nicht geladen. Führe {command} aus. Dein Anbieter-Schlüssel funktioniert weiter.",
"ComposerOversizedSubmitHeld": "Nicht gesendet: Diese Nachricht enthält mehr als {limit} Zeichen und konnte nicht als Einfügedatei gespeichert werden ({error}). Der vollständige Text steht noch im Eingabefeld: Kürze ihn oder mache .codewhale/pastes beschreibbar und drücke dann erneut Enter.",
"ComposerOversizedSubmitHeldShort": "Nicht gesendet: über {limit} Zeichen; Einfügedatei nicht gespeichert.",
"CmdPluginOwnerReport": "Erweiterungshost:\n Status: {state}\n Aktive Werkzeuge ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "ruhend",
"PhaseDraft": "entwurf",
"PhaseWorking": "arbeitet",
"PhaseReasoning": "überlegt",
"PhaseReasoning": "denkt nach",
"PhaseReading": "liest",
"PhaseUsingTool": "nutzt Tool",
"PhaseSubagents": "Subagenten laufen",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "live",
"ExtensionPromptUnavailable": "Extension instructions are unavailable; inspect /plugin show.",
"ProfileConstitutionUnavailableLocal": "Codewhale account preferences could not be loaded, so this turn uses your local settings. Your provider key still works; run codewhale login to restore account preferences.",
"AccountPreferencesNotLoaded": "Account not loaded. Run {command}. Your provider key still works.",
"ComposerOversizedSubmitHeld": "Not sent: this message is over {limit} characters and could not be saved as a paste file ({error}). The full text is still in the composer: shorten it, or make .codewhale/pastes writable, then press Enter again.",
"ComposerOversizedSubmitHeldShort": "Not sent: over {limit} characters; paste file not saved.",
"CmdPluginOwnerReport": "Extension host:\n State: {state}\n Live tools ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "idle",
"PhaseDraft": "draft",
"PhaseWorking": "working",
"PhaseReasoning": "reasoning",
"PhaseReasoning": "thinking",
"PhaseReading": "reading",
"PhaseUsingTool": "using tool",
"PhaseSubagents": "agents underway",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "activo",
"ExtensionPromptUnavailable": "Las instrucciones de las extensiones no están disponibles; revisa /plugin show.",
"ProfileConstitutionUnavailableLocal": "No se pudieron cargar las preferencias de la cuenta de Codewhale; este turno usa tu configuración local. Tu clave del proveedor sigue funcionando; ejecuta codewhale login para restaurar las preferencias de la cuenta.",
"AccountPreferencesNotLoaded": "Cuenta no cargada. Ejecuta {command}. Tu clave del proveedor sigue funcionando.",
"ComposerOversizedSubmitHeld": "No se envió: este mensaje supera los {limit} caracteres y no se pudo guardar en un archivo de texto pegado ({error}). El texto completo sigue en el campo de entrada: acórtalo o habilita la escritura en .codewhale/pastes y vuelve a presionar Enter.",
"ComposerOversizedSubmitHeldShort": "No se envió: supera los {limit} caracteres; no se guardó el archivo pegado.",
"CmdPluginOwnerReport": "Host de extensiones:\n Estado: {state}\n Herramientas activas ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "inactivo",
"PhaseDraft": "borrador",
"PhaseWorking": "trabajando",
"PhaseReasoning": "razonando",
"PhaseReasoning": "pensando",
"PhaseReading": "leyendo",
"PhaseUsingTool": "usando herramienta",
"PhaseSubagents": "subagentes en curso",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "actif",
"ExtensionPromptUnavailable": "Les instructions des extensions sont indisponibles ; consultez /plugin show.",
"ProfileConstitutionUnavailableLocal": "Les préférences du compte Codewhale n'ont pas pu être chargées ; ce tour utilise vos réglages locaux. Votre clé de fournisseur fonctionne toujours ; lancez codewhale login pour rétablir les préférences du compte.",
"AccountPreferencesNotLoaded": "Compte non chargé. Lancez {command}. Votre clé de fournisseur fonctionne toujours.",
"ComposerOversizedSubmitHeld": "Non envoyé : ce message dépasse {limit} caractères et n’a pas pu être enregistré dans un fichier de texte collé ({error}). Le texte intégral reste dans la zone de saisie : raccourcissez-le ou autorisez l’écriture dans .codewhale/pastes, puis appuyez à nouveau sur Enter.",
"ComposerOversizedSubmitHeldShort": "Non envoyé : plus de {limit} caractères ; fichier collé non enregistré.",
"CmdPluginOwnerReport": "Hôte des extensions :\n État : {state}\n Outils actifs ({count}) : {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "inactif",
"PhaseDraft": "brouillon",
"PhaseWorking": "travail en cours",
"PhaseReasoning": "raisonnement",
"PhaseReasoning": "réflexion",
"PhaseReading": "lecture",
"PhaseUsingTool": "utilisation d'outil",
"PhaseSubagents": "sous-agents en cours",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "सक्रिय",
"ExtensionPromptUnavailable": "एक्सटेंशन के निर्देश उपलब्ध नहीं हैं; /plugin show से जाँचें।",
"ProfileConstitutionUnavailableLocal": "Codewhale खाते की प्राथमिकताएँ लोड नहीं हो सकीं, इसलिए यह टर्न आपकी स्थानीय सेटिंग्स का उपयोग करता है। आपकी प्रदाता कुंजी काम करती रहेगी; खाते की प्राथमिकताएँ वापस लाने के लिए codewhale login चलाएँ।",
"AccountPreferencesNotLoaded": "खाता लोड नहीं हुआ। {command} चलाएँ। आपकी प्रदाता कुंजी काम करती रहेगी।",
"ComposerOversizedSubmitHeld": "नहीं भेजा गया: इस संदेश में {limit} से अधिक वर्ण हैं और इसे पेस्ट फ़ाइल के रूप में सहेजा नहीं जा सका ({error})। पूरा पाठ अभी भी इनपुट बॉक्स में है: इसे छोटा करें या .codewhale/pastes में लिखने की अनुमति दें, फिर Enter दबाएँ।",
"ComposerOversizedSubmitHeldShort": "नहीं भेजा गया: {limit} से अधिक वर्ण; पेस्ट फ़ाइल सहेजी नहीं गई।",
"CmdPluginOwnerReport": "एक्सटेंशन होस्ट:\n स्थिति: {state}\n सक्रिय टूल ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "निष्क्रिय",
"PhaseDraft": "मसौदा",
"PhaseWorking": "काम चल रहा",
"PhaseReasoning": "तर्क चल रहा",
"PhaseReasoning": "सोच रहा",
"PhaseReading": "पढ़ रहा",
"PhaseUsingTool": "टूल इस्तेमाल हो रहा",
"PhaseSubagents": "सबएजेंट चल रहे हैं",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "aktif",
"ExtensionPromptUnavailable": "Instruksi ekstensi tidak tersedia; periksa /plugin show.",
"ProfileConstitutionUnavailableLocal": "Preferensi akun Codewhale tidak dapat dimuat, jadi giliran ini memakai pengaturan lokal. Kunci penyedia tetap berfungsi; jalankan codewhale login untuk memulihkan preferensi akun.",
"AccountPreferencesNotLoaded": "Akun tidak dimuat. Jalankan {command}. Kunci penyedia tetap berfungsi.",
"ComposerOversizedSubmitHeld": "Belum terkirim: pesan ini melebihi {limit} karakter dan tidak dapat disimpan sebagai berkas teks tempelan ({error}). Teks lengkap masih ada di kolom input: persingkat teks atau izinkan penulisan ke .codewhale/pastes, lalu tekan Enter lagi.",
"ComposerOversizedSubmitHeldShort": "Belum terkirim: melebihi {limit} karakter; berkas tempelan tidak tersimpan.",
"CmdPluginOwnerReport": "Host ekstensi:\n Status: {state}\n Alat aktif ({count}): {tools}",
@@ -1667,7 +1668,7 @@
"PhaseIdle": "diam",
"PhaseDraft": "draf",
"PhaseWorking": "bekerja",
"PhaseReasoning": "menalar",
"PhaseReasoning": "berpikir",
"PhaseReading": "membaca",
"PhaseUsingTool": "memakai tool",
"PhaseSubagents": "subagen berjalan",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "稼働中",
"ExtensionPromptUnavailable": "拡張機能の指示を利用できません。/plugin show で確認してください。",
"ProfileConstitutionUnavailableLocal": "Codewhale アカウントの設定を読み込めなかったため、このターンはローカル設定を使います。プロバイダーキーはそのまま使えます。アカウント設定を戻すには codewhale login を実行してください。",
"AccountPreferencesNotLoaded": "アカウントを読み込めませんでした。{command} を実行してください。プロバイダーキーはそのまま使えます。",
"ComposerOversizedSubmitHeld": "未送信:メッセージが {limit} 文字を超えており、貼り付けファイルに保存できませんでした({error})。全文は入力欄に残っています。短くするか .codewhale/pastes を書き込み可能にしてから、もう一度 Enter を押してください。",
"ComposerOversizedSubmitHeldShort": "未送信:{limit} 文字超。貼り付けファイルは未保存です。",
"CmdPluginOwnerReport": "拡張機能ホスト:\n 状態: {state}\n 有効なツール ({count}): {tools}",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "실행 중",
"ExtensionPromptUnavailable": "확장 기능의 지침을 사용할 수 없습니다. /plugin show에서 확인하세요.",
"ProfileConstitutionUnavailableLocal": "Codewhale 계정 설정을 불러오지 못해 이번 턴은 로컬 설정을 사용합니다. 공급자 키는 계속 사용할 수 있으며, 계정 설정을 복원하려면 codewhale login을 실행하세요.",
"AccountPreferencesNotLoaded": "계정을 불러오지 못했습니다. {command}을 실행하세요. 공급자 키는 계속 사용할 수 있습니다.",
"ComposerOversizedSubmitHeld": "전송하지 못했습니다. 메시지가 {limit}자를 초과하며 붙여넣기 파일로 저장할 수 없습니다({error}). 전체 텍스트는 입력란에 그대로 있습니다. 내용을 줄이거나 .codewhale/pastes에 쓰기 권한을 부여한 뒤 Enter를 다시 누르세요.",
"ComposerOversizedSubmitHeldShort": "전송 안 됨: {limit}자 초과, 붙여넣기 파일 저장 실패.",
"CmdPluginOwnerReport": "확장 호스트:\n 상태: {state}\n 활성 도구 ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "유휴",
"PhaseDraft": "초안",
"PhaseWorking": "작업 중",
"PhaseReasoning": "추론 중",
"PhaseReasoning": "생각 중",
"PhaseReading": "읽는 중",
"PhaseUsingTool": "도구 사용 중",
"PhaseSubagents": "서브에이전트 실행 중",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "ativo",
"ExtensionPromptUnavailable": "As instruções das extensões não estão disponíveis; confira /plugin show.",
"ProfileConstitutionUnavailableLocal": "Não foi possível carregar as preferências da conta Codewhale; este turno usa suas configurações locais. Sua chave do provedor continua funcionando; execute codewhale login para restaurar as preferências da conta.",
"AccountPreferencesNotLoaded": "Conta não carregada. Execute {command}. Sua chave do provedor continua funcionando.",
"ComposerOversizedSubmitHeld": "Não enviado: esta mensagem excede {limit} caracteres e não pôde ser salva em um arquivo de texto colado ({error}). O texto completo continua no campo de entrada: reduza-o ou permita a gravação em .codewhale/pastes e pressione Enter novamente.",
"ComposerOversizedSubmitHeldShort": "Não enviado: acima de {limit} caracteres; arquivo colado não salvo.",
"CmdPluginOwnerReport": "Host de extensões:\n Estado: {state}\n Ferramentas ativas ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "ocioso",
"PhaseDraft": "rascunho",
"PhaseWorking": "trabalhando",
"PhaseReasoning": "raciocinando",
"PhaseReasoning": "pensando",
"PhaseReading": "lendo",
"PhaseUsingTool": "usando ferramenta",
"PhaseSubagents": "subagentes em andamento",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "активен",
"ExtensionPromptUnavailable": "Инструкции расширений недоступны; проверьте /plugin show.",
"ProfileConstitutionUnavailableLocal": "Не удалось загрузить настройки аккаунта Codewhale, поэтому этот ход использует локальные настройки. Ключ провайдера продолжает работать; выполните codewhale login, чтобы вернуть настройки аккаунта.",
"AccountPreferencesNotLoaded": "Аккаунт не загружен. Выполните {command}. Ключ провайдера продолжает работать.",
"ComposerOversizedSubmitHeld": "Не отправлено: сообщение длиннее {limit} символов, и его не удалось сохранить в файл вставленного текста ({error}). Полный текст остался в поле ввода: сократите его или разрешите запись в .codewhale/pastes, затем снова нажмите Enter.",
"ComposerOversizedSubmitHeldShort": "Не отправлено: больше {limit} символов; файл вставки не сохранён.",
"CmdPluginOwnerReport": "Хост расширений:\n Состояние: {state}\n Активные инструменты ({count}): {tools}",
+1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "активний",
"ExtensionPromptUnavailable": "Інструкції розширень недоступні; перевірте /plugin show.",
"ProfileConstitutionUnavailableLocal": "Не вдалося завантажити налаштування облікового запису Codewhale, тому цей хід використовує локальні налаштування. Ключ провайдера й далі працює; виконайте codewhale login, щоб повернути налаштування облікового запису.",
"AccountPreferencesNotLoaded": "Обліковий запис не завантажено. Виконайте {command}. Ключ провайдера й далі працює.",
"ComposerOversizedSubmitHeld": "Не надіслано: повідомлення перевищує {limit} символів, і його не вдалося зберегти у файл вставленого тексту ({error}). Повний текст залишається в полі введення: скоротіть його або дозвольте запис у .codewhale/pastes, а потім знову натисніть Enter.",
"ComposerOversizedSubmitHeldShort": "Не надіслано: понад {limit} символів; файл вставки не збережено.",
"CmdPluginOwnerReport": "Хост розширень:\n Стан: {state}\n Активні інструменти ({count}): {tools}",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "đang chạy",
"ExtensionPromptUnavailable": "Không thể tải hướng dẫn của tiện ích mở rộng; kiểm tra /plugin show.",
"ProfileConstitutionUnavailableLocal": "Không thể tải tùy chọn tài khoản Codewhale nên lượt này dùng cài đặt cục bộ. Khóa nhà cung cấp của bạn vẫn hoạt động; chạy codewhale login để khôi phục tùy chọn tài khoản.",
"AccountPreferencesNotLoaded": "Chưa tải được tài khoản. Chạy {command}. Khóa nhà cung cấp của bạn vẫn hoạt động.",
"ComposerOversizedSubmitHeld": "Chưa gửi: tin nhắn vượt quá {limit} ký tự và không thể lưu thành tệp văn bản đã dán ({error}). Toàn bộ nội dung vẫn còn trong ô nhập: hãy rút ngắn hoặc cho phép ghi vào .codewhale/pastes, rồi nhấn Enter lần nữa.",
"ComposerOversizedSubmitHeldShort": "Chưa gửi: vượt quá {limit} ký tự; chưa lưu tệp dán.",
"CmdPluginOwnerReport": "Máy chủ tiện ích mở rộng:\n Trạng thái: {state}\n Công cụ đang hoạt động ({count}): {tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "rảnh",
"PhaseDraft": "nháp",
"PhaseWorking": "đang xử lý",
"PhaseReasoning": "đang suy luận",
"PhaseReasoning": "đang suy nghĩ",
"PhaseReading": "đang đọc",
"PhaseUsingTool": "đang dùng công cụ",
"PhaseSubagents": "đang chạy tác tử con",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "运行中",
"ExtensionPromptUnavailable": "扩展指令不可用;请通过 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "无法加载 Codewhale 账号偏好,本轮改用本地设置。你的提供商密钥仍可正常使用;运行 codewhale login 可恢复账号偏好。",
"AccountPreferencesNotLoaded": "账号未加载。请运行 {command}。你的提供商密钥仍可正常使用。",
"ComposerOversizedSubmitHeld": "未发送:消息超过 {limit} 个字符,无法保存为粘贴文本文件({error})。完整文本仍保留在输入框中:请缩短内容,或允许写入 .codewhale/pastes,然后再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未发送:超过 {limit} 个字符;粘贴文件未保存。",
"CmdPluginOwnerReport": "扩展宿主:\n 状态:{state}\n 活动工具({count}):{tools}",
@@ -1690,7 +1691,7 @@
"PhaseIdle": "空闲",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "推理中",
"PhaseReasoning": "思考中",
"PhaseReading": "读取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "智能体工作中",
+2 -1
View File
@@ -25,6 +25,7 @@
"TerminalDockLive": "執行中",
"ExtensionPromptUnavailable": "擴充功能指令無法使用;請透過 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "無法載入 Codewhale 帳號偏好,本輪改用本機設定。你的服務商金鑰仍可正常使用;執行 codewhale login 可恢復帳號偏好。",
"AccountPreferencesNotLoaded": "帳號未載入。請執行 {command}。你的服務商金鑰仍可正常使用。",
"ComposerOversizedSubmitHeld": "未傳送:訊息超過 {limit} 個字元,無法儲存為貼上文字檔案({error})。完整文字仍保留在輸入框中:請縮短內容,或允許寫入 .codewhale/pastes,然後再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未傳送:超過 {limit} 個字元;貼上檔案未儲存。",
"CmdPluginOwnerReport": "擴充功能主機:\n 狀態:{state}\n 活動工具({count}):{tools}",
@@ -1688,7 +1689,7 @@
"PhaseIdle": "閒置",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "推理中",
"PhaseReasoning": "思考中",
"PhaseReading": "讀取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "代理程式工作中",
+2
View File
@@ -714,6 +714,7 @@ pub enum MessageId {
ExtensionsInventorySkills,
ExtensionPromptUnavailable,
ProfileConstitutionUnavailableLocal,
AccountPreferencesNotLoaded,
ExtensionsMarketplaceDetail,
ExtensionsMarketplaceUnavailable,
ExtensionsMcpEmpty,
@@ -3651,6 +3652,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ExtensionsInventorySkills,
MessageId::ExtensionPromptUnavailable,
MessageId::ProfileConstitutionUnavailableLocal,
MessageId::AccountPreferencesNotLoaded,
MessageId::ExtensionsMarketplaceDetail,
MessageId::ExtensionsMarketplaceUnavailable,
MessageId::ExtensionsMcpEmpty,
+37 -6
View File
@@ -492,6 +492,8 @@ pub struct Settings {
/// One-time Operate-retirement toast has been shown. Same suppression
/// contract as `yolo_deprecation_shown`.
pub operate_deprecation_shown: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub account_preferences_notice_shown: Option<String>,
/// Round 3 (2026-09-01) moved the work bar under the composer. Every
/// settings.toml saved before that carries `work_surface_placement =
/// "top"` — the old default, persisted verbatim by ordinary saves, not a
@@ -650,9 +652,7 @@ impl Default for Settings {
mention_menu_limit: 128,
mention_walk_depth: 10,
mention_menu_behavior: "fuzzy".to_string(),
// Reasoning is useful when explicitly requested, but it should
// never displace the actual conversation in the default TUI.
show_thinking: false,
show_thinking: true,
thinking_default_expanded: false,
thinking_preview_lines: default_thinking_preview_lines(),
thinking_highlight: true,
@@ -694,6 +694,7 @@ impl Default for Settings {
feature_intro_shown: false,
yolo_deprecation_shown: false,
operate_deprecation_shown: false,
account_preferences_notice_shown: None,
legacy_operate_default: false,
work_surface_bottom_migrated: false,
behavioral_tip_impressions: std::collections::BTreeMap::new(),
@@ -3747,6 +3748,36 @@ mod tests {
assert_eq!(reloaded.permission_posture.as_deref(), Some("ask"));
}
#[test]
fn thinking_shows_by_default_and_a_saved_choice_survives_reload_and_other_saves() {
let _g = config_path_test_guard();
let tmp = tempfile::tempdir().expect("tempdir");
let settings_path = tmp.path().join("settings.toml");
let _config_override =
EnvVarRestore::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml"));
let absent = Settings::load().expect("load without a settings file");
assert!(absent.show_thinking);
assert!(!absent.thinking_default_expanded);
assert!(!absent.is_set("show_thinking"));
std::fs::write(&settings_path, "theme = \"light\"\n").expect("settings without the key");
let unnamed = Settings::load().expect("load settings without the key");
assert!(unnamed.show_thinking);
assert!(!unnamed.is_set("show_thinking"));
for saved in [false, true] {
std::fs::write(&settings_path, format!("show_thinking = {saved}\n"))
.expect("saved choice");
let loaded = Settings::load().expect("load the saved choice");
assert_eq!(loaded.show_thinking, saved);
assert!(loaded.is_set("show_thinking"));
Settings::transact(|settings| settings.set("locale", "en")).expect("another save");
let reloaded = Settings::load().expect("reload after another save");
assert_eq!(reloaded.show_thinking, saved);
}
}
#[test]
fn settings_load_keeps_top_placement_chosen_after_the_bottom_migration() {
let _g = config_path_test_guard();
@@ -3886,7 +3917,7 @@ mod tests {
let mut settings = Settings::default();
// Density is calm by default; motion is an independent axis.
assert!(settings.calm_mode);
assert!(!settings.show_thinking);
settings.show_thinking = false;
let changed = settings.apply_preset("CALM").expect("calm preset applies");
assert_eq!(
@@ -3916,8 +3947,8 @@ mod tests {
assert!(settings.fancy_animations);
assert_eq!(settings.transcript_spacing, "spacious");
assert_eq!(settings.tool_collapse_mode, "compact");
// Thinking is opt-in so the transcript stays focused on the chat.
assert!(!settings.show_thinking);
assert!(settings.show_thinking);
assert!(!settings.thinking_default_expanded);
}
#[test]
+54
View File
@@ -87,6 +87,27 @@ pub(crate) use types::{
WORKFLOW_DRAFT_INSTRUCTION_PREFIX,
};
const ACCOUNT_PREFERENCES_NOTICE: &str = "account-preferences-notice";
fn account_sign_in_marker(profile: Option<&str>) -> String {
use sha2::{Digest, Sha256};
let api_base = codewhale_engine::profile_constitution::runtime_account_api_base();
let session = codewhale_secrets::account::AccountSessionStore::new(
codewhale_secrets::Secrets::file_backed_read_only(),
profile,
&api_base,
)
.runtime_info_at(Utc::now())
.ok()
.and_then(|info| info.session_id)
.unwrap_or_default();
Sha256::digest(session.as_bytes())
.iter()
.take(8)
.map(|byte| format!("{byte:02x}"))
.collect()
}
// === Types ===
/// One login owns one mailbox. A cancelled task can only write its abandoned
@@ -2448,6 +2469,7 @@ pub struct App {
/// re-render per character. `None` means "no bump since the last
/// finalize" so the first chunk of a block always renders immediately.
pub thinking_revision_last_bump_at: Option<Instant>,
pub(crate) thinking_revision_owed: bool,
/// Newline-gated streaming collector state.
pub streaming_state: StreamingState,
/// Live approximate output tokens for the current assistant stream.
@@ -3791,6 +3813,38 @@ impl App {
);
}
pub(crate) fn notify_account_preferences_not_loaded(&mut self) {
let sign_in = account_sign_in_marker(self.config_profile.as_deref());
self.notify_account_preferences_not_loaded_for(&sign_in);
}
fn notify_account_preferences_not_loaded_for(&mut self, sign_in: &str) {
let first_for_sign_in = Settings::transact_opt(|settings| {
if settings.account_preferences_notice_shown.as_deref() == Some(sign_in) {
return Ok(None);
}
settings.account_preferences_notice_shown = Some(sign_in.to_string());
Ok(Some(()))
});
if matches!(first_for_sign_in, Ok(None)) {
return;
}
let text = self
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
self.push_status_toast_record(StatusToast::standing(
text,
StatusToastLevel::Warning,
ACCOUNT_PREFERENCES_NOTICE,
));
}
pub(crate) fn dismiss_account_preferences_notice(&mut self) -> bool {
let before = self.status_toasts.len();
self.retire_event_notices(ACCOUNT_PREFERENCES_NOTICE);
self.status_toasts.len() != before
}
/// One-release migration notice for the Shift+Tab/Ctrl+T rebinding: users
/// pressing Shift+Tab expecting the old thinking cycle land here first.
fn notify_keybinding_migration_once(&mut self) {
+1
View File
@@ -1142,6 +1142,7 @@ impl App {
streaming_thinking_active_entry: None,
pending_thinking_translation_cells: HashMap::new(),
thinking_revision_last_bump_at: None,
thinking_revision_owed: false,
streaming_state: StreamingState::new(),
streaming_output_token_estimate: 0,
reasoning_buffer: String::new(),
+40
View File
@@ -184,6 +184,46 @@ fn feature_intro_shows_once_persists_then_is_idempotent() {
let _ = std::fs::remove_dir_all(&tmp);
}
#[test]
fn account_preferences_notice_shows_once_per_sign_in_and_can_be_dismissed() {
let _env_lock = lock_test_env();
let tmp = tempfile::tempdir().unwrap();
let config_path = tmp.path().join("config.toml");
let _env = EnvVarGuard::set(
"DEEPSEEK_CONFIG_PATH",
config_path.to_string_lossy().as_ref(),
);
let shown = |app: &App| {
let notice = app
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
app.status_toasts
.iter()
.filter(|toast| toast.text == notice && toast.kind == StatusToastKind::Standing)
.count()
};
let mut app = App::new(test_options(false), &Config::default());
app.notify_account_preferences_not_loaded_for("first-sign-in");
app.notify_account_preferences_not_loaded_for("first-sign-in");
assert_eq!(shown(&app), 1);
assert_eq!(
Settings::load()
.expect("settings should load")
.account_preferences_notice_shown
.as_deref(),
Some("first-sign-in")
);
assert!(app.dismiss_account_preferences_notice());
assert_eq!(shown(&app), 0);
let mut later_session = App::new(test_options(false), &Config::default());
later_session.notify_account_preferences_not_loaded_for("first-sign-in");
assert_eq!(shown(&later_session), 0, "the same sign-in is told once");
later_session.notify_account_preferences_not_loaded_for("second-sign-in");
assert_eq!(shown(&later_session), 1, "a later sign-in is told again");
}
#[test]
fn initial_input_scenario() {
// Scenario consolidation of: initial_input_prefill_waits_for_manual_submit, initial_input_submit_marks_startup_dispatch
@@ -9,7 +9,7 @@
│ Background (default) ✎ │current terminal
│ Pet mode Off [ ] │saved terminal
│ Quiet transcript On [x] │startup terminal
│ Model reasoning in chat Off [ ] │source settings.toml
│ Model reasoning in chat On [x] │source settings.toml
│ Thinking Default Expanded Off [ ] │scope SAVED
│ Thinking Preview Lines 2 ✎ │apply applies on save
│ Thinking background highlight On [x] │kind choice
@@ -9,7 +9,7 @@
Background (default) ✎ SAVED █
Pet mode Off [ ] SAVED █
Quiet transcript On [x] SAVED █
Model reasoning in chat Off [ ] SAVED █
Model reasoning in chat On [x] SAVED █
Thinking Default Expanded Off [ ] SAVED █
Thinking Preview Lines 2 ✎ SAVED │
Thinking background highlight On [x] SAVED │
+46 -6
View File
@@ -30,15 +30,17 @@ use crate::tui::history::HistoryCell;
/// frame cap (see `frame_rate_limiter`) yet imperceptible as lag.
///
/// Correctness: this only skips *intermediate* repaints. Appended content is
/// never dropped — it lands in the cell immediately — and finalize always
/// forces a bump so the final reasoning text is fully rendered.
/// never dropped — it lands in the cell immediately — the display beat renders
/// a skipped bump once the window has passed, and finalize always forces a
/// bump so the final reasoning text is fully rendered.
const THINKING_REVISION_THROTTLE: Duration = Duration::from_millis(100);
/// Bump the active-cell revision for a streaming thinking mutation, but at
/// most once per [`THINKING_REVISION_THROTTLE`] window. Returns whether a bump
/// was actually emitted. Skipped bumps coalesce into the next one (or into the
/// forced finalize bump), so no content is ever lost — only redundant
/// intermediate re-wraps are dropped.
/// was actually emitted. Skipped bumps coalesce into the next one, into
/// [`render_owed_revision`] on a later display beat, or into the forced
/// finalize bump, so no content is ever lost — only redundant intermediate
/// re-wraps are dropped.
fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
let due = app
.thinking_revision_last_bump_at
@@ -47,9 +49,17 @@ fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
app.thinking_revision_last_bump_at = Some(now);
app.bump_active_cell_revision();
}
app.thinking_revision_owed = !due;
due
}
pub(super) fn render_owed_revision(app: &mut App, now: Instant) -> bool {
if app.streaming_thinking_active_entry.is_none() {
app.thinking_revision_owed = false;
}
app.thinking_revision_owed && bump_thinking_revision_throttled(app, now)
}
/// Ensure an in-flight Thinking entry exists in `active_cell` and return its
/// entry index. If no thinking entry is currently streaming, push a fresh one.
/// P2.3: thinking shares the active cell with subsequent tool calls so the
@@ -76,7 +86,8 @@ pub(super) fn ensure_active_entry(app: &mut App) -> usize {
/// committed to the cell immediately; the active-cell revision bump that
/// triggers a re-wrap of the live tail is debounced to at most one per
/// [`THINKING_REVISION_THROTTLE`] window (#1620). Skipped bumps coalesce into
/// the next append or the forced finalize bump, so no content is ever lost.
/// the next append, a later display beat or the forced finalize bump, so no
/// content is ever lost.
pub(super) fn append(app: &mut App, entry_idx: usize, text: &str) {
append_at(app, entry_idx, text, Instant::now());
}
@@ -308,6 +319,7 @@ pub(super) fn finalize_active_entry(app: &mut App, duration: Option<f32>, remain
// the debounce window so the next thinking block's first chunk renders
// immediately rather than being coalesced into a stale window.
app.thinking_revision_last_bump_at = None;
app.thinking_revision_owed = false;
app.bump_active_cell_revision();
true
}
@@ -426,6 +438,34 @@ mod tests {
);
}
#[test]
fn a_paused_stream_renders_the_reasoning_it_already_received() {
let mut app = test_app();
let entry = ensure_active_entry(&mut app);
app.thinking_revision_last_bump_at = None;
let t0 = Instant::now();
append_at(&mut app, entry, "first slice ", t0);
let shown = app.active_cell_revision;
append_at(&mut app, entry, "held back", t0 + Duration::from_millis(16));
assert_eq!(app.active_cell_revision, shown);
assert!(!render_owed_revision(
&mut app,
t0 + Duration::from_millis(32)
));
assert_eq!(app.active_cell_revision, shown);
assert!(render_owed_revision(
&mut app,
t0 + THINKING_REVISION_THROTTLE
));
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
assert!(!render_owed_revision(
&mut app,
t0 + THINKING_REVISION_THROTTLE * 3
));
assert_eq!(app.active_cell_revision, shown.wrapping_add(1));
}
/// #7123: a reasoning block that opens while the display beat still holds
/// an uncommitted slice of prose must not drop that slice. `start_block`
/// resets `StreamingState`, so the tail has to be delivered first.
+9 -11
View File
@@ -7232,14 +7232,16 @@ pub(crate) async fn run_event_loop(
continue;
}
// An idle operator can dismiss the persistent context warning
// without affecting vim or attachment handling. While a turn
// is active Esc retains its cancellation meaning.
// or the account preferences notice without affecting vim or
// attachment handling. While a turn is active Esc retains its
// cancellation meaning.
KeyCode::Esc
if !app.is_loading
&& app.input.is_empty()
&& !slash_menu_open
&& !mention_menu_open
&& app.dismiss_context_pressure_warning() =>
&& (app.dismiss_context_pressure_warning()
|| app.dismiss_account_preferences_notice()) =>
{
continue;
}
@@ -9326,14 +9328,10 @@ pub(super) fn apply_engine_status(app: &mut App, message: String) -> bool {
app.add_message(HistoryCell::System { content: message });
return true;
}
// The account-profile fallback notice is sent once per engine; keep it in
// the transcript so the next turn's status line cannot erase it.
let retain = app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str();
if retain {
app.add_message(HistoryCell::System {
content: message.clone(),
});
if app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str() {
app.notify_account_preferences_not_loaded();
return false;
}
app.status_message = Some(message);
retain
false
}
+4 -1
View File
@@ -1555,7 +1555,10 @@ pub(crate) fn commit_streaming_display_tick(
updated = true;
}
if app.streaming_state.has_pending_stream_text(0) {
if streaming_thinking::render_owed_revision(app, now) {
updated = true;
}
if app.streaming_state.has_pending_stream_text(0) || app.thinking_revision_owed {
stream_display_clock.note_delta(now);
}
+26 -8
View File
@@ -27343,7 +27343,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
let latest = pager.body_text();
assert!(latest.contains("SECOND-PROMPT"), "{latest}");
assert!(latest.contains("[∿ reasoning 1/1 · running]"), "{latest}");
assert!(latest.contains("[∿ thinking 1/1 · running]"), "{latest}");
assert!(latest.contains("SECOND-THOUGHT"), "{latest}");
assert!(latest.contains("[◆ · running]"), "{latest}");
assert!(latest.contains("SECOND-ANSWER"), "{latest}");
@@ -27355,7 +27355,7 @@ fn turn_inspector_switches_isolated_full_turn_pages_with_tagged_reasoning_and_ou
for expected in [
"FIRST-PROMPT",
"keep this input line",
"[∿ reasoning 1/1 · Done]",
"[∿ thinking 1/1 · Done]",
"FIRST-THOUGHT",
"full reasoning tail",
"[⚙ using tool]",
@@ -35488,23 +35488,41 @@ fn extension_prompt_origin_keeps_denials_and_other_policy_holds() {
}
#[test]
fn account_profile_fallback_notice_stays_in_the_transcript() {
fn account_profile_fallback_notice_is_a_standing_notice_outside_the_transcript() {
let _home = SettingsHomeGuard::new();
let mut app = App::new(create_test_options(), &Config::default());
let notice = app
let engine_notice = app
.tr(MessageId::ProfileConstitutionUnavailableLocal)
.to_string();
let before = app.history.len();
assert!(super::event_loop::apply_engine_status(
let status_before = app.status_message.clone();
assert!(!super::event_loop::apply_engine_status(
&mut app,
notice.clone()
engine_notice
));
assert_eq!(app.history.len(), before);
assert_eq!(app.status_message, status_before);
assert!(!super::event_loop::apply_engine_status(
&mut app,
"Executing tools sequentially".into()
));
assert_eq!(app.history.len(), before + 1);
assert!(matches!(&app.history[before], HistoryCell::System { content } if *content == notice));
assert_eq!(app.history.len(), before);
let notice = app
.tr(MessageId::AccountPreferencesNotLoaded)
.replace("{command}", "codewhale login");
let standing = |app: &App| {
app.status_toasts
.iter()
.find(|toast| toast.text == notice)
.cloned()
};
app.sync_status_message_to_toasts();
let shown = standing(&app).expect("a later routine status does not erase the notice");
assert_eq!(shown.level, StatusToastLevel::Warning);
assert_eq!(shown.ttl_ms, None);
assert!(app.dismiss_account_preferences_notice());
assert!(standing(&app).is_none());
assert!(!app.dismiss_account_preferences_notice());
}
#[test]
+2
View File
@@ -32,6 +32,8 @@ mod plugin_e2e_acceptance;
mod screen_mode_inline_pty;
#[cfg(all(unix, feature = "long-running-tests"))]
mod search_text_pty;
#[cfg(all(unix, feature = "long-running-tests"))]
mod thinking_stream_pty;
mod tool_lifecycle_acceptance;
#[cfg(all(unix, feature = "long-running-tests"))]
mod work_bar_keys_pty;
@@ -0,0 +1,585 @@
use std::io::{BufRead, BufReader, Read, Write};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::{Duration, Instant};
use super::qa_harness::{
harness::{Harness, SealedWorkspace, make_sealed_workspace},
keys,
};
const WAIT: Duration = Duration::from_secs(20);
const MODEL: &str = "deepseek-v4-pro";
const PROMPTS: [&str; 2] = ["first slow fixture prompt", "second slow fixture prompt"];
const ANSWER: &str = "Fixture answer after thinking.";
const REASONING_LINES: usize = 6;
const NOTICE: &str = "Account not loaded. Run codewhale login.";
const RETIRED_TRANSCRIPT_NOTICE: &str = "could not be loaded";
fn reasoning_line(index: usize) -> String {
format!("thinking fixture line {index} of {REASONING_LINES}")
}
struct PacedProvider {
address: SocketAddr,
gate: mpsc::Sender<()>,
stop: Arc<AtomicBool>,
turns: Arc<AtomicUsize>,
profile_requests: Arc<AtomicUsize>,
acceptor: Option<std::thread::JoinHandle<()>>,
}
struct Served {
gates: Mutex<mpsc::Receiver<()>>,
stop: Arc<AtomicBool>,
turns: Arc<AtomicUsize>,
profile_requests: Arc<AtomicUsize>,
}
impl PacedProvider {
fn start() -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("loopback provider");
let address = listener.local_addr().expect("loopback address");
let (gate, gates) = mpsc::channel();
let stop = Arc::new(AtomicBool::new(false));
let turns = Arc::new(AtomicUsize::new(0));
let profile_requests = Arc::new(AtomicUsize::new(0));
let served = Arc::new(Served {
gates: Mutex::new(gates),
stop: Arc::clone(&stop),
turns: Arc::clone(&turns),
profile_requests: Arc::clone(&profile_requests),
});
let acceptor = std::thread::spawn(move || {
for stream in listener.incoming() {
if served.stop.load(Ordering::Acquire) {
break;
}
let Ok(stream) = stream else { continue };
let served = Arc::clone(&served);
std::thread::spawn(move || {
let _ = serve(stream, &served);
});
}
});
Self {
address,
gate,
stop,
turns,
profile_requests,
acceptor: Some(acceptor),
}
}
fn model_base(&self) -> String {
format!("http://{}/v1", self.address)
}
fn account_base(&self) -> String {
format!("http://{}", self.address)
}
fn release(&self, gates: usize) {
for _ in 0..gates {
self.gate.send(()).expect("provider gate");
}
}
fn turns(&self) -> usize {
self.turns.load(Ordering::Acquire)
}
}
impl Drop for PacedProvider {
fn drop(&mut self) {
self.stop.store(true, Ordering::Release);
let _ = TcpStream::connect(self.address);
if let Some(acceptor) = self.acceptor.take() {
let _ = acceptor.join();
}
}
}
fn serve(stream: TcpStream, served: &Served) -> std::io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(10)))?;
let mut reader = BufReader::new(stream.try_clone()?);
let mut stream = stream;
let mut request_line = String::new();
if reader.read_line(&mut request_line)? == 0 {
return Ok(());
}
let mut content_length = 0usize;
loop {
let mut header = String::new();
if reader.read_line(&mut header)? == 0 {
break;
}
let header = header.trim_end();
if header.is_empty() {
break;
}
if let Some((name, value)) = header.split_once(':')
&& name.eq_ignore_ascii_case("content-length")
{
content_length = value.trim().parse().unwrap_or(0);
}
}
let mut body = vec![0u8; content_length.min(8 * 1024 * 1024)];
reader.read_exact(&mut body)?;
let mut parts = request_line.split_whitespace();
let method = parts.next().unwrap_or_default();
let path = parts.next().unwrap_or_default();
if method == "GET" && path.ends_with("/models") {
return respond_json(
&mut stream,
"200 OK",
&serde_json::json!({"object":"list","data":[{"id":MODEL,"object":"model"}]}),
);
}
if path.starts_with("/api/") {
served.profile_requests.fetch_add(1, Ordering::AcqRel);
return respond_json(
&mut stream,
"401 Unauthorized",
&serde_json::json!({"error":"loopback fixture account is signed out"}),
);
}
if method != "POST" {
return respond_json(
&mut stream,
"404 Not Found",
&serde_json::json!({"error":"not served by the loopback fixture"}),
);
}
let request: serde_json::Value = serde_json::from_slice(&body).unwrap_or_default();
let streaming = request["stream"].as_bool().unwrap_or(false);
let with_tools = request["tools"]
.as_array()
.is_some_and(|tools| !tools.is_empty());
let text = String::from_utf8_lossy(&body);
let turn = PROMPTS
.iter()
.rposition(|prompt| text.contains(prompt))
.map(|index| index + 1);
let paced = streaming
&& with_tools
&& turn.is_some_and(|turn| {
served
.turns
.compare_exchange(turn - 1, turn, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
});
if !streaming {
return respond_json(
&mut stream,
"200 OK",
&serde_json::json!({
"id":"loopback","object":"chat.completion","model":MODEL,
"choices":[{"index":0,"message":{"role":"assistant","content":"ok"},"finish_reason":"stop"}],
"usage":{"prompt_tokens":1,"completion_tokens":1,"total_tokens":2}
}),
);
}
stream.write_all(
b"HTTP/1.1 200 OK\r\ncontent-type: text/event-stream\r\ncache-control: no-cache\r\nconnection: close\r\n\r\n",
)?;
stream.flush()?;
if paced {
wait_for_gate(served)?;
for index in 1..=REASONING_LINES {
send_delta(
&mut stream,
serde_json::json!({"reasoning_content": format!("{}\n", reasoning_line(index))}),
)?;
}
wait_for_gate(served)?;
send_delta(&mut stream, serde_json::json!({"content": ANSWER}))?;
wait_for_gate(served)?;
} else {
send_delta(&mut stream, serde_json::json!({"content": "ok"}))?;
}
send_chunk(
&mut stream,
&serde_json::json!({
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],
"usage":{"prompt_tokens":8,"completion_tokens":4,"total_tokens":12}
}),
)?;
stream.write_all(b"data: [DONE]\n\n")?;
stream.flush()
}
fn wait_for_gate(served: &Served) -> std::io::Result<()> {
let gates = served.gates.lock().expect("provider gates");
loop {
if served.stop.load(Ordering::Acquire) {
return Err(std::io::ErrorKind::Interrupted.into());
}
match gates.recv_timeout(Duration::from_millis(100)) {
Ok(()) => return Ok(()),
Err(mpsc::RecvTimeoutError::Timeout) => {}
Err(mpsc::RecvTimeoutError::Disconnected) => {
return Err(std::io::ErrorKind::BrokenPipe.into());
}
}
}
}
fn send_delta(stream: &mut TcpStream, delta: serde_json::Value) -> std::io::Result<()> {
send_chunk(
stream,
&serde_json::json!({
"id":"loopback","object":"chat.completion.chunk","model":MODEL,
"choices":[{"index":0,"delta":delta,"finish_reason":null}]
}),
)
}
fn send_chunk(stream: &mut TcpStream, chunk: &serde_json::Value) -> std::io::Result<()> {
stream.write_all(format!("data: {chunk}\n\n").as_bytes())?;
stream.flush()
}
fn respond_json(
stream: &mut TcpStream,
status: &str,
body: &serde_json::Value,
) -> std::io::Result<()> {
let body = body.to_string();
stream.write_all(
format!(
"HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}",
body.len()
)
.as_bytes(),
)?;
stream.flush()
}
fn prepare(workspace: &SealedWorkspace, provider: &PacedProvider, settings: Option<&str>) {
let state = workspace.home().join(".codewhale");
std::fs::write(state.join(".onboarded"), "").unwrap();
std::fs::write(
state.join("config.toml"),
format!(
"provider = \"deepseek\"\n[providers.deepseek]\nbase_url = {:?}\napi_key = \"loopback-only\"\nmodel = {MODEL:?}\n[notifications]\nmethod = \"off\"\ncompletion_sound = \"off\"\n",
provider.model_base()
),
)
.unwrap();
if let Some(settings) = settings {
std::fs::write(state.join("settings.toml"), settings).unwrap();
}
let trust = workspace.workspace().join(".deepseek");
std::fs::create_dir_all(&trust).unwrap();
std::fs::write(trust.join("trusted"), "").unwrap();
}
fn launch(
workspace: &SealedWorkspace,
provider: &PacedProvider,
(rows, cols): (u16, u16),
session: &str,
animated: bool,
) -> Harness {
let mut tui = Harness::builder(Harness::codewhale_binary())
.cwd(workspace.workspace())
.clear_env()
.seal_home(workspace.home())
.env("PATH", "/usr/bin:/bin:/usr/sbin:/sbin")
.env("SHELL", "/bin/sh")
.env("CODEWHALE_CLOUD_API_BASE", provider.account_base())
.env("CODEWHALE_DISABLE_MODELS_DEV_FETCH", "1")
.env("CODEWHALE_NO_UPDATE_CHECK", "1")
.env("NO_ANIMATIONS", if animated { "0" } else { "1" })
.args([
"--workspace",
workspace.workspace().to_str().unwrap(),
"--no-project-config",
"--skip-onboarding",
session,
])
.size(rows, cols)
.spawn()
.unwrap();
tui.wait_for_text("accept edits", WAIT)
.unwrap_or_else(|error| panic!("the dial chip never painted: {error}"));
tui
}
fn expect(tui: &mut Harness, what: &str, predicate: impl FnMut(&super::qa_harness::Frame) -> bool) {
if let Err(error) = tui.wait_for(predicate, WAIT) {
panic!("waiting for {what}: {error}");
}
}
fn settle(tui: &mut Harness) {
let deadline = Instant::now() + Duration::from_secs(2);
let mut quiet_since = Instant::now();
while Instant::now() < deadline && quiet_since.elapsed() < Duration::from_millis(300) {
if tui.pump() {
quiet_since = Instant::now();
}
std::thread::sleep(Duration::from_millis(20));
}
}
fn report(tui: &mut Harness, label: &str) {
let frame = tui.frame();
println!(
"--- {label} at {}x{} ---\n{}",
frame.cols(),
frame.rows(),
frame.debug_dump()
);
}
fn footer_row(tui: &mut Harness) -> (u16, String) {
let frame = tui.frame();
(0..frame.rows())
.rev()
.map(|row| (row, frame.row(row)))
.find(|(_, text)| text.contains("accept edits"))
.unwrap_or_else(|| panic!("no dial chip on screen:\n{}", frame.debug_dump()))
}
fn assert_status_and_water(tui: &mut Harness, label: &str) {
let (footer, status) = footer_row(tui);
let frame = tui.frame();
for retired in ["operate", "Operate", "Work bar"] {
assert!(
!frame.contains(retired),
"{label}: `{retired}` is on screen:\n{}",
frame.debug_dump()
);
}
assert!(status.contains("accept edits"), "{label}: {status}");
for row in 0..footer.saturating_sub(3) {
let text = frame.row(row);
let cells: Vec<char> = text.chars().collect();
let school = cells.windows(2).any(|pair| {
pair.iter()
.all(|cell| ('\u{2801}'..='\u{28FF}').contains(cell))
});
assert!(
!school && !text.contains(['˚', '°', '≈', '~']),
"{label}: ambient art on transcript row {row} `{text}`:\n{}",
frame.debug_dump()
);
}
}
fn thinking_rows(tui: &mut Harness) -> usize {
let frame = tui.frame();
(0..frame.rows())
.filter(|&row| frame.row(row).contains("Thinking…"))
.count()
}
fn stream_first_turn(tui: &mut Harness, provider: &PacedProvider, thinking_shown: bool) {
tui.type_line(PROMPTS[0]).unwrap();
expect(tui, "the first request and its interrupt hint", |frame| {
provider.turns() == 1 && frame.contains("Esc to interrupt")
});
std::thread::sleep(Duration::from_millis(900));
report(tui, "loading");
assert_status_and_water(tui, "loading");
provider.release(1);
let tail = reasoning_line(REASONING_LINES);
if thinking_shown {
expect(tui, "the live thinking block", |frame| {
frame.contains("Thinking…") && frame.contains(&tail)
});
} else {
expect(tui, "the thinking activity line", |frame| {
frame.contains("Thinking…")
});
}
std::thread::sleep(Duration::from_millis(1300));
report(tui, "thinking");
assert_status_and_water(tui, "thinking");
assert_eq!(thinking_rows(tui), 1, "one Thinking line");
let (_, status) = footer_row(tui);
assert!(
status.contains("thinking ") && !status.contains("reasoning"),
"the clock names the phase: {status}"
);
let frame = tui.frame();
assert!(!frame.contains("reasoning..."), "{}", frame.debug_dump());
assert_eq!(frame.contains(&tail), thinking_shown);
assert!(
!frame.contains(&reasoning_line(1)),
"a long trace stays inside its window:\n{}",
frame.debug_dump()
);
provider.release(1);
expect(tui, "the streamed answer", |frame| frame.contains(ANSWER));
report(tui, "answer");
let frame = tui.frame();
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
assert_eq!(frame.contains("Thought for "), thinking_shown);
assert!(!frame.contains(&tail), "{}", frame.debug_dump());
provider.release(1);
expect(tui, "the settled turn", |frame| {
!frame.contains("Esc to interrupt")
});
settle(tui);
report(tui, "settled");
assert_status_and_water(tui, "settled");
}
#[test]
fn thinking_streams_stops_and_restores_at_the_shipped_widths() {
for size in [(24, 80), (32, 100), (32, 120), (40, 140)] {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
let mut tui = launch(&workspace, &provider, size, "--fresh", true);
stream_first_turn(&mut tui, &provider, true);
tui.type_line(PROMPTS[1]).unwrap();
expect(&mut tui, "the second request", |_| provider.turns() == 2);
provider.release(1);
let tail = reasoning_line(REASONING_LINES);
expect(&mut tui, "thinking before the stop", |frame| {
frame.contains("Thinking…") && frame.contains(&tail)
});
tui.send(keys::key::esc()).unwrap();
expect(&mut tui, "the stopped turn", |frame| {
!frame.contains("Esc to interrupt") && !frame.contains("Thinking…")
});
settle(&mut tui);
report(&mut tui, "stopped");
assert_status_and_water(&mut tui, "stopped");
provider.release(2);
tui.send(keys::key::ctrl('u')).unwrap();
settle(&mut tui);
tui.send(keys::key::ctrl_d()).unwrap();
tui.wait_for_exit(Duration::from_secs(10));
tui.shutdown();
let mut tui = launch(&workspace, &provider, size, "--continue", false);
expect(&mut tui, "the restored conversation", |frame| {
frame.contains(ANSWER)
});
settle(&mut tui);
report(&mut tui, "restored");
assert_status_and_water(&mut tui, "restored");
let frame = tui.frame();
assert!(!frame.contains("Thinking…"), "{}", frame.debug_dump());
tui.shutdown();
}
}
#[test]
fn a_saved_choice_to_hide_thinking_survives_the_new_default() {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, Some("show_thinking = false\n"));
let mut tui = launch(&workspace, &provider, (24, 80), "--fresh", false);
stream_first_turn(&mut tui, &provider, false);
let frame = tui.frame();
assert!(!frame.contains("Thought"), "{}", frame.debug_dump());
tui.shutdown();
let saved: toml::Value = toml::from_str(
&std::fs::read_to_string(workspace.home().join(".codewhale/settings.toml")).unwrap(),
)
.unwrap();
assert_eq!(saved["show_thinking"].as_bool(), Some(false));
}
fn sign_in(workspace: &SealedWorkspace, provider: &PacedProvider, session: &str) {
let path = workspace.home().join(".codewhale/secrets/secrets.json");
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
let secrets =
codewhale_secrets::Secrets::new(Arc::new(codewhale_secrets::FileKeyringStore::new(path)));
let bundle = serde_json::from_value(serde_json::json!({
"tokenType": "Bearer",
"accessToken": "loopback-access",
"refreshToken": "loopback-refresh",
"session": {"id": session},
"user": {"id": "loopback-account"}
}))
.unwrap();
codewhale_secrets::account::AccountSessionStore::new(secrets, None, &provider.account_base())
.save(bundle)
.unwrap();
}
fn run_ungated_turn(tui: &mut Harness, provider: &PacedProvider) {
provider.release(3);
tui.type_line(PROMPTS[0]).unwrap();
expect(tui, "the answer", |frame| frame.contains(ANSWER));
expect(tui, "the settled turn", |frame| {
!frame.contains("Esc to interrupt")
});
}
#[test]
fn account_preferences_notice_is_shown_once_per_sign_in_and_esc_dismisses_it() {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
sign_in(&workspace, &provider, "first-sign-in");
for cols in [80, 120] {
let workspace = make_sealed_workspace().unwrap();
let provider = PacedProvider::start();
prepare(&workspace, &provider, None);
sign_in(&workspace, &provider, "first-sign-in");
let mut tui = launch(&workspace, &provider, (32, cols), "--fresh", false);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the standing notice", |frame| {
frame.contains(NOTICE)
});
report(&mut tui, "account notice");
assert!(provider.profile_requests.load(Ordering::Acquire) > 0);
let (_, status) = footer_row(&mut tui);
assert!(
status.contains(NOTICE),
"the notice is footer chrome: {status}"
);
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
tui.send(keys::key::esc()).unwrap();
expect(&mut tui, "the dismissed notice", |frame| {
!frame.contains(NOTICE)
});
report(&mut tui, "account notice dismissed");
tui.shutdown();
}
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the first notice", |frame| frame.contains(NOTICE));
tui.shutdown();
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
provider.turns.store(0, Ordering::Release);
run_ungated_turn(&mut tui, &provider);
tui.wait_for_idle(Duration::from_secs(6), Duration::from_secs(30))
.unwrap();
report(&mut tui, "same sign-in, later session");
assert!(
!tui.frame().contains(NOTICE),
"the same sign-in is told once"
);
assert!(!tui.frame().contains(RETIRED_TRANSCRIPT_NOTICE));
tui.shutdown();
sign_in(&workspace, &provider, "second-sign-in");
let mut tui = launch(&workspace, &provider, (32, 120), "--fresh", false);
provider.turns.store(0, Ordering::Release);
run_ungated_turn(&mut tui, &provider);
expect(&mut tui, "the notice for a later sign-in", |frame| {
frame.contains(NOTICE)
});
report(&mut tui, "later sign-in");
tui.shutdown();
}
+4 -1
View File
@@ -2230,7 +2230,10 @@ Common settings keys:
`@`-mention completions are populated. `fuzzy` searches the workspace and
applies mention frecency. `browser` lists only the immediate children of the
currently typed directory segment in deterministic alphabetical order.
- `show_thinking` (on/off)
- `show_thinking` (on/off, default on): shows model thinking in the transcript,
live while it streams and collapsed under a `Thought for …` header once the
answer starts. Off hides it; answers and tools stay visible. A value already
saved in `settings.toml` is kept.
- `thinking_default_expanded` (on/off, default off): renders thinking blocks
expanded initially when `show_thinking` is enabled. Space still toggles the
selected block, and it decides only the blocks you have not touched: a block
+1 -1
View File
@@ -805,7 +805,7 @@ owner = "crates/tui/src/commands/groups/session/export.rs"
[[feature]]
id = "live-thinking"
name = "Live thinking"
summary = "/verbose toggles live model thinking in the transcript."
summary = "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full."
status = "stable"
surfaces = { tui = "stable" }
owner = "crates/tui/src/commands/groups/config/mod.rs"
+1 -1
View File
@@ -1152,7 +1152,7 @@ codewhale 还把用户偏好存储在:
- `mention_menu_limit`(整数,默认 `128`):输入区渲染可见窗口之前保留的 `@`-提及弹出候选最大数。可见行仍取决于终端高度。
- `mention_walk_depth`(整数,默认 `10`):`@`-提及补全遍历的最大工作区深度。在深度嵌套的工作区设为 `0` 表示无限深度;在非常大的仓库保持默认,除非需要。
- `mention_menu_behavior`(`fuzzy`、`browser`;默认 `fuzzy`):控制 `@`-提及补全如何填充。`fuzzy` 搜索工作区并应用提及 frecency。`browser` 只按确定性字母顺序列出当前输入目录段的直接子项。
- `show_thinking`(on/off)
- `show_thinking`(on/off,默认 on):在对话记录中显示模型思考,流式输出时实时显示,回答开始后折叠到“思考用时 …”标题下。设为 off 则隐藏思考;回答和工具仍然可见。`settings.toml` 中已保存的值保持不变。
- `thinking_default_expanded`(on/off,默认 off):启用 `show_thinking` 时,让思考块初始以展开状态渲染。Space 仍可切换所选块,并且它只决定你尚未动过的块:你自己展开或折叠过的块,即使之后更改这个设置,也会保持该状态。这在 Space 绑定可能被拦截的 SSH/tmux 环境中很有用。
- `thinking_preview_lines`(整数,默认 `2`):**折叠**的已完成思考仍显示多少正文行。`0` 仅头部;`10` 是较旧的完整显示。实时流式预览不变。用 Space 展开块,或设置 `thinking_default_expanded` 打开每个块。
- `help_expand_groups`(on/off,默认 off):让帮助/快捷键从每个组展开开始。默认折叠长尾(Grok 风格);输入过滤仍展开匹配。
+10 -10
View File
@@ -89,8 +89,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
},
{
"path": "docs/PROVIDERS.md",
@@ -121,8 +121,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
},
{
"path": "docs/LEGACY_PATHS.md",
@@ -153,8 +153,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
@@ -296,8 +296,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
@@ -328,8 +328,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"sha256": "fb934929d28d87dd161a26c427ffa21595ac358d994a46806c53f84fd49cb0f8",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
"sha256": "00f8d3b2b40dc4145a78b5bf93bd6d4e96c83478fdb5b4e0b7316108926ba1b7",
"content": "# Configuration\n\ncodewhale reads configuration from a TOML file plus environment variables.\nAt process startup it may also load literal built-in-provider credentials from\na workspace-local `.env` file. Use the tracked `.env.example` as the template;\ncopy it to `.env`, then add only credential values.\n\nA workspace is not configuration authority. Codewhale therefore ignores\nconfig/profile/home paths, provider/model/base-URL routing, MCP/plugin state,\napproval/sandbox/shell posture, executable paths, runtime settings, and every\nother non-credential `.env` entry. Variable expansion is rejected so a\nrepository cannot substitute an ambient secret into a credential value. Use\n`config.toml`, CLI flags, or values exported by the launching shell for those\nexplicit control-plane settings. `.env` is read from a stable regular-file\nhandle, is capped at 1 MiB, and symbolic links, reparse points, and multiply\nlinked files are rejected.\n\n## Reading and checking configuration from the CLI\n\n`codewhale config get <key>` reads scalar keys, whole tables such as `tools`,\nand nested paths such as `tools.user_input_timeout_seconds`. Displayed tables\nand nested values apply the same recursive credential redaction as `config dump`.\n\n`config set` supports its named scalar keys and the provider, route, and\nnotification commands. It refuses a key that nothing reads and suggests the\nnearest real key (`config set calm_mod on` names `calm_mode`). A settings.toml\nkey such as `calm_mode` or `tool_collapse` is validated and written to the\nuser-global settings.toml, not config.toml; `config get` reads it back from\nthere even when an old config.toml copy (which nothing reads) is still present,\nand names that copy so you can `config unset` it. Settings keys have no project\nscope, so `--project` refuses them. A config.toml key is checked against the\ntype its reader expects and stored as a TOML boolean or number where the\nreader needs one (`yolo = true`, `max_subagents = 4`); `reasoning_effort`\naccepts the same aliases as `/effort` (#6563). Other dotted writes fail before\nmodifying the file and name the TOML table to edit. For example, set a tools\ntimeout in the file as:\n\n```toml\n[tools]\nuser_input_timeout_seconds = 0\n```\n\n`codewhale config doctor` checks credential presence and endpoint shape, and\nwarns about each config.toml root key that nothing reads: a settings.toml key\nleft in config.toml is named as misplaced, and any other unread key gets a\ndid-you-mean. Keys read by any runtime reader are not reported. The warnings do\nnot fail the check, and a clean result does not validate every runtime setting's\nvalue (#6083, #6563).\n\n## Usage and execution statistics\n\n`codewhale stats` is an alias of `codewhale metrics`; both use the same local,\nread-only backend. Use `--json` for the structured `stats` report. For example:\n\n```sh\ncodewhale stats --days 7 --models 10 --tools 20 --roles 10\ncodewhale metrics --project /path/to/workspace --json\n```\n\n`--days N` accepts a positive number of days and conflicts with `--since`.\n`--since 24h` and the existing duration syntax remain available. `--models N`,\n`--tools N` and `--roles N` select the top positive N rows by known tokens,\nidentified tool executions and recorded Fleet tasks respectively. These limits\nonly shorten the display; they do not remove stored receipts.\n\nThe report separates saved-document and Runtime views, frozen native-source\nmoney and token subtotals by actual model, retained tool-result history\nestimates and outcomes, completed compaction observations, and recorded Fleet\nroles. USD and CNY stay separate. A cumulative correction replaces its proven\npredecessor; copied Session/Runtime receipts and inherited Runtime fork rows\nare not added as new provider work. Conflicting source versions are shown\nseparately and excluded from the subtotal. Saved legacy display totals remain\nvisible as document projections without being charged again globally.\n\nCoverage is explicit. A missing quote, usage receipt, tool outcome, output\nshape or recovery link is unknown, not zero. Empty scalar usage from older\ntransport records cannot prove a reported zero. Token counts describe known\nreported data; tool history tokens are estimates of retained text, not bytes\nproven to have been replayed on a provider request. Latest prepared-request\ncomponents and source-linked compaction/role economics are unavailable where\nthey were not captured. The report does not infer provider invoices or prove\ncomplete historical coverage.\n\n`--project WORKSPACE` filters current saved/thread/Fleet views. Original source\nworkspace is not yet captured, so native money and model subtotals are\nunavailable for that filter; mutable current metadata is not used to assign a\ncharge. Older receipt projections without reliable workspace scope are also\nunavailable under this filter. Without it, those legacy receipt views remain\nseparate fromLine truncated
}
]
},
+2 -2
View File
@@ -2,7 +2,7 @@
"format": "codewhale.features.v1",
"source": {
"path": "docs/features.toml",
"sha256": "4c4dfe17cea716328253c687d9d0665e1b24fc3b59d00f2a81cf1157dfea7072"
"sha256": "a63042dc25a658f626735cc40e7a7cf4c67eaa51bfbb037dc2820d083d10fe3a"
},
"features": [
{
@@ -1070,7 +1070,7 @@
{
"id": "live-thinking",
"name": "Live thinking",
"summary": "/verbose toggles live model thinking in the transcript.",
"summary": "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full.",
"status": "stable",
"surfaces": {
"tui": "stable"