Compare commits

..
1 Commits
Author SHA1 Message Date
CodeWhale BotandClaude Opus 5.5 622a493078 feat(tui-kit): animated data displays an agent can send as JSON (CW-8202)
The vendored terminal kit could move things (MotionStep, MotionPolicy) but
had nothing that shows numbers. This adds three displays and a data
description of them:

- DataSparkline: one row, newest values on the right, with now/min/max in
  words. DataBars: a titled list of bars in eighth-cell steps that grow
  with a stagger and glide from previous values. DataTrend: a braille line
  chart with its range on the axis; it draws in from the left with a
  body-ink pen and falls back to a sparkline when too small.
- Every value is in the words, so colour and ink never carry a fact alone,
  and ASCII and NoColor profiles keep the rules. Draw-in reads a caller
  progress from the new MotionStep::drawn, so reduced and still motion
  paint the end state and the kit still never reads a clock.
- DataDisplay is the same vocabulary as serde JSON, borrowing the UI SDK
  idea (vercel-labs/ui) that a component says what it is and holds as data
  so an agent reads and writes facts, not pixels. from_json validates at
  the trust boundary (finite numbers, <=1024 points, <=24 bars, labels
  <=120 chars, trend rows 3..=16); describe() gives the plain-text facts;
  DATA_DISPLAY_SCHEMA is the tool schema.
- Gallery entries (6), examples/data.rs (live dashboard, --json FILE for an
  agent-sent display, --text, --svg DIR), CHANGELOG and COMPONENTS rows.

Not in this slice: an Engine tool or transcript event that lets the model
emit a DataDisplay. README's gallery count (199) and rendered previews are
not regenerated for the 6 new entries.

Checks (kit crate, standalone target): cargo test --lib 137 passed, 0
failed (13 new in components::chart, incl. rule checks over 9 profiles x
40/80/120 cols and an insta snapshot); cargo test --doc 11 passed; cargo
clippy --all-targets -D warnings clean; cargo fmt --check clean. No Engine
crate glob-imports the kit or uses the new names; Engine build not run
locally, CI covers it on landing.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-11-12 18:11:16 -08:00
44 changed files with 2110 additions and 886 deletions

No files matched your search

+1 -7
View File
@@ -605,7 +605,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
def(
"show_thinking",
SettingKind::Bool(SHOW_THINKING),
"true",
"false",
ui(
TAB_APPEARANCE,
"display",
@@ -1546,12 +1546,6 @@ 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",
+10 -26
View File
@@ -244,38 +244,22 @@ pub async fn capture(
if let Some(snapshot) = supplied {
return snapshot.render().map(Some);
}
#[cfg(any(test, feature = "test-support"))]
{
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());
}
}
// 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);
}
}
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>> =
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1668,7 +1667,7 @@
"PhaseIdle": "inactiu",
"PhaseDraft": "esborrany",
"PhaseWorking": "treballant",
"PhaseReasoning": "pensant",
"PhaseReasoning": "raonant",
"PhaseReading": "llegint",
"PhaseUsingTool": "usant una eina",
"PhaseSubagents": "subagents en execució",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1668,7 +1667,7 @@
"PhaseIdle": "ruhend",
"PhaseDraft": "entwurf",
"PhaseWorking": "arbeitet",
"PhaseReasoning": "denkt nach",
"PhaseReasoning": "überlegt",
"PhaseReading": "liest",
"PhaseUsingTool": "nutzt Tool",
"PhaseSubagents": "Subagenten laufen",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "idle",
"PhaseDraft": "draft",
"PhaseWorking": "working",
"PhaseReasoning": "thinking",
"PhaseReasoning": "reasoning",
"PhaseReading": "reading",
"PhaseUsingTool": "using tool",
"PhaseSubagents": "agents underway",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "inactivo",
"PhaseDraft": "borrador",
"PhaseWorking": "trabajando",
"PhaseReasoning": "pensando",
"PhaseReasoning": "razonando",
"PhaseReading": "leyendo",
"PhaseUsingTool": "usando herramienta",
"PhaseSubagents": "subagentes en curso",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1668,7 +1667,7 @@
"PhaseIdle": "inactif",
"PhaseDraft": "brouillon",
"PhaseWorking": "travail en cours",
"PhaseReasoning": "réflexion",
"PhaseReasoning": "raisonnement",
"PhaseReading": "lecture",
"PhaseUsingTool": "utilisation d'outil",
"PhaseSubagents": "sous-agents en cours",
+1 -2
View File
@@ -25,7 +25,6 @@
"TerminalDockLive": "सक्रिय",
"ExtensionPromptUnavailable": "एक्सटेंशन के निर्देश उपलब्ध नहीं हैं; /plugin show से जाँचें।",
"ProfileConstitutionUnavailableLocal": "Codewhale खाते की प्राथमिकताएँ लोड नहीं हो सकीं, इसलिए यह टर्न आपकी स्थानीय सेटिंग्स का उपयोग करता है। आपकी प्रदाता कुंजी काम करती रहेगी; खाते की प्राथमिकताएँ वापस लाने के लिए codewhale login चलाएँ।",
"AccountPreferencesNotLoaded": "खाता लोड नहीं हुआ। {command} चलाएँ। आपकी प्रदाता कुंजी काम करती रहेगी।",
"ComposerOversizedSubmitHeld": "नहीं भेजा गया: इस संदेश में {limit} से अधिक वर्ण हैं और इसे पेस्ट फ़ाइल के रूप में सहेजा नहीं जा सका ({error})। पूरा पाठ अभी भी इनपुट बॉक्स में है: इसे छोटा करें या .codewhale/pastes में लिखने की अनुमति दें, फिर Enter दबाएँ।",
"ComposerOversizedSubmitHeldShort": "नहीं भेजा गया: {limit} से अधिक वर्ण; पेस्ट फ़ाइल सहेजी नहीं गई।",
"CmdPluginOwnerReport": "एक्सटेंशन होस्ट:\n स्थिति: {state}\n सक्रिय टूल ({count}): {tools}",
@@ -1668,7 +1667,7 @@
"PhaseIdle": "निष्क्रिय",
"PhaseDraft": "मसौदा",
"PhaseWorking": "काम चल रहा",
"PhaseReasoning": "सोच रहा",
"PhaseReasoning": "तर्क चल रहा",
"PhaseReading": "पढ़ रहा",
"PhaseUsingTool": "टूल इस्तेमाल हो रहा",
"PhaseSubagents": "सबएजेंट चल रहे हैं",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1668,7 +1667,7 @@
"PhaseIdle": "diam",
"PhaseDraft": "draf",
"PhaseWorking": "bekerja",
"PhaseReasoning": "berpikir",
"PhaseReasoning": "menalar",
"PhaseReading": "membaca",
"PhaseUsingTool": "memakai tool",
"PhaseSubagents": "subagen berjalan",
-1
View File
@@ -25,7 +25,6 @@
"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 -2
View File
@@ -25,7 +25,6 @@
"TerminalDockLive": "실행 중",
"ExtensionPromptUnavailable": "확장 기능의 지침을 사용할 수 없습니다. /plugin show에서 확인하세요.",
"ProfileConstitutionUnavailableLocal": "Codewhale 계정 설정을 불러오지 못해 이번 턴은 로컬 설정을 사용합니다. 공급자 키는 계속 사용할 수 있으며, 계정 설정을 복원하려면 codewhale login을 실행하세요.",
"AccountPreferencesNotLoaded": "계정을 불러오지 못했습니다. {command}을 실행하세요. 공급자 키는 계속 사용할 수 있습니다.",
"ComposerOversizedSubmitHeld": "전송하지 못했습니다. 메시지가 {limit}자를 초과하며 붙여넣기 파일로 저장할 수 없습니다({error}). 전체 텍스트는 입력란에 그대로 있습니다. 내용을 줄이거나 .codewhale/pastes에 쓰기 권한을 부여한 뒤 Enter를 다시 누르세요.",
"ComposerOversizedSubmitHeldShort": "전송 안 됨: {limit}자 초과, 붙여넣기 파일 저장 실패.",
"CmdPluginOwnerReport": "확장 호스트:\n 상태: {state}\n 활성 도구 ({count}): {tools}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "유휴",
"PhaseDraft": "초안",
"PhaseWorking": "작업 중",
"PhaseReasoning": "생각 중",
"PhaseReasoning": "추론 중",
"PhaseReading": "읽는 중",
"PhaseUsingTool": "도구 사용 중",
"PhaseSubagents": "서브에이전트 실행 중",
+1 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "ocioso",
"PhaseDraft": "rascunho",
"PhaseWorking": "trabalhando",
"PhaseReasoning": "pensando",
"PhaseReasoning": "raciocinando",
"PhaseReading": "lendo",
"PhaseUsingTool": "usando ferramenta",
"PhaseSubagents": "subagentes em andamento",
-1
View File
@@ -25,7 +25,6 @@
"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,7 +25,6 @@
"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 -2
View File
@@ -25,7 +25,6 @@
"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}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "rảnh",
"PhaseDraft": "nháp",
"PhaseWorking": "đang xử lý",
"PhaseReasoning": "đang suy nghĩ",
"PhaseReasoning": "đang suy luận",
"PhaseReading": "đang đọc",
"PhaseUsingTool": "đang dùng công cụ",
"PhaseSubagents": "đang chạy tác tử con",
+1 -2
View File
@@ -25,7 +25,6 @@
"TerminalDockLive": "运行中",
"ExtensionPromptUnavailable": "扩展指令不可用;请通过 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "无法加载 Codewhale 账号偏好,本轮改用本地设置。你的提供商密钥仍可正常使用;运行 codewhale login 可恢复账号偏好。",
"AccountPreferencesNotLoaded": "账号未加载。请运行 {command}。你的提供商密钥仍可正常使用。",
"ComposerOversizedSubmitHeld": "未发送:消息超过 {limit} 个字符,无法保存为粘贴文本文件({error})。完整文本仍保留在输入框中:请缩短内容,或允许写入 .codewhale/pastes,然后再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未发送:超过 {limit} 个字符;粘贴文件未保存。",
"CmdPluginOwnerReport": "扩展宿主:\n 状态:{state}\n 活动工具({count}):{tools}",
@@ -1691,7 +1690,7 @@
"PhaseIdle": "空闲",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "思考中",
"PhaseReasoning": "推理中",
"PhaseReading": "读取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "智能体工作中",
+1 -2
View File
@@ -25,7 +25,6 @@
"TerminalDockLive": "執行中",
"ExtensionPromptUnavailable": "擴充功能指令無法使用;請透過 /plugin show 查看。",
"ProfileConstitutionUnavailableLocal": "無法載入 Codewhale 帳號偏好,本輪改用本機設定。你的服務商金鑰仍可正常使用;執行 codewhale login 可恢復帳號偏好。",
"AccountPreferencesNotLoaded": "帳號未載入。請執行 {command}。你的服務商金鑰仍可正常使用。",
"ComposerOversizedSubmitHeld": "未傳送:訊息超過 {limit} 個字元,無法儲存為貼上文字檔案({error})。完整文字仍保留在輸入框中:請縮短內容,或允許寫入 .codewhale/pastes,然後再次按 Enter。",
"ComposerOversizedSubmitHeldShort": "未傳送:超過 {limit} 個字元;貼上檔案未儲存。",
"CmdPluginOwnerReport": "擴充功能主機:\n 狀態:{state}\n 活動工具({count}):{tools}",
@@ -1689,7 +1688,7 @@
"PhaseIdle": "閒置",
"PhaseDraft": "草稿",
"PhaseWorking": "工作中",
"PhaseReasoning": "思考中",
"PhaseReasoning": "推理中",
"PhaseReading": "讀取中",
"PhaseUsingTool": "使用工具中",
"PhaseSubagents": "代理程式工作中",
-2
View File
@@ -714,7 +714,6 @@ pub enum MessageId {
ExtensionsInventorySkills,
ExtensionPromptUnavailable,
ProfileConstitutionUnavailableLocal,
AccountPreferencesNotLoaded,
ExtensionsMarketplaceDetail,
ExtensionsMarketplaceUnavailable,
ExtensionsMcpEmpty,
@@ -3652,7 +3651,6 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ExtensionsInventorySkills,
MessageId::ExtensionPromptUnavailable,
MessageId::ProfileConstitutionUnavailableLocal,
MessageId::AccountPreferencesNotLoaded,
MessageId::ExtensionsMarketplaceDetail,
MessageId::ExtensionsMarketplaceUnavailable,
MessageId::ExtensionsMcpEmpty,
+6 -37
View File
@@ -492,8 +492,6 @@ 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
@@ -652,7 +650,9 @@ impl Default for Settings {
mention_menu_limit: 128,
mention_walk_depth: 10,
mention_menu_behavior: "fuzzy".to_string(),
show_thinking: true,
// Reasoning is useful when explicitly requested, but it should
// never displace the actual conversation in the default TUI.
show_thinking: false,
thinking_default_expanded: false,
thinking_preview_lines: default_thinking_preview_lines(),
thinking_highlight: true,
@@ -694,7 +694,6 @@ 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(),
@@ -3748,36 +3747,6 @@ 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();
@@ -3917,7 +3886,7 @@ mod tests {
let mut settings = Settings::default();
// Density is calm by default; motion is an independent axis.
assert!(settings.calm_mode);
settings.show_thinking = false;
assert!(!settings.show_thinking);
let changed = settings.apply_preset("CALM").expect("calm preset applies");
assert_eq!(
@@ -3947,8 +3916,8 @@ mod tests {
assert!(settings.fancy_animations);
assert_eq!(settings.transcript_spacing, "spacious");
assert_eq!(settings.tool_collapse_mode, "compact");
assert!(settings.show_thinking);
assert!(!settings.thinking_default_expanded);
// Thinking is opt-in so the transcript stays focused on the chat.
assert!(!settings.show_thinking);
}
#[test]
-54
View File
@@ -87,27 +87,6 @@ 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
@@ -2469,7 +2448,6 @@ 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.
@@ -3813,38 +3791,6 @@ 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,7 +1142,6 @@ 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,46 +184,6 @@ 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 On [x] │source settings.toml
│ Model reasoning in chat Off [ ] │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 On [x] SAVED █
Model reasoning in chat Off [ ] SAVED █
Thinking Default Expanded Off [ ] SAVED █
Thinking Preview Lines 2 ✎ SAVED │
Thinking background highlight On [x] SAVED │
+6 -46
View File
@@ -30,17 +30,15 @@ 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 — 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.
/// never dropped — it lands in the cell immediately — 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, 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.
/// 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.
fn bump_thinking_revision_throttled(app: &mut App, now: Instant) -> bool {
let due = app
.thinking_revision_last_bump_at
@@ -49,17 +47,9 @@ 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
@@ -86,8 +76,7 @@ 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, a later display beat or the forced finalize bump, so no
/// content is ever lost.
/// the next append 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());
}
@@ -319,7 +308,6 @@ 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
}
@@ -438,34 +426,6 @@ 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.
+11 -9
View File
@@ -7232,16 +7232,14 @@ pub(crate) async fn run_event_loop(
continue;
}
// An idle operator can dismiss the persistent context warning
// or the account preferences notice without affecting vim or
// attachment handling. While a turn is active Esc retains its
// cancellation meaning.
// 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_account_preferences_notice()) =>
&& app.dismiss_context_pressure_warning() =>
{
continue;
}
@@ -9328,10 +9326,14 @@ pub(super) fn apply_engine_status(app: &mut App, message: String) -> bool {
app.add_message(HistoryCell::System { content: message });
return true;
}
if app.tr(MessageId::ProfileConstitutionUnavailableLocal) == message.as_str() {
app.notify_account_preferences_not_loaded();
return false;
// 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(),
});
}
app.status_message = Some(message);
false
retain
}
+1 -4
View File
@@ -1555,10 +1555,7 @@ pub(crate) fn commit_streaming_display_tick(
updated = true;
}
if streaming_thinking::render_owed_revision(app, now) {
updated = true;
}
if app.streaming_state.has_pending_stream_text(0) || app.thinking_revision_owed {
if app.streaming_state.has_pending_stream_text(0) {
stream_display_clock.note_delta(now);
}
+8 -26
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("[∿ thinking 1/1 · running]"), "{latest}");
assert!(latest.contains("[∿ reasoning 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",
"[∿ thinking 1/1 · Done]",
"[∿ reasoning 1/1 · Done]",
"FIRST-THOUGHT",
"full reasoning tail",
"[⚙ using tool]",
@@ -35488,41 +35488,23 @@ fn extension_prompt_origin_keeps_denials_and_other_policy_holds() {
}
#[test]
fn account_profile_fallback_notice_is_a_standing_notice_outside_the_transcript() {
fn account_profile_fallback_notice_stays_in_the_transcript() {
let _home = SettingsHomeGuard::new();
let mut app = App::new(create_test_options(), &Config::default());
let engine_notice = app
let notice = app
.tr(MessageId::ProfileConstitutionUnavailableLocal)
.to_string();
let before = app.history.len();
let status_before = app.status_message.clone();
assert!(!super::event_loop::apply_engine_status(
assert!(super::event_loop::apply_engine_status(
&mut app,
engine_notice
notice.clone()
));
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);
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());
assert_eq!(app.history.len(), before + 1);
assert!(matches!(&app.history[before], HistoryCell::System { content } if *content == notice));
}
#[test]
-2
View File
@@ -32,8 +32,6 @@ 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;
@@ -1,585 +0,0 @@
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();
}
+1 -4
View File
@@ -2230,10 +2230,7 @@ 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, 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.
- `show_thinking` (on/off)
- `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 = "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full."
summary = "/verbose toggles live model thinking in the transcript."
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,默认 on):在对话记录中显示模型思考,流式输出时实时显示,回答开始后折叠到“思考用时 …”标题下。设为 off 则隐藏思考;回答和工具仍然可见。`settings.toml` 中已保存的值保持不变。
- `show_thinking`(on/off)
- `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
View File
@@ -2,6 +2,16 @@
## Unreleased — 0.2.0
- Add data displays: `DataSparkline`, `DataBars` and `DataTrend` (a braille
line chart). The words carry every value; the ink draws in from a caller
progress (`MotionStep::drawn`) and is at its end state under reduced or
still motion. Bars glide from their previous values.
- Add `DataDisplay`, the same displays as JSON an agent can send:
`DataDisplay::from_json` validates it (finite numbers, at most 1024 points
or 24 bars, labels up to 120 characters), `describe` says the same facts as
plain text, and `DATA_DISPLAY_SCHEMA` is its tool schema. Try
`cargo run --example data`.
- Reexport the shared avatar contract from Codewhale Protocol. Rendering and
artwork stay in this library. Protocol 0.10.3 registry proof must precede
this package publication.
+1
View File
@@ -57,6 +57,7 @@ Gallery names are the exact names accepted by the gallery example; the
| Engine `agent_roster.rs::render_agent_roster`, `widgets/workflow_panel.rs::row_receipt_text`, `gate_receipts.rs`; GPUI `usage.rs::TurnRow`, `workbar.rs::Readout`, `working_context.rs::ExecutionReceipt` | `Receipt`, `ReceiptTable`, `ReceiptValue`, `Cost`, formatting helpers | Primitive/composite | `receipt-row`, `receipt-table`, `receipt-table-compact`, `receipt-table-minimal`, `receipt-table-clipped` |
| Engine `diff_render.rs::BoundedDiffRender`, `render_diff_bounded`, `history/file_mutation.rs`; GPUI `review.rs::DiffRowKind`, `workspace/render/review.rs::ReviewRows` | `Diff`, `DiffLine`, `DiffKind`, `DiffGutter`, `DiffHighlight`, `parse_unified` | Primitive | `diff`, `diff-wrapped`, `diff-no-numbers`, `diff-highlighted`, `review-scene` |
| Engine `widgets/workflow_panel.rs::WorkflowPanel`, `WorkflowPanelRow`, `history/checklist.rs`; GPUI `plans.rs`, `workspace/render/workers.rs` | `WorkflowTree`, `TreeNode`, `TreeState`, `CountBar` | Primitive/composite | `workflow-tree`, `workflow-tree-selected`, `workflow-tree-clipped`, `workflow-tree-long`, `count-bars` |
| Kit-native; data from the host or an agent's JSON (CW-8202) | `DataSparkline`, `DataBars`, `DataBarItem`, `DataTrend`, `DataDisplay`, `DataDisplayView`, `DataWords`, `DATA_DISPLAY_SCHEMA`, `MotionStep::drawn` | Primitive | `data-sparkline`, `data-bars`, `data-bars-growing`, `data-trend`, `data-trend-drawing`, `data-from-agent` |
| Engine `approval/view.rs::ApprovalView`, `approval/elevation.rs::ElevationView`, `auto_review.rs`; GPUI `workspace/render/conversation.rs::Workspace::render_attention`, `workspace/render/review.rs::Workspace::render_restore_confirmation` | `ApprovalCard`, `ApprovalSubject`, `ApprovalState`, `ApprovalChoice`, `ApprovalPaint`, `DecisionBand`, `DecisionBandPlan`, `DecisionBandAction`, `DecisionBandSave`, `ReviewVerdict`, `ReviewAggregate` | Composite | `approval-command`, `approval-outside`, `approval-patch`, `approval-elevation`, `approval-clipped`, `approval-spoofed`, `approval-native-band`, `approval-native-band-collapsed`, `review-verdicts`, `review-aggregate` |
| GPUI `workspace/render/conversation.rs::Workspace::render_attention`, `workspace/render/files.rs::Workspace::render_artifact_column` | `AttentionQueue`, `AttentionItem`, `ArtifactShelf`, `Artifact` | Composite | `attention-queue`, `attention-focused`, `attention-narrow`, `attention-empty`, `artifact`, `artifact-shelf`, `artifact-narrow`, `artifact-unknown`, `artifact-empty` |
| Engine `views/mod.rs::ConfigView`, `ConfigRow`, `ConfigView::render_setting_detail`; GPUI `settings.rs::Spec`, `Applies`, `workspace/settings/mod.rs::Settings` | `SettingRow`, `SettingDetail`, `SettingWords` | Composite | `setting-row`, `setting-detail` |
+276
View File
@@ -0,0 +1,276 @@
//! Live data displays: `cargo run --example data`.
//!
//! A sparkline takes a new value every 400 ms, the trend draws in, and the
//! bars glide to new values every three seconds. r replays, m switches
//! between full and reduced motion, q or Escape exits.
//!
//! `--json FILE` shows one agent-sent [`DataDisplay`] instead.
//! `--text` prints the final frame and each display's plain description.
//! `--svg DIR` writes frames at fixed draw-in points.
use std::{
io,
path::PathBuf,
time::{Duration, Instant},
};
use codewhale_ratatui::{
DataBarItem, DataDisplay, DataWords, MotionEasing, MotionMode, MotionPolicy, MotionStep,
MotionTiming, Paint, Role, Theme,
testing::{self, Profile},
};
use crossterm::{
event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use ratatui::{Terminal, backend::CrosstermBackend, buffer::Buffer, layout::Rect};
const TICK: Duration = Duration::from_millis(400);
const RESHUFFLE: Duration = Duration::from_secs(3);
const DRAW_IN: MotionTiming =
MotionTiming::space(Duration::from_millis(1_400), MotionEasing::EaseOut);
const GLIDE: MotionTiming = MotionTiming::PANEL;
struct Noise(u64);
impl Noise {
fn next(&mut self) -> f64 {
self.0 = self
.0
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1);
(self.0 >> 33) as f64 / f64::from(1u32 << 31)
}
}
struct Board {
noise: Noise,
requests: Vec<f64>,
latency: Vec<f64>,
bars: DataDisplay,
bars_before: DataDisplay,
draw: MotionStep,
glide: MotionStep,
agent: Option<DataDisplay>,
}
fn crate_bars(noise: &mut Noise) -> DataDisplay {
DataDisplay::Bars {
label: "Tests by crate".into(),
unit: "tests".into(),
items: ["engine", "tui", "runtime", "protocol", "config"]
.into_iter()
.map(|name| DataBarItem::new(name, (40.0 + noise.next() * 400.0).round()))
.collect(),
}
}
impl Board {
fn new(agent: Option<DataDisplay>) -> Self {
let mut noise = Noise(7);
let latency = (0..96)
.map(|i| 80.0 + 45.0 * (f64::from(i) / 9.0).sin() + noise.next() * 30.0)
.collect();
let requests = (0..40).map(|_| 20.0 + noise.next() * 60.0).collect();
let bars = crate_bars(&mut noise);
Self {
noise,
requests,
latency,
bars_before: DataDisplay::Bars {
label: String::new(),
unit: String::new(),
items: Vec::new(),
},
bars,
draw: MotionStep::at_rest(0.0),
glide: MotionStep::at_rest(1.0),
agent,
}
}
fn replay(&mut self, now: Instant, policy: MotionPolicy) {
self.draw = MotionStep::at_rest(0.0);
self.draw.go(1.0, now, DRAW_IN, policy);
}
fn tick(&mut self) {
let last = self.requests.last().copied().unwrap_or(50.0);
let next = (last + (self.noise.next() - 0.5) * 30.0).clamp(5.0, 120.0);
self.requests.push(next.round());
if self.requests.len() > 200 {
self.requests.remove(0);
}
}
fn reshuffle(&mut self, now: Instant, policy: MotionPolicy) {
self.bars_before = std::mem::replace(&mut self.bars, crate_bars(&mut self.noise));
self.glide = MotionStep::at_rest(0.0);
self.glide.go(1.0, now, GLIDE, policy);
}
fn paint(&self, area: Rect, buf: &mut Buffer, theme: &Theme, draw: f32, glide: f32) {
buf.set_style(area, theme.bg(Role::Background));
let inner = Rect::new(
area.x + 2,
area.y + 1,
area.width.saturating_sub(4),
area.height.saturating_sub(2),
);
if let Some(agent) = &self.agent {
agent.view().drawn(draw).paint(inner, buf, theme);
return;
}
let row =
|y: u16, h: u16| Rect::new(inner.x, inner.y + y, inner.width, h).intersection(inner);
let requests = DataDisplay::Sparkline {
label: "Requests".into(),
unit: "rpm".into(),
values: self.requests.clone(),
};
let latency = DataDisplay::Trend {
label: "Latency".into(),
unit: "ms".into(),
values: self.latency.clone(),
rows: Some(9),
};
requests.view().drawn(draw).paint(row(0, 1), buf, theme);
latency.view().drawn(draw).paint(row(2, 9), buf, theme);
self.bars
.view()
.from(&self.bars_before)
.drawn(glide.min(draw))
.paint(row(12, 6), buf, theme);
}
fn displays(&self) -> Vec<DataDisplay> {
if let Some(agent) = &self.agent {
return vec![agent.clone()];
}
vec![
DataDisplay::Sparkline {
label: "Requests".into(),
unit: "rpm".into(),
values: self.requests.clone(),
},
DataDisplay::Trend {
label: "Latency".into(),
unit: "ms".into(),
values: self.latency.clone(),
rows: Some(9),
},
self.bars.clone(),
]
}
}
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
let value = |flag: &str| {
args.iter()
.position(|a| a == flag)
.and_then(|i| args.get(i + 1))
.cloned()
};
let agent = match value("--json") {
Some(path) => {
let json = std::fs::read_to_string(&path)?;
match DataDisplay::from_json(&json) {
Ok(shown) => Some(shown),
Err(e) => {
eprintln!("{path}: {e}");
std::process::exit(2);
}
}
}
None => None,
};
let board = Board::new(agent);
if args.iter().any(|a| a == "--text") {
let buf = testing::render(84, 20, |area, buf| {
board.paint(area, buf, &Profile::DarkTrue.theme(), 1.0, 1.0);
});
println!("{}", testing::text(&buf));
for shown in board.displays() {
println!("{}", shown.describe(&DataWords::default()));
}
return Ok(());
}
if let Some(dir) = value("--svg") {
let dir = PathBuf::from(dir);
std::fs::create_dir_all(&dir)?;
let theme = Profile::DarkTrue.theme();
for (i, at) in [0.2_f32, 0.5, 0.8, 1.0].into_iter().enumerate() {
let buf = testing::render(84, 20, |area, buf| board.paint(area, buf, &theme, at, at));
let path = dir.join(format!("data-{i}.svg"));
std::fs::write(&path, testing::svg(&buf, &theme))?;
println!("{}", path.display());
}
return Ok(());
}
run(board)
}
fn run(mut board: Board) -> io::Result<()> {
enable_raw_mode()?;
let mut out = io::stdout();
execute!(out, EnterAlternateScreen)?;
let mut terminal = Terminal::new(CrosstermBackend::new(out))?;
let theme = Theme::detect().tui();
let mut mode = MotionMode::Full;
let mut policy = MotionPolicy::new(mode, &theme);
let start = Instant::now();
board.replay(start, policy);
let (mut next_tick, mut next_shuffle) = (start + TICK, start + RESHUFFLE);
let result = loop {
let now = Instant::now();
if now >= next_tick {
board.tick();
next_tick = now + TICK;
}
if now >= next_shuffle {
board.reshuffle(now, policy);
next_shuffle = now + RESHUFFLE;
}
let (draw, glide) = (
board.draw.drawn(now, policy),
board.glide.drawn(now, policy),
);
terminal.draw(|f| board.paint(f.area(), f.buffer_mut(), &theme, draw, glide))?;
let due = [
board.draw.next_frame_at(now, policy),
board.glide.next_frame_at(now, policy),
]
.into_iter()
.flatten()
.chain([next_tick, next_shuffle])
.min()
.unwrap_or(next_tick);
if event::poll(due.saturating_duration_since(Instant::now()))?
&& let Event::Key(key) = event::read()?
&& key.kind != KeyEventKind::Release
{
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break Ok(()),
KeyCode::Char('r') => board.replay(Instant::now(), policy),
KeyCode::Char('m') => {
mode = if mode == MotionMode::Full {
MotionMode::Reduced
} else {
MotionMode::Full
};
policy = MotionPolicy::new(mode, &theme);
}
_ => {}
}
}
};
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
result
}
File diff suppressed because it is too large. Load diff
+2
View File
@@ -59,10 +59,12 @@ pub use tabs::*;
pub use toggle::*;
// Package Display.
mod chart;
mod diff;
mod progress;
mod receipt;
mod tree;
pub use chart::*;
pub use diff::*;
pub use progress::*;
pub use receipt::*;
+8
View File
@@ -366,6 +366,14 @@ impl MotionStep {
}
}
/// Progress for a draw-in, in `0..=1`: eased where `policy` lets space
/// move, the target where it does not. Feed it to a data display's
/// `drawn`.
#[must_use]
pub fn drawn(&self, now: Instant, policy: MotionPolicy) -> f32 {
self.shown(now, policy, MotionChannel::Space)
}
/// A column between `from` and `to`: slide a marker, nudge a panel.
#[must_use]
pub fn offset(&self, now: Instant, policy: MotionPolicy, from: u16, to: u16) -> u16 {
@@ -0,0 +1,360 @@
---
source: src/components/chart.rs
assertion_line: 1242
expression: "testing::snapshot(12, |area, buf, theme|\n{\n let row = |y: u16, h: u16|\n { Rect::new(area.x, area.y + y, area.width, h).intersection(area) };\n DataSparkline::new(\"Requests\",\n &values).unit(\"rpm\").paint(row(0, 1), buf, theme);\n DataBars::new(\"Tokens by model\",\n &items).unit(\"tokens\").paint(row(2, 5), buf, theme);\n DataTrend::new(\"Latency\",\n &values).unit(\"ms\").rows(5).paint(row(7, 5), buf, theme);\n})"
---
== dark-truecolor · 40x12
Requests ▁▃▅▄▅▆▇███▆▅▄▃▂▁ now 45.6 rpm
Tokens by model
opus ██████████████████ 12.4k tokens
sonnet ███████████▌ 7900 tokens
haiku ████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms
69.7 ms │ ⡠⠒⠑⠒⠒⠢⣀ ⡀⡠⠔⠉⠉⢢⡀
│⢀⠔⠁ ⢇ ⢀⠔⠉ ⠈⠑⢄
│⠁ ⠑⠢⡀ ⡠⠊
17.2 ms │ ⠈⠱⣀⣀⠤⠊⠊
---
r0 c0..8 fg=Foreground bg=Background bold
r0 c8..10 bg=Background
r0 c10..26 fg=Accent bg=Background
r0 c26..28 bg=Background
r0 c28..32 fg=Muted bg=Background
r0 c32..40 fg=Foreground bg=Background
r1 c0..40 bg=Background
r2 c0..15 fg=Foreground bg=Background bold
r2 c15..40 bg=Background
r3 c0..6 fg=Foreground bg=Background
r3 c6..8 bg=Background
r3 c8..26 fg=Accent bg=Background
r3 c26..28 bg=Background
r3 c28..40 fg=Muted bg=Background
r4 c0..6 fg=Foreground bg=Background
r4 c6..8 bg=Background
r4 c8..20 fg=Accent bg=Background
r4 c20..28 bg=Background
r4 c28..39 fg=Muted bg=Background
r4 c39..40 bg=Background
r5 c0..6 fg=Foreground bg=Background
r5 c6..8 bg=Background
r5 c8..13 fg=Accent bg=Background
r5 c13..28 bg=Background
r5 c28..39 fg=Muted bg=Background
r5 c39..40 bg=Background
r6 c0..6 fg=Foreground bg=Background
r6 c6..28 bg=Background
r6 c28..36 fg=Muted bg=Background
r6 c36..40 bg=Background
r7 c0..7 fg=Foreground bg=Background bold
r7 c7..9 bg=Background
r7 c9..13 fg=Muted bg=Background
r7 c13..20 fg=Foreground bg=Background
r7 c20..40 bg=Background
r8 c0..8 fg=Muted bg=Background
r8 c8..9 fg=Dim bg=Background
r8 c9..11 bg=Background
r8 c11..18 fg=Accent bg=Background
r8 c18..31 bg=Background
r8 c31..38 fg=Accent bg=Background
r8 c38..40 bg=Background
r9 c0..8 fg=Muted bg=Background
r9 c8..9 fg=Dim bg=Background
r9 c9..12 fg=Accent bg=Background
r9 c12..18 bg=Background
r9 c18..19 fg=Accent bg=Background
r9 c19..29 bg=Background
r9 c29..32 fg=Accent bg=Background
r9 c32..37 bg=Background
r9 c37..40 fg=Accent bg=Background
r10 c0..8 fg=Muted bg=Background
r10 c8..9 fg=Dim bg=Background
r10 c9..10 fg=Accent bg=Background
r10 c10..19 bg=Background
r10 c19..22 fg=Accent bg=Background
r10 c22..28 bg=Background
r10 c28..30 fg=Accent bg=Background
r10 c30..40 bg=Background
r11 c0..8 fg=Muted bg=Background
r11 c8..9 fg=Dim bg=Background
r11 c9..21 bg=Background
r11 c21..28 fg=Accent bg=Background
r11 c28..40 bg=Background
== dark-truecolor · 80x12
Requests ▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ██████████████████████████████████████████████████████████ 12.4k tokens
sonnet █████████████████████████████████████ 7900 tokens
haiku ██████████████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms │ ⣀⠤⠒⠊⠑⠒⠒⠒⠒⠒⠒⠢⠤⢄⣀ ⣀ ⣀⡠⠤⠒⠒⠉⠉⠉⠉⠢⢄⣀⣀
│ ⣀⠤⠒⠉ ⠑⠤⣀ ⢀⡠⠒⠉ ⠉ ⠉⠑⠒⠤⣀
│⠉ ⠉⠑⠒⠤⢄⣀ ⡠⠔⠊⠁
17.2 ms │ ⠉⠑⢄⣀⣀⣀⣀⣀⠤⠤⠒⠊⠒⠊⠉
---
r0 c0..8 fg=Foreground bg=Background bold
r0 c8..10 bg=Background
r0 c10..38 fg=Accent bg=Background
r0 c38..40 bg=Background
r0 c40..44 fg=Muted bg=Background
r0 c44..52 fg=Foreground bg=Background
r0 c52..54 bg=Background
r0 c54..58 fg=Muted bg=Background
r0 c58..66 fg=Foreground bg=Background
r0 c66..68 bg=Background
r0 c68..72 fg=Muted bg=Background
r0 c72..80 fg=Foreground bg=Background
r1 c0..80 bg=Background
r2 c0..15 fg=Foreground bg=Background bold
r2 c15..80 bg=Background
r3 c0..6 fg=Foreground bg=Background
r3 c6..8 bg=Background
r3 c8..66 fg=Accent bg=Background
r3 c66..68 bg=Background
r3 c68..80 fg=Muted bg=Background
r4 c0..6 fg=Foreground bg=Background
r4 c6..8 bg=Background
r4 c8..45 fg=Accent bg=Background
r4 c45..68 bg=Background
r4 c68..79 fg=Muted bg=Background
r4 c79..80 bg=Background
r5 c0..6 fg=Foreground bg=Background
r5 c6..8 bg=Background
r5 c8..23 fg=Accent bg=Background
r5 c23..68 bg=Background
r5 c68..79 fg=Muted bg=Background
r5 c79..80 bg=Background
r6 c0..6 fg=Foreground bg=Background
r6 c6..68 bg=Background
r6 c68..76 fg=Muted bg=Background
r6 c76..80 bg=Background
r7 c0..7 fg=Foreground bg=Background bold
r7 c7..9 bg=Background
r7 c9..13 fg=Muted bg=Background
r7 c13..20 fg=Foreground bg=Background
r7 c20..22 bg=Background
r7 c22..26 fg=Muted bg=Background
r7 c26..33 fg=Foreground bg=Background
r7 c33..35 bg=Background
r7 c35..39 fg=Muted bg=Background
r7 c39..46 fg=Foreground bg=Background
r7 c46..80 bg=Background
r8 c0..8 fg=Muted bg=Background
r8 c8..9 fg=Dim bg=Background
r8 c9..14 bg=Background
r8 c14..29 fg=Accent bg=Background
r8 c29..60 bg=Background
r8 c60..61 fg=Accent bg=Background
r8 c61..62 bg=Background
r8 c62..75 fg=Accent bg=Background
r8 c75..80 bg=Background
r9 c0..8 fg=Muted bg=Background
r9 c8..9 fg=Dim bg=Background
r9 c9..10 bg=Background
r9 c10..14 fg=Accent bg=Background
r9 c14..29 bg=Background
r9 c29..32 fg=Accent bg=Background
r9 c32..56 bg=Background
r9 c56..60 fg=Accent bg=Background
r9 c60..61 bg=Background
r9 c61..62 fg=Accent bg=Background
r9 c62..75 bg=Background
r9 c75..80 fg=Accent bg=Background
r10 c0..8 fg=Muted bg=Background
r10 c8..9 fg=Dim bg=Background
r10 c9..10 fg=Accent bg=Background
r10 c10..32 bg=Background
r10 c32..38 fg=Accent bg=Background
r10 c38..53 bg=Background
r10 c53..57 fg=Accent bg=Background
r10 c57..80 bg=Background
r11 c0..8 fg=Muted bg=Background
r11 c8..9 fg=Dim bg=Background
r11 c9..38 bg=Background
r11 c38..53 fg=Accent bg=Background
r11 c53..80 bg=Background
== dark-truecolor · 120x12
Requests ▄▅▆▆▇██▇██▇▇▇▇▅▅▄▄▃▃▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ██████████████████████████████████████████████████████████████████████████████████████████████████ 12.4k tokens
sonnet ██████████████████████████████████████████████████████████████▍ 7900 tokens
haiku ████████████████████████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms │ ⢀⣀⠤⠤⠒⠒⠊⠉⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⣀⣀ ⣀⡀ ⣀⣀⠤⠤⠒⠒⠒⠊⠉⠉⠉⠉⠉⠉⠒⠤⢄⣀⣀⣀
│ ⢀⣀⠤⠔⠒⠉⠁ ⠉⠢⠤⣀⣀ ⣀⡠⠤⠒⠊⠉ ⠈⠉ ⠉⠉⠑⠒⠒⠤⠤⣀
│⠉⠁ ⠉⠉⠒⠒⠢⠤⢄⣀⣀ ⢀⣀⠤⠔⠊⠉
17.2 ms │ ⠉⠉⠑⠢⢄⣀⣀⣀⣀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠊⠑⠒⠒⠉⠁
---
r0 c0..8 fg=Foreground bg=Background bold
r0 c8..10 bg=Background
r0 c10..58 fg=Accent bg=Background
r0 c58..60 bg=Background
r0 c60..64 fg=Muted bg=Background
r0 c64..72 fg=Foreground bg=Background
r0 c72..74 bg=Background
r0 c74..78 fg=Muted bg=Background
r0 c78..86 fg=Foreground bg=Background
r0 c86..88 bg=Background
r0 c88..92 fg=Muted bg=Background
r0 c92..100 fg=Foreground bg=Background
r0 c100..120 bg=Background
r1 c0..120 bg=Background
r2 c0..15 fg=Foreground bg=Background bold
r2 c15..120 bg=Background
r3 c0..6 fg=Foreground bg=Background
r3 c6..8 bg=Background
r3 c8..106 fg=Accent bg=Background
r3 c106..108 bg=Background
r3 c108..120 fg=Muted bg=Background
r4 c0..6 fg=Foreground bg=Background
r4 c6..8 bg=Background
r4 c8..71 fg=Accent bg=Background
r4 c71..108 bg=Background
r4 c108..119 fg=Muted bg=Background
r4 c119..120 bg=Background
r5 c0..6 fg=Foreground bg=Background
r5 c6..8 bg=Background
r5 c8..33 fg=Accent bg=Background
r5 c33..108 bg=Background
r5 c108..119 fg=Muted bg=Background
r5 c119..120 bg=Background
r6 c0..6 fg=Foreground bg=Background
r6 c6..108 bg=Background
r6 c108..116 fg=Muted bg=Background
r6 c116..120 bg=Background
r7 c0..7 fg=Foreground bg=Background bold
r7 c7..9 bg=Background
r7 c9..13 fg=Muted bg=Background
r7 c13..20 fg=Foreground bg=Background
r7 c20..22 bg=Background
r7 c22..26 fg=Muted bg=Background
r7 c26..33 fg=Foreground bg=Background
r7 c33..35 bg=Background
r7 c35..39 fg=Muted bg=Background
r7 c39..46 fg=Foreground bg=Background
r7 c46..120 bg=Background
r8 c0..8 fg=Muted bg=Background
r8 c8..9 fg=Dim bg=Background
r8 c9..16 bg=Background
r8 c16..40 fg=Accent bg=Background
r8 c40..89 bg=Background
r8 c89..91 fg=Accent bg=Background
r8 c91..92 bg=Background
r8 c92..112 fg=Accent bg=Background
r8 c112..120 bg=Background
r9 c0..8 fg=Muted bg=Background
r9 c8..9 fg=Dim bg=Background
r9 c9..10 bg=Background
r9 c10..17 fg=Accent bg=Background
r9 c17..40 bg=Background
r9 c40..45 fg=Accent bg=Background
r9 c45..83 bg=Background
r9 c83..89 fg=Accent bg=Background
r9 c89..90 bg=Background
r9 c90..92 fg=Accent bg=Background
r9 c92..112 bg=Background
r9 c112..120 fg=Accent bg=Background
r10 c0..8 fg=Muted bg=Background
r10 c8..9 fg=Dim bg=Background
r10 c9..11 fg=Accent bg=Background
r10 c11..45 bg=Background
r10 c45..54 fg=Accent bg=Background
r10 c54..77 bg=Background
r10 c77..83 fg=Accent bg=Background
r10 c83..120 bg=Background
r11 c0..8 fg=Muted bg=Background
r11 c8..9 fg=Dim bg=Background
r11 c9..54 bg=Background
r11 c54..78 fg=Accent bg=Background
r11 c78..120 bg=Background
== no-color · 40x12
Requests ▁▃▅▄▅▆▇███▆▅▄▃▂▁ now 45.6 rpm
Tokens by model
opus ██████████████████ 12.4k tokens
sonnet ███████████▌ 7900 tokens
haiku ████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms
69.7 ms │ ⡠⠒⠑⠒⠒⠢⣀ ⡀⡠⠔⠉⠉⢢⡀
│⢀⠔⠁ ⢇ ⢀⠔⠉ ⠈⠑⢄
│⠁ ⠑⠢⡀ ⡠⠊
17.2 ms │ ⠈⠱⣀⣀⠤⠊⠊
== no-color · 80x12
Requests ▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ██████████████████████████████████████████████████████████ 12.4k tokens
sonnet █████████████████████████████████████ 7900 tokens
haiku ██████████████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms │ ⣀⠤⠒⠊⠑⠒⠒⠒⠒⠒⠒⠢⠤⢄⣀ ⣀ ⣀⡠⠤⠒⠒⠉⠉⠉⠉⠢⢄⣀⣀
│ ⣀⠤⠒⠉ ⠑⠤⣀ ⢀⡠⠒⠉ ⠉ ⠉⠑⠒⠤⣀
│⠉ ⠉⠑⠒⠤⢄⣀ ⡠⠔⠊⠁
17.2 ms │ ⠉⠑⢄⣀⣀⣀⣀⣀⠤⠤⠒⠊⠒⠊⠉
== no-color · 120x12
Requests ▄▅▆▆▇██▇██▇▇▇▇▅▅▄▄▃▃▂▁▁▁▁▁▂▂▂▃▃▄▅▆▆▆▇▇████▇▇▆▆▅▅ now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ██████████████████████████████████████████████████████████████████████████████████████████████████ 12.4k tokens
sonnet ██████████████████████████████████████████████████████████████▍ 7900 tokens
haiku ████████████████████████▌ 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms │ ⢀⣀⠤⠤⠒⠒⠊⠉⠒⠒⠒⠒⠒⠒⠒⠒⠒⠒⠢⠤⠤⠤⣀⣀ ⣀⡀ ⣀⣀⠤⠤⠒⠒⠒⠊⠉⠉⠉⠉⠉⠉⠒⠤⢄⣀⣀⣀
│ ⢀⣀⠤⠔⠒⠉⠁ ⠉⠢⠤⣀⣀ ⣀⡠⠤⠒⠊⠉ ⠈⠉ ⠉⠉⠑⠒⠒⠤⠤⣀
│⠉⠁ ⠉⠉⠒⠒⠢⠤⢄⣀⣀ ⢀⣀⠤⠔⠊⠉
17.2 ms │ ⠉⠉⠑⠢⢄⣀⣀⣀⣀⣀⣀⣀⡠⠤⠤⠤⠒⠒⠊⠑⠒⠒⠉⠁
== ascii · 40x12
Requests _.---=####=--.__ now 45.6 rpm
Tokens by model
opus ################## 12.4k tokens
sonnet ############ 7900 tokens
haiku ##### 3100 tokens
cache 0 tokens
Latency now 45.6 ms
69.7 ms | ....... .....:.
|... : ... ...
|. ... ..
17.2 ms | .:.....
== ascii · 80x12
Requests _________..--===########==-- now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ########################################################## 12.4k tokens
sonnet ##################################### 7900 tokens
haiku ############### 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms | ............... . .............
| .... ... .... . .....
|. ...... ....
17.2 ms | ...............
== ascii · 120x12
Requests --==##########----.._________..--===########==-- now 45.6 rpm min 17.2 rpm max 69.7 rpm
Tokens by model
opus ################################################################################################## 12.4k tokens
sonnet ############################################################### 7900 tokens
haiku ######################### 3100 tokens
cache 0 tokens
Latency now 45.6 ms min 17.2 ms max 69.7 ms
69.7 ms | ........................ .. ....................
| ....... ..... ...... .. ........
|.. ......... ......
17.2 ms | ........................
+118
View File
@@ -0,0 +1,118 @@
//! Gallery: data displays fully drawn, mid draw-in, empty and from agent JSON.
use ratatui::{buffer::Buffer, layout::Rect};
use super::Entry;
use crate::{DataBarItem, DataBars, DataDisplay, DataSparkline, DataTrend, Paint, Theme};
fn series() -> Vec<f64> {
(0..64)
.map(|i| 120.0 + 60.0 * (f64::from(i) / 6.0).sin() + f64::from((i * 7) % 11) * 3.0)
.collect()
}
fn items() -> Vec<DataBarItem> {
vec![
DataBarItem::new("engine", 412.0),
DataBarItem::new("tui", 268.0),
DataBarItem::new("runtime", 131.0),
DataBarItem::new("protocol", 44.0),
]
}
fn row(area: Rect, y: u16, h: u16) -> Rect {
Rect::new(area.x, area.y.saturating_add(y), area.width, h).intersection(area)
}
fn sparklines(area: Rect, buf: &mut Buffer, theme: &Theme) {
let values = series();
DataSparkline::new("Requests", &values)
.unit("rpm")
.paint(row(area, 0, 1), buf, theme);
DataSparkline::new("Requests", &values)
.unit("rpm")
.drawn(0.45)
.paint(row(area, 1, 1), buf, theme);
DataSparkline::new("Requests", &[]).paint(row(area, 2, 1), buf, theme);
}
fn bars(area: Rect, buf: &mut Buffer, theme: &Theme) {
let items = items();
DataBars::new("Tests by crate", &items).paint(row(area, 0, 5), buf, theme);
}
fn bars_growing(area: Rect, buf: &mut Buffer, theme: &Theme) {
let items = items();
DataBars::new("Tests by crate", &items)
.previous(&[300.0, 300.0, 300.0, 300.0])
.drawn(0.5)
.paint(row(area, 0, 5), buf, theme);
}
fn trend(area: Rect, buf: &mut Buffer, theme: &Theme) {
let values = series();
DataTrend::new("Latency", &values)
.unit("ms")
.rows(7)
.paint(area, buf, theme);
}
fn trend_drawing(area: Rect, buf: &mut Buffer, theme: &Theme) {
let values = series();
DataTrend::new("Latency", &values)
.unit("ms")
.rows(7)
.drawn(0.6)
.paint(area, buf, theme);
}
fn from_agent(area: Rect, buf: &mut Buffer, theme: &Theme) {
let shown = DataDisplay::from_json(
r#"{"kind":"bars","label":"Tokens this turn","unit":"tokens",
"items":[{"label":"read","value":18200},{"label":"wrote","value":2400},
{"label":"cached","value":61000}]}"#,
)
.expect("gallery fixture is a valid data display");
shown.view().paint(area, buf, theme);
}
pub(crate) fn entries() -> Vec<Entry> {
vec![
Entry {
name: "data-sparkline",
width: 80,
height: 3,
draw: sparklines,
},
Entry {
name: "data-bars",
width: 60,
height: 5,
draw: bars,
},
Entry {
name: "data-bars-growing",
width: 60,
height: 5,
draw: bars_growing,
},
Entry {
name: "data-trend",
width: 72,
height: 7,
draw: trend,
},
Entry {
name: "data-trend-drawing",
width: 72,
height: 7,
draw: trend_drawing,
},
Entry {
name: "data-from-agent",
width: 60,
height: 4,
draw: from_agent,
},
]
}
+2
View File
@@ -31,6 +31,7 @@ mod approval;
mod artifacts;
mod atmosphere;
mod attention;
mod chart;
mod chrome;
mod display;
mod habitat;
@@ -104,6 +105,7 @@ pub fn entries() -> Vec<Entry> {
lists::entries(),
chrome::entries(),
display::entries(),
chart::entries(),
approval::entries(),
motion::entries(),
workspace::entries(),
+10 -10
View File
@@ -89,8 +89,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"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
"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
},
{
"path": "docs/PROVIDERS.md",
@@ -121,8 +121,8 @@
"sources": [
{
"path": "docs/CONFIGURATION.md",
"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
"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
},
{
"path": "docs/LEGACY_PATHS.md",
@@ -153,8 +153,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"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
"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
}
]
},
@@ -296,8 +296,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"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
"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
}
]
},
@@ -328,8 +328,8 @@
},
{
"path": "docs/CONFIGURATION.md",
"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
"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
}
]
},
+2 -2
View File
@@ -2,7 +2,7 @@
"format": "codewhale.features.v1",
"source": {
"path": "docs/features.toml",
"sha256": "a63042dc25a658f626735cc40e7a7cf4c67eaa51bfbb037dc2820d083d10fe3a"
"sha256": "4c4dfe17cea716328253c687d9d0665e1b24fc3b59d00f2a81cf1157dfea7072"
},
"features": [
{
@@ -1070,7 +1070,7 @@
{
"id": "live-thinking",
"name": "Live thinking",
"summary": "Model thinking shows live in the transcript, then collapses under a Thought header; /verbose shows it in full.",
"summary": "/verbose toggles live model thinking in the transcript.",
"status": "stable",
"surfaces": {
"tui": "stable"