Merge branch 'main' into feat/docs-sandbox-web

This commit is contained in:
Hunter Bown
2026-08-19 23:08:21 -07:00
committed by GitHub
45 changed files with 1033 additions and 204 deletions
+3
View File
@@ -170,6 +170,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: actions/setup-node@v7
with:
@@ -181,6 +183,7 @@ jobs:
- name: Check release helper contracts
run: |
bash .github/scripts/agent-task-metadata.test.sh
bash scripts/release/check-feature-release-notes.test.sh
bash scripts/release/generate-release-body.test.sh
bash scripts/release/install-dogfood.test.sh
bash scripts/release/prepare-release.test.sh
+23 -14
View File
@@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
partial reply was preserved, the retry budget is enforced in mechanism, and
recovery produces exactly one authoritative final answer.
Codewhale v0.9.10 is a retention-and-identity release: the shell and
Codewhale v0.9.10 is a retention, identity, and product-clarity release: the shell and
transcript can no longer retain unbounded tool output in memory or on disk,
mid-turn history inserts no longer strand in-flight tool rows, every agent
that ran this session is visible from `/agents list`, the PTY acceptance
@@ -157,15 +157,20 @@ abort under load.
- Short CLI no longer waits up to three seconds for a telemetry POST on
exit; `session_end` is recorded and the buffer ships on the next
interactive session.
- `/goal`, `/workspace`, `/tokens`, `/translate`, and `/hooks` appear at
the empty palette and bare `/` menu instead of only under Advanced
(#5442, #5439).
- Welcome, the last onboarding tips screen, and `/home` name
`/workspace`, `/restore`, and `/tokens` instead of opening on
governance and setup. That copy says `/restore` rolls workspace
files back to a per-turn snapshot rather than rewinding a turn,
which is what `/undo` does, and `/home` aligns its `/links` row to
the same command column as the rest of the list (#5442).
- Bare `/` now opens a deliberately small starter set: `/help`, `/setup`,
`/model`, `/settings`, `/resume`, and `/rc`. The full command inventory
remains searchable through `/help`, the command palette, and direct prefix
typing, without front-loading the entire control surface (#5442, #5439).
- First run now asks only for decisions needed to become usable, keeps the
offline route explicit, and leaves optional provider, tools, policy, and
appearance work in the progressive `/setup` repair guide. The idle aquarium
asks one task-oriented question instead of advertising a tutorial or command
billboard, and the localized telemetry choice appears after the workspace is
ready without blocking the composer.
- Provider, active model, and reasoning effort moved out of the crowded header
into a quiet, width-aware footer identity. Compact layouts keep model and
effort before provider detail and shed whole low-priority groups instead of
clipping the composer or control hints.
- The model picker no longer re-parses `~/.codewhale/config.toml` once per
provider when deciding who has a saved key.
@@ -214,10 +219,10 @@ abort under load.
validate, and downloads binaries only from that source. Explicit
`CODEWHALE_RELEASE_BASE_URL` / `CODEWHALE_USE_CNB_MIRROR=1` still skip the
race; other targets stay on GitHub.
- `/workflow`, `/goal`, and `/auto` are pinned at the top of the empty `/`
menu, named on the idle welcome and footer, and occupy the first three
default Hotbar slots. `/auto` turns on Auto-Review (the existing
permission posture) and says when to use the other two instead (#5439).
- `/workflow <objective>` and `/workflow run <path>` now produce a bounded,
tool-less proposal for review. Only `/workflow confirm` can launch that
reviewed draft; status, cancel, settings, and the `/workflows` dashboard stay
host-owned and do not spend a model turn (#5439).
- `feat(tui): add cancellable cadence to continuous goals` (M-Maciej) —
`[goal] continuation_delay_seconds` gives coordinator goals a visible quiet
period between successful turns while reusing the existing coalesced goal
@@ -611,6 +616,10 @@ a published OpenAI-compatible host ship here (#5350).
### Added
- Opt-in multiline composer mode (`composer_multiline_mode = true`) makes
Enter insert a newline and Shift+Enter send. Alt+Enter, Ctrl+J, and supported
Ctrl+Enter/Cmd+Enter behavior stays unchanged (#5345, @AiurArtanis).
- `/plugin marketplace add|list|show|remove|install` completes the
federated marketplace journey (#5311). `add` reads one LOCAL catalog
document in the real published schemas (Kimi, Claude, Codex, or
+18 -17
View File
@@ -40,7 +40,8 @@ pub const SETUP_STATE_FILE_NAME: &str = "setup_state.json";
///
/// The notice is owed whenever
/// [`SetupState::telemetry_notice_decided_for`] does not match this string.
/// Bumping it re-asks prior acceptors and unanswered users, so it is bumped only
/// Bumping it re-shows the disclosure to prior acceptors and unanswered users,
/// so it is bumped only
/// when the collection policy, schema, or disclosure materially changes. Prior
/// declines remain off. Keying it to the app version would re-prompt every
/// release, which is nagging with extra steps.
@@ -330,13 +331,13 @@ pub struct SetupState {
pub inherited: bool,
// ── Telemetry notice ────────────────────────────────────────────────
/// [`TELEMETRY_NOTICE_VERSION`] whose telemetry notice the user has
/// answered. `None` means the notice is still owed.
/// [`TELEMETRY_NOTICE_VERSION`] whose telemetry disclosure was shown.
/// `None` means the notice is still owed.
///
/// Never auto-completed and never deferred-completed: unlike the
/// constitution checkpoint, which records a `Deferred` completion on the
/// skip-onboarding path, a telemetry notice that was not rendered and
/// answered leaves this `None`. Collection follows the documented default
/// skip-onboarding path, a telemetry notice that was not rendered leaves
/// this `None`. Collection follows the documented default
/// while the notice remains owed on the next interactive launch.
///
/// These are *fields* rather than a new [`SetupStep`] variant on purpose:
@@ -345,9 +346,9 @@ pub struct SetupState {
/// checkpoint — while unknown fields are ignored.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telemetry_notice_decided_for: Option<String>,
/// The user's answer to the notice. `false` with any recorded notice
/// version is a durable opt-out; `true` records acknowledgment of that
/// version's disclosure.
/// The privacy preference recorded with the notice. `false` with any
/// recorded notice version is a durable opt-out; `true` records that the
/// default-on disclosure was shown for that version.
#[serde(default, skip_serializing_if = "is_false")]
pub telemetry_opt_in: bool,
}
@@ -482,20 +483,20 @@ impl SetupState {
self
}
/// True when the telemetry notice for `version` has not been answered.
/// True when the telemetry notice for `version` has not been shown.
///
/// A decision recorded against a *different* notice version does not
/// count: the content changed, so the answer is stale and is owed again.
/// count: the content changed, so the disclosure is owed again.
#[must_use]
pub fn needs_telemetry_notice(&self, version: &str) -> bool {
self.telemetry_notice_decided_for.as_deref() != Some(version)
}
/// Record the user's answer to the telemetry notice for `version`.
/// Record the privacy preference associated with the telemetry notice.
///
/// Call this only from a path where the notice was actually rendered and
/// the user actually answered. Deferral, skip-onboarding, and any
/// non-interactive surface must leave the record untouched.
/// Default-on may be recorded only after the notice was actually rendered;
/// an explicit opt-out may also arrive from Settings. Deferral,
/// skip-onboarding, and non-interactive surfaces leave it untouched.
pub fn record_telemetry_notice(
&mut self,
version: impl Into<String>,
@@ -506,15 +507,15 @@ impl SetupState {
self
}
/// True when the user was asked the current notice and kept counting on.
/// True when the current default-on disclosure was shown and remains on.
#[must_use]
pub fn telemetry_accepted(&self, version: &str) -> bool {
!self.needs_telemetry_notice(version) && self.telemetry_opt_in
}
/// True when the user was asked the current notice and said no.
/// True when the current notice record contains an explicit opt-out.
///
/// Distinct from "never asked": only a recorded decline is an opt-out, and
/// Distinct from "never shown": only a recorded decline is an opt-out, and
/// only an opt-out may be acted on destructively.
#[must_use]
pub fn telemetry_declined(&self, version: &str) -> bool {
+2 -2
View File
@@ -244,8 +244,8 @@ pub fn load_setup_state_for_decision_at(path: &Path) -> Option<SetupState> {
/// 4. Endpoint configured but refused by [`validate_endpoint`] → `ForcedOff`.
/// 5. Otherwise `Enabled`.
///
/// The notice is only ever *rendered* on a TTY. The interactive TUI explains
/// the default in a native startup modal before telemetry is armed; headless
/// The notice is only ever *rendered* on a TTY. The interactive TUI draws a
/// localized nonblocking disclosure before telemetry is armed; headless
/// surfaces use the same documented default and kill switches.
pub fn decide_in_home(
home: Option<&Path>,
+23 -14
View File
@@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
partial reply was preserved, the retry budget is enforced in mechanism, and
recovery produces exactly one authoritative final answer.
Codewhale v0.9.10 is a retention-and-identity release: the shell and
Codewhale v0.9.10 is a retention, identity, and product-clarity release: the shell and
transcript can no longer retain unbounded tool output in memory or on disk,
mid-turn history inserts no longer strand in-flight tool rows, every agent
that ran this session is visible from `/agents list`, the PTY acceptance
@@ -157,15 +157,20 @@ abort under load.
- Short CLI no longer waits up to three seconds for a telemetry POST on
exit; `session_end` is recorded and the buffer ships on the next
interactive session.
- `/goal`, `/workspace`, `/tokens`, `/translate`, and `/hooks` appear at
the empty palette and bare `/` menu instead of only under Advanced
(#5442, #5439).
- Welcome, the last onboarding tips screen, and `/home` name
`/workspace`, `/restore`, and `/tokens` instead of opening on
governance and setup. That copy says `/restore` rolls workspace
files back to a per-turn snapshot rather than rewinding a turn,
which is what `/undo` does, and `/home` aligns its `/links` row to
the same command column as the rest of the list (#5442).
- Bare `/` now opens a deliberately small starter set: `/help`, `/setup`,
`/model`, `/settings`, `/resume`, and `/rc`. The full command inventory
remains searchable through `/help`, the command palette, and direct prefix
typing, without front-loading the entire control surface (#5442, #5439).
- First run now asks only for decisions needed to become usable, keeps the
offline route explicit, and leaves optional provider, tools, policy, and
appearance work in the progressive `/setup` repair guide. The idle aquarium
asks one task-oriented question instead of advertising a tutorial or command
billboard, and the localized telemetry choice appears after the workspace is
ready without blocking the composer.
- Provider, active model, and reasoning effort moved out of the crowded header
into a quiet, width-aware footer identity. Compact layouts keep model and
effort before provider detail and shed whole low-priority groups instead of
clipping the composer or control hints.
- The model picker no longer re-parses `~/.codewhale/config.toml` once per
provider when deciding who has a saved key.
@@ -214,10 +219,10 @@ abort under load.
validate, and downloads binaries only from that source. Explicit
`CODEWHALE_RELEASE_BASE_URL` / `CODEWHALE_USE_CNB_MIRROR=1` still skip the
race; other targets stay on GitHub.
- `/workflow`, `/goal`, and `/auto` are pinned at the top of the empty `/`
menu, named on the idle welcome and footer, and occupy the first three
default Hotbar slots. `/auto` turns on Auto-Review (the existing
permission posture) and says when to use the other two instead (#5439).
- `/workflow <objective>` and `/workflow run <path>` now produce a bounded,
tool-less proposal for review. Only `/workflow confirm` can launch that
reviewed draft; status, cancel, settings, and the `/workflows` dashboard stay
host-owned and do not spend a model turn (#5439).
- `feat(tui): add cancellable cadence to continuous goals` (M-Maciej) —
`[goal] continuation_delay_seconds` gives coordinator goals a visible quiet
period between successful turns while reusing the existing coalesced goal
@@ -611,6 +616,10 @@ a published OpenAI-compatible host ship here (#5350).
### Added
- Opt-in multiline composer mode (`composer_multiline_mode = true`) makes
Enter insert a newline and Shift+Enter send. Alt+Enter, Ctrl+J, and supported
Ctrl+Enter/Cmd+Enter behavior stays unchanged (#5345, @AiurArtanis).
- `/plugin marketplace add|list|show|remove|install` completes the
federated marketplace journey (#5311). `add` reads one LOCAL catalog
document in the real published schemas (Kimi, Claude, Codex, or
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "triar",
"TelemetryNoticeActionConfirm": "confirmar",
"TelemetryNoticeActionExit": "sortir",
"TelemetryNoticeReceiptEnabled": "Sí, el recompte anònim continua actiu.",
"TelemetryNoticeReceiptEnabled": "El recompte anònim d'ús està activat. Mai es recullen converses ni codi. Canvia-ho a /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "El recompte anònim d'ús està desactivat. No t'ho tornarem a preguntar.",
"TelemetryNoticeReceiptEnabledUnsaved": "El recompte anònim continua actiu en aquesta sessió. Codewhale no ha pogut desar l'elecció i tornarà a preguntar al pròxim inici.",
"TelemetryNoticeReceiptEnabledUnsaved": "El recompte anònim d'ús està activat en aquesta sessió. Codewhale no ha pogut desar aquest avís i el tornarà a mostrar. Canvia-ho a /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "El recompte anònim d'ús està desactivat en aquesta sessió. Codewhale no ha pogut desar l'elecció i tornarà a preguntar al pròxim inici.",
"TelemetryPreferenceEnabledNextLaunch": "El recompte anònim d'ús s'activarà al pròxim inici. Mai es recullen converses ni codi.",
"TelemetryPreferenceDisabled": "El recompte anònim d'ús està desactivat. S'han esborrat les dades locals de telemetria.",
"TelemetryPreferenceDisabledWithWarning": "El recompte anònim d'ús està desactivat, però una part de la neteja requereix atenció: {detail}",
"TelemetryPreferenceDisabledForSession": "El recompte anònim d'ús està desactivat en aquesta sessió, però la preferència no s'ha desat i pot tornar al pròxim inici: {detail}",
"TelemetryPreferenceSaveFailed": "La preferència de telemetria no s'ha pogut canviar de manera segura: {detail}",
"StatusPickerTitle": " Línia d'estat ",
"StatusPickerInstruction": "Tria els elements que vols al peu:",
"StatusPickerActionToggle": "commutar ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Mode d'inici",
"ConfigLabelAllowShell": "Accés al shell",
"ConfigLabelManagedAllowShell": "Accés al shell (gestionat)",
"ConfigLabelTelemetry": "Recompte anònim d'ús",
"ConfigHintTelemetry": "Preferència desada per a recomptes anònims de funcions i errors; mai converses ni codi",
"ConfigValueTelemetryOn": "Activat (desat)",
"ConfigValueTelemetryOff": "Desactivat (desat)",
"ConfigLabelStreamTimeout": "Temps d'espera del flux",
"ConfigLabelTheme": "Tema",
"ConfigLabelLocale": "Idioma",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "wählen",
"TelemetryNoticeActionConfirm": "bestätigen",
"TelemetryNoticeActionExit": "beenden",
"TelemetryNoticeReceiptEnabled": "Ja, die anonyme Zählung bleibt eingeschaltet.",
"TelemetryNoticeReceiptEnabled": "Anonyme Nutzungszählungen sind aktiviert. Unterhaltungen und Code werden nie erfasst. Ändern unter /settings; Schema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Die anonyme Nutzungszählung ist ausgeschaltet. Sie werden nicht erneut gefragt.",
"TelemetryNoticeReceiptEnabledUnsaved": "Die anonyme Zählung bleibt für diese Sitzung eingeschaltet. Codewhale konnte die Auswahl nicht speichern und fragt beim nächsten Start erneut.",
"TelemetryNoticeReceiptEnabledUnsaved": "Anonyme Nutzungszählungen sind für diese Sitzung aktiviert. Codewhale konnte diesen Hinweis nicht speichern und zeigt ihn erneut. Ändern unter /settings; Schema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Die anonyme Nutzungszählung ist für diese Sitzung ausgeschaltet. Codewhale konnte die Auswahl nicht speichern und fragt beim nächsten Start erneut.",
"TelemetryPreferenceEnabledNextLaunch": "Anonyme Nutzungszählungen sind ab dem nächsten Start aktiviert. Unterhaltungen und Code werden nie erfasst.",
"TelemetryPreferenceDisabled": "Anonyme Nutzungszählungen sind deaktiviert. Lokale Telemetriedaten wurden gelöscht.",
"TelemetryPreferenceDisabledWithWarning": "Anonyme Nutzungszählungen sind deaktiviert, aber ein Teil der Bereinigung benötigt Aufmerksamkeit: {detail}",
"TelemetryPreferenceDisabledForSession": "Anonyme Nutzungszählungen sind für diese Sitzung deaktiviert, aber die Einstellung wurde nicht gespeichert und kann beim nächsten Start zurückkehren: {detail}",
"TelemetryPreferenceSaveFailed": "Die Telemetrieeinstellung konnte nicht sicher geändert werden: {detail}",
"StatusPickerTitle": " Statuszeile ",
"StatusPickerInstruction": "Wähle die Chips für die Fußzeile:",
"StatusPickerActionToggle": "umschalten ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Startmodus",
"ConfigLabelAllowShell": "Shell-Zugriff",
"ConfigLabelManagedAllowShell": "Shell-Zugriff (verwaltet)",
"ConfigLabelTelemetry": "Anonyme Nutzungszählung",
"ConfigHintTelemetry": "Gespeicherte Einstellung für anonyme Funktions- und Fehlerzählungen; nie Unterhaltungen oder Code",
"ConfigValueTelemetryOn": "An (gespeichert)",
"ConfigValueTelemetryOff": "Aus (gespeichert)",
"ConfigLabelStreamTimeout": "Stream-Timeout",
"ConfigLabelTheme": "Theme",
"ConfigLabelLocale": "Sprache",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "choose",
"TelemetryNoticeActionConfirm": "confirm",
"TelemetryNoticeActionExit": "exit",
"TelemetryNoticeReceiptEnabled": "Yes — anonymous counts stay on.",
"TelemetryNoticeReceiptEnabled": "Anonymous usage counts are on. Conversations and code are never collected. Change this in /settings; schema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Anonymous usage counting is off. You will not be asked again.",
"TelemetryNoticeReceiptEnabledUnsaved": "Counts stay on for this session. Codewhale could not save the choice, so it will ask again next launch.",
"TelemetryNoticeReceiptEnabledUnsaved": "Anonymous usage counts are on for this session. Codewhale could not save this notice, so it will appear again. Change this in /settings; schema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Anonymous usage counting is off for this session. Codewhale could not save the choice, so it will ask again next launch.",
"TelemetryPreferenceEnabledNextLaunch": "Anonymous usage counts will be on next launch. Conversations and code are never collected.",
"TelemetryPreferenceDisabled": "Anonymous usage counts are off. Local telemetry state was erased.",
"TelemetryPreferenceDisabledWithWarning": "Anonymous usage counts are off, but part of the cleanup needs attention: {detail}",
"TelemetryPreferenceDisabledForSession": "Anonymous usage counts are off for this session, but the preference was not saved and may return next launch: {detail}",
"TelemetryPreferenceSaveFailed": "The telemetry preference could not be changed safely: {detail}",
"StatusPickerTitle": " Status line ",
"StatusPickerInstruction": "Pick the chips you want in the footer:",
"StatusPickerActionToggle": "toggle ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Startup mode",
"ConfigLabelAllowShell": "Shell access",
"ConfigLabelManagedAllowShell": "Shell access (managed)",
"ConfigLabelTelemetry": "Anonymous usage counts",
"ConfigHintTelemetry": "Saved preference for anonymous feature and error counts; never conversations or code",
"ConfigValueTelemetryOn": "On (saved)",
"ConfigValueTelemetryOff": "Off (saved)",
"ConfigLabelStreamTimeout": "Stream timeout",
"ConfigLabelTheme": "Theme",
"ConfigLabelLocale": "Language",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "elegir",
"TelemetryNoticeActionConfirm": "confirmar",
"TelemetryNoticeActionExit": "salir",
"TelemetryNoticeReceiptEnabled": "Sí, el conteo anónimo sigue activo.",
"TelemetryNoticeReceiptEnabled": "El conteo anónimo de uso está activado. Nunca se recopilan conversaciones ni código. Cámbialo en /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "El conteo anónimo de uso está desactivado. No volveremos a preguntarte.",
"TelemetryNoticeReceiptEnabledUnsaved": "El conteo anónimo sigue activo en esta sesión. Codewhale no pudo guardar la opción y volverá a preguntar en el próximo inicio.",
"TelemetryNoticeReceiptEnabledUnsaved": "El conteo anónimo de uso está activado en esta sesión. Codewhale no pudo guardar este aviso y volverá a mostrarlo. Cámbialo en /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "El conteo anónimo de uso está desactivado en esta sesión. Codewhale no pudo guardar la opción, así que volverá a preguntar en el próximo inicio.",
"TelemetryPreferenceEnabledNextLaunch": "El conteo anónimo de uso se activará en el próximo inicio. Nunca se recopilan conversaciones ni código.",
"TelemetryPreferenceDisabled": "El conteo anónimo de uso está desactivado. Se borraron los datos locales de telemetría.",
"TelemetryPreferenceDisabledWithWarning": "El conteo anónimo de uso está desactivado, pero parte de la limpieza requiere atención: {detail}",
"TelemetryPreferenceDisabledForSession": "El conteo anónimo de uso está desactivado en esta sesión, pero la preferencia no se guardó y puede volver en el próximo inicio: {detail}",
"TelemetryPreferenceSaveFailed": "No se pudo cambiar la preferencia de telemetría de forma segura: {detail}",
"StatusPickerTitle": " Línea de estado ",
"StatusPickerInstruction": "Elige los elementos que quieres en el pie:",
"StatusPickerActionToggle": "alternar ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Modo de inicio",
"ConfigLabelAllowShell": "Acceso al shell",
"ConfigLabelManagedAllowShell": "Acceso al shell (administrado)",
"ConfigLabelTelemetry": "Conteo anónimo de uso",
"ConfigHintTelemetry": "Preferencia guardada para conteos anónimos de funciones y errores; nunca conversaciones ni código",
"ConfigValueTelemetryOn": "Activado (guardado)",
"ConfigValueTelemetryOff": "Desactivado (guardado)",
"ConfigLabelStreamTimeout": "Tiempo límite del flujo",
"ConfigLabelTheme": "Tema",
"ConfigLabelLocale": "Idioma",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "choisir",
"TelemetryNoticeActionConfirm": "confirmer",
"TelemetryNoticeActionExit": "quitter",
"TelemetryNoticeReceiptEnabled": "Oui, les comptages anonymes restent actifs.",
"TelemetryNoticeReceiptEnabled": "Le comptage anonyme est activé. Les conversations et le code ne sont jamais collectés. Modifiez-le dans /settings ; schéma : docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Le comptage anonyme de l'utilisation est désactivé. La question ne sera plus posée.",
"TelemetryNoticeReceiptEnabledUnsaved": "Les comptages anonymes restent actifs pour cette session. Codewhale n'a pas pu enregistrer ce choix et reposera la question au prochain lancement.",
"TelemetryNoticeReceiptEnabledUnsaved": "Le comptage anonyme est acti pour cette session. Codewhale n'a pas pu enregistrer cet avis et le montrera de nouveau. Modifiez-le dans /settings ; schéma : docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Le comptage anonyme est désactivé pour cette session. Codewhale n'a pas pu enregistrer le choix et reposera la question au prochain lancement.",
"TelemetryPreferenceEnabledNextLaunch": "Le comptage anonyme sera activé au prochain lancement. Les conversations et le code ne sont jamais collectés.",
"TelemetryPreferenceDisabled": "Le comptage anonyme est désactivé. Les données locales de télémétrie ont été effacées.",
"TelemetryPreferenceDisabledWithWarning": "Le comptage anonyme est désactivé, mais une partie du nettoyage demande votre attention : {detail}",
"TelemetryPreferenceDisabledForSession": "Le comptage anonyme est désactivé pour cette session, mais la préférence n'a pas été enregistrée et peut revenir au prochain lancement : {detail}",
"TelemetryPreferenceSaveFailed": "La préférence de télémétrie n'a pas pu être modifiée en toute sécurité : {detail}",
"StatusPickerTitle": " Barre d'état ",
"StatusPickerInstruction": "Choisissez les éléments à afficher dans le pied de page :",
"StatusPickerActionToggle": "basculer ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Mode au démarrage",
"ConfigLabelAllowShell": "Accès au shell",
"ConfigLabelManagedAllowShell": "Accès au shell (géré)",
"ConfigLabelTelemetry": "Comptage anonyme de l'utilisation",
"ConfigHintTelemetry": "Préférence enregistrée pour les comptes anonymes de fonctions et d'erreurs ; jamais les conversations ni le code",
"ConfigValueTelemetryOn": "Activé (enregistré)",
"ConfigValueTelemetryOff": "Désactivé (enregistré)",
"ConfigLabelStreamTimeout": "Délai du flux",
"ConfigLabelTheme": "Thème",
"ConfigLabelLocale": "Langue",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "चुनें",
"TelemetryNoticeActionConfirm": "पुष्टि",
"TelemetryNoticeActionExit": "बाहर",
"TelemetryNoticeReceiptEnabled": "हाँ, अनाम गणना चालू रहेगी।",
"TelemetryNoticeReceiptEnabled": "अनाम उपयोग गणना चालू है। बातचीत और कोड कभी एकत्र नहीं किए जाते। इसे /settings में बदलें; स्कीमा: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "अनाम उपयोग गणना बंद है। दोबारा नहीं पूछा जाएगा।",
"TelemetryNoticeReceiptEnabledUnsaved": "इस सत्र में अनाम गणना चालू रहेगी। Codewhale चयन सहेज नहीं सका, इसलिए अगली बार शुरू होने पर फिर पूछेगा।",
"TelemetryNoticeReceiptEnabledUnsaved": "इस सत्र में अनाम उपयोग गणना चालू है। Codewhale इस सूचना को सहेज नहीं सका, इसलिए यह फिर दिखाई देगी। इसे /settings में बदलें; स्कीमा: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "इस सत्र में अनाम उपयोग गणना बंद है। Codewhale चयन सहेज नहीं सका, इसलिए अगली बार शुरू होने पर फिर पूछेगा।",
"TelemetryPreferenceEnabledNextLaunch": "अगली बार शुरू होने पर अनाम उपयोग गणना चालू होगी। बातचीत और कोड कभी एकत्र नहीं किए जाते।",
"TelemetryPreferenceDisabled": "अनाम उपयोग गणना बंद है। स्थानीय टेलीमेट्री डेटा मिटा दिया गया है।",
"TelemetryPreferenceDisabledWithWarning": "अनाम उपयोग गणना बंद है, लेकिन सफ़ाई के एक हिस्से पर ध्यान देना होगा: {detail}",
"TelemetryPreferenceDisabledForSession": "इस सत्र में अनाम उपयोग गणना बंद है, लेकिन प्राथमिकता सहेजी नहीं गई और अगली बार लौट सकती है: {detail}",
"TelemetryPreferenceSaveFailed": "टेलीमेट्री प्राथमिकता सुरक्षित रूप से नहीं बदली जा सकी: {detail}",
"StatusPickerTitle": " स्टेटस लाइन ",
"StatusPickerInstruction": "फ़ुटर में चाहिए वाले चिप चुनें:",
"StatusPickerActionToggle": "टॉगल ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "स्टार्टअप मोड",
"ConfigLabelAllowShell": "शेल एक्सेस",
"ConfigLabelManagedAllowShell": "शेल एक्सेस (प्रबंधित)",
"ConfigLabelTelemetry": "अनाम उपयोग गणना",
"ConfigHintTelemetry": "अनाम सुविधा और त्रुटि गणना की सहेजी गई प्राथमिकता; बातचीत या कोड कभी नहीं",
"ConfigValueTelemetryOn": "चालू (सहेजा गया)",
"ConfigValueTelemetryOff": "बंद (सहेजा गया)",
"ConfigLabelStreamTimeout": "स्ट्रीम टाइमआउट",
"ConfigLabelTheme": "थीम",
"ConfigLabelLocale": "भाषा",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "pilih",
"TelemetryNoticeActionConfirm": "konfirmasi",
"TelemetryNoticeActionExit": "keluar",
"TelemetryNoticeReceiptEnabled": "Ya, penghitungan anonim tetap aktif.",
"TelemetryNoticeReceiptEnabled": "Penghitungan penggunaan anonim aktif. Percakapan dan kode tidak pernah dikumpulkan. Ubah di /settings; skema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Penghitungan penggunaan anonim dinonaktifkan. Anda tidak akan ditanya lagi.",
"TelemetryNoticeReceiptEnabledUnsaved": "Penghitungan anonim tetap aktif untuk sesi ini. Codewhale tidak dapat menyimpan pilihan, sehingga akan bertanya lagi saat peluncuran berikutnya.",
"TelemetryNoticeReceiptEnabledUnsaved": "Penghitungan penggunaan anonim aktif untuk sesi ini. Codewhale tidak dapat menyimpan pemberitahuan ini, sehingga akan muncul lagi. Ubah di /settings; skema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Penghitungan penggunaan anonim dinonaktifkan untuk sesi ini. Codewhale tidak dapat menyimpan pilihan, sehingga akan bertanya lagi saat peluncuran berikutnya.",
"TelemetryPreferenceEnabledNextLaunch": "Penghitungan penggunaan anonim akan aktif pada peluncuran berikutnya. Percakapan dan kode tidak pernah dikumpulkan.",
"TelemetryPreferenceDisabled": "Penghitungan penggunaan anonim dinonaktifkan. Data telemetri lokal telah dihapus.",
"TelemetryPreferenceDisabledWithWarning": "Penghitungan penggunaan anonim dinonaktifkan, tetapi sebagian pembersihan perlu diperhatikan: {detail}",
"TelemetryPreferenceDisabledForSession": "Penghitungan penggunaan anonim dinonaktifkan untuk sesi ini, tetapi preferensi tidak tersimpan dan dapat kembali pada peluncuran berikutnya: {detail}",
"TelemetryPreferenceSaveFailed": "Preferensi telemetri tidak dapat diubah dengan aman: {detail}",
"StatusPickerTitle": " Baris status ",
"StatusPickerInstruction": "Pilih chip yang ingin ditampilkan di bilah bawah:",
"StatusPickerActionToggle": "alihkan ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Mode startup",
"ConfigLabelAllowShell": "Akses shell",
"ConfigLabelManagedAllowShell": "Akses shell (terkelola)",
"ConfigLabelTelemetry": "Penghitungan penggunaan anonim",
"ConfigHintTelemetry": "Preferensi tersimpan untuk hitungan fitur dan kesalahan anonim; tidak pernah percakapan atau kode",
"ConfigValueTelemetryOn": "Aktif (tersimpan)",
"ConfigValueTelemetryOff": "Nonaktif (tersimpan)",
"ConfigLabelStreamTimeout": "Batas waktu stream",
"ConfigLabelTheme": "Tema",
"ConfigLabelLocale": "Bahasa",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "選択",
"TelemetryNoticeActionConfirm": "確定",
"TelemetryNoticeActionExit": "終了",
"TelemetryNoticeReceiptEnabled": "はい。匿名の利用状況集計はオンのままです。",
"TelemetryNoticeReceiptEnabled": "匿名の利用状況集計はオンです。会話やコードは一切収集しません。/settings で変更できます。スキーマ: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "匿名の利用状況集計はオフです。今後は確認しません。",
"TelemetryNoticeReceiptEnabledUnsaved": "このセッションでは匿名の利用状況集計オンのままです。Codewhale は選択を保存できなかったため、次回の起動時に再度確認します。",
"TelemetryNoticeReceiptEnabledUnsaved": "このセッションでは匿名の利用状況集計オンです。Codewhale はこの通知を保存できなかったため、再度表示します。/settings で変更できます。スキーマ: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "このセッションでは匿名の利用状況集計がオフです。Codewhale は選択を保存できなかったため、次回の起動時に再度確認します。",
"TelemetryPreferenceEnabledNextLaunch": "匿名の利用状況集計は次回の起動時からオンになります。会話やコードは一切収集しません。",
"TelemetryPreferenceDisabled": "匿名の利用状況集計はオフです。ローカルのテレメトリーデータを消去しました。",
"TelemetryPreferenceDisabledWithWarning": "匿名の利用状況集計はオフですが、クリーンアップの一部に対応が必要です: {detail}",
"TelemetryPreferenceDisabledForSession": "このセッションでは匿名の利用状況集計がオフですが、設定を保存できなかったため次回の起動時に戻る可能性があります: {detail}",
"TelemetryPreferenceSaveFailed": "テレメトリー設定を安全に変更できませんでした: {detail}",
"StatusPickerTitle": " ステータス行 ",
"StatusPickerInstruction": "フッターに表示する項目を選択:",
"StatusPickerActionToggle": "切替 ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "起動モード",
"ConfigLabelAllowShell": "シェルアクセス",
"ConfigLabelManagedAllowShell": "シェルアクセス(管理)",
"ConfigLabelTelemetry": "匿名の利用状況集計",
"ConfigHintTelemetry": "匿名の機能・エラー件数に関する保存済み設定。会話やコードは対象外",
"ConfigValueTelemetryOn": "オン(保存済み)",
"ConfigValueTelemetryOff": "オフ(保存済み)",
"ConfigLabelStreamTimeout": "ストリームタイムアウト",
"ConfigLabelTheme": "テーマ",
"ConfigLabelLocale": "言語",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "선택",
"TelemetryNoticeActionConfirm": "확인",
"TelemetryNoticeActionExit": "종료",
"TelemetryNoticeReceiptEnabled": "네, 익명 집계는 계속 켜져 있습니다.",
"TelemetryNoticeReceiptEnabled": "익명 사용량 집계가 켜져 있습니다. 대화와 코드는 수집하지 않습니다. /settings에서 변경하세요. 스키마: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "익명 사용량 집계가 꺼져 있습니다. 다시 묻지 않습니다.",
"TelemetryNoticeReceiptEnabledUnsaved": "이 세션에서는 익명 집계가 계속 켜져 있습니다. Codewhale이 선택을 저장하지 못했으므로 다음 실행 때 다시 묻습니다.",
"TelemetryNoticeReceiptEnabledUnsaved": "이 세션에서는 익명 사용량 집계가 켜져 있습니다. Codewhale이 이 알림을 저장하지 못했으므로 다시 표시됩니다. /settings에서 변경하세요. 스키마: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "이 세션에서는 익명 사용량 집계가 꺼져 있습니다. Codewhale이 선택을 저장하지 못했으므로 다음 실행 때 다시 묻습니다.",
"TelemetryPreferenceEnabledNextLaunch": "익명 사용량 집계는 다음 실행부터 켜집니다. 대화와 코드는 수집하지 않습니다.",
"TelemetryPreferenceDisabled": "익명 사용량 집계가 꺼졌습니다. 로컬 텔레메트리 데이터를 지웠습니다.",
"TelemetryPreferenceDisabledWithWarning": "익명 사용량 집계가 꺼졌지만 일부 정리에 주의가 필요합니다: {detail}",
"TelemetryPreferenceDisabledForSession": "이 세션에서는 익명 사용량 집계가 꺼졌지만 설정을 저장하지 못해 다음 실행 때 다시 켜질 수 있습니다: {detail}",
"TelemetryPreferenceSaveFailed": "텔레메트리 설정을 안전하게 변경하지 못했습니다: {detail}",
"StatusPickerTitle": " 상태 표시줄 ",
"StatusPickerInstruction": "하단 표시줄에 표시할 항목을 선택하세요:",
"StatusPickerActionToggle": "전환 ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "시작 모드",
"ConfigLabelAllowShell": "셸 접근",
"ConfigLabelManagedAllowShell": "셸 접근 (관리됨)",
"ConfigLabelTelemetry": "익명 사용량 집계",
"ConfigHintTelemetry": "익명 기능 및 오류 집계의 저장된 설정; 대화나 코드는 제외",
"ConfigValueTelemetryOn": "켬 (저장됨)",
"ConfigValueTelemetryOff": "끔 (저장됨)",
"ConfigLabelStreamTimeout": "스트림 시간 제한",
"ConfigLabelTheme": "테마",
"ConfigLabelLocale": "언어",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "escolher",
"TelemetryNoticeActionConfirm": "confirmar",
"TelemetryNoticeActionExit": "sair",
"TelemetryNoticeReceiptEnabled": "Sim, as contagens anônimas continuam ativas.",
"TelemetryNoticeReceiptEnabled": "A contagem anônima de uso está ativada. Conversas e código nunca são coletados. Altere em /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "A contagem anônima de uso está desligada. Não perguntaremos novamente.",
"TelemetryNoticeReceiptEnabledUnsaved": "As contagens anônimas continuam ativas nesta sessão. O Codewhale não conseguiu salvar a escolha e perguntará novamente na próxima inicialização.",
"TelemetryNoticeReceiptEnabledUnsaved": "A contagem anônima de uso está ativa nesta sessão. O Codewhale não conseguiu salvar este aviso e vai exibi-lo novamente. Altere em /settings; esquema: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "A contagem anônima de uso está desligada nesta sessão. O Codewhale não conseguiu salvar a escolha e perguntará novamente na próxima inicialização.",
"TelemetryPreferenceEnabledNextLaunch": "A contagem anônima de uso será ativada na próxima inicialização. Conversas e código nunca são coletados.",
"TelemetryPreferenceDisabled": "A contagem anônima de uso está desativada. Os dados locais de telemetria foram apagados.",
"TelemetryPreferenceDisabledWithWarning": "A contagem anônima de uso está desativada, mas parte da limpeza precisa de atenção: {detail}",
"TelemetryPreferenceDisabledForSession": "A contagem anônima de uso está desativada nesta sessão, mas a preferência não foi salva e pode voltar na próxima inicialização: {detail}",
"TelemetryPreferenceSaveFailed": "Não foi possível alterar a preferência de telemetria com segurança: {detail}",
"StatusPickerTitle": " Linha de status ",
"StatusPickerInstruction": "Escolha os itens que deseja no rodapé:",
"StatusPickerActionToggle": "alternar ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Modo de inicialização",
"ConfigLabelAllowShell": "Acesso ao shell",
"ConfigLabelManagedAllowShell": "Acesso ao shell (gerenciado)",
"ConfigLabelTelemetry": "Contagem anônima de uso",
"ConfigHintTelemetry": "Preferência salva para contagens anônimas de recursos e erros; nunca conversas ou código",
"ConfigValueTelemetryOn": "Ativada (salva)",
"ConfigValueTelemetryOff": "Desativada (salva)",
"ConfigLabelStreamTimeout": "Tempo limite do fluxo",
"ConfigLabelTheme": "Tema",
"ConfigLabelLocale": "Idioma",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "выбрать",
"TelemetryNoticeActionConfirm": "подтвердить",
"TelemetryNoticeActionExit": "выйти",
"TelemetryNoticeReceiptEnabled": "Да, анонимный подсчёт остаётся включённым.",
"TelemetryNoticeReceiptEnabled": "Анонимный подсчёт использования включён. Разговоры и код никогда не собираются. Изменить в /settings; схема: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Анонимный подсчёт использования отключён. Повторного вопроса не будет.",
"TelemetryNoticeReceiptEnabledUnsaved": "Анонимный подсчёт остаётся включённым для этого сеанса. Codewhale не удалось сохранить выбор, поэтому при следующем запуске вопрос повторится.",
"TelemetryNoticeReceiptEnabledUnsaved": "Анонимный подсчёт использования включён для этого сеанса. Codewhale не удалось сохранить уведомление, поэтому оно появится снова. Изменить в /settings; схема: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Анонимный подсчёт использования отключён для этого сеанса. Codewhale не удалось сохранить выбор, поэтому при следующем запуске вопрос повторится.",
"TelemetryPreferenceEnabledNextLaunch": "Анонимный подсчёт использования включится при следующем запуске. Разговоры и код никогда не собираются.",
"TelemetryPreferenceDisabled": "Анонимный подсчёт использования отключён. Локальные данные телеметрии удалены.",
"TelemetryPreferenceDisabledWithWarning": "Анонимный подсчёт использования отключён, но часть очистки требует внимания: {detail}",
"TelemetryPreferenceDisabledForSession": "Анонимный подсчёт использования отключён для этого сеанса, но настройка не сохранена и может вернуться при следующем запуске: {detail}",
"TelemetryPreferenceSaveFailed": "Не удалось безопасно изменить настройку телеметрии: {detail}",
"StatusPickerTitle": " Строка статуса ",
"StatusPickerInstruction": "Выберите элементы для нижней панели:",
"StatusPickerActionToggle": "перекл. ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Режим при запуске",
"ConfigLabelAllowShell": "Доступ к шеллу",
"ConfigLabelManagedAllowShell": "Доступ к шеллу (управляемый)",
"ConfigLabelTelemetry": "Анонимный подсчёт использования",
"ConfigHintTelemetry": "Сохранённая настройка анонимных счётчиков функций и ошибок; никогда разговоры или код",
"ConfigValueTelemetryOn": "Вкл. (сохранено)",
"ConfigValueTelemetryOff": "Выкл. (сохранено)",
"ConfigLabelStreamTimeout": "Тайм-аут потока",
"ConfigLabelTheme": "Тема",
"ConfigLabelLocale": "Язык",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "вибрати",
"TelemetryNoticeActionConfirm": "підтвердити",
"TelemetryNoticeActionExit": "вийти",
"TelemetryNoticeReceiptEnabled": "Так, анонімний підрахунок залишається ввімкненим.",
"TelemetryNoticeReceiptEnabled": "Анонімний підрахунок використання ввімкнено. Розмови та код ніколи не збираються. Змінити в /settings; схема: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Анонімний підрахунок використання вимкнено. Повторного запитання не буде.",
"TelemetryNoticeReceiptEnabledUnsaved": "Анонімний підрахунок залишається ввімкненим для цього сеансу. Codewhale не вдалося зберегти вибір, тому під час наступного запуску запитання повториться.",
"TelemetryNoticeReceiptEnabledUnsaved": "Анонімний підрахунок використання ввімкнено для цього сеансу. Codewhale не вдалося зберегти повідомлення, тому воно з'явиться знову. Змінити в /settings; схема: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Анонімний підрахунок використання вимкнено для цього сеансу. Codewhale не вдалося зберегти вибір, тому під час наступного запуску запитання повториться.",
"TelemetryPreferenceEnabledNextLaunch": "Анонімний підрахунок використання ввімкнеться під час наступного запуску. Розмови та код ніколи не збираються.",
"TelemetryPreferenceDisabled": "Анонімний підрахунок використання вимкнено. Локальні дані телеметрії видалено.",
"TelemetryPreferenceDisabledWithWarning": "Анонімний підрахунок використання вимкнено, але частина очищення потребує уваги: {detail}",
"TelemetryPreferenceDisabledForSession": "Анонімний підрахунок використання вимкнено для цього сеансу, але налаштування не збережено й може повернутися під час наступного запуску: {detail}",
"TelemetryPreferenceSaveFailed": "Не вдалося безпечно змінити налаштування телеметрії: {detail}",
"StatusPickerTitle": " Рядок стану ",
"StatusPickerInstruction": "Виберіть елементи, які показувати в нижньому рядку:",
"StatusPickerActionToggle": "перемкнути ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Режим запуску",
"ConfigLabelAllowShell": "Доступ до оболонки",
"ConfigLabelManagedAllowShell": "Доступ до оболонки (керовано)",
"ConfigLabelTelemetry": "Анонімний підрахунок використання",
"ConfigHintTelemetry": "Збережене налаштування анонімних лічильників функцій і помилок; ніколи розмови чи код",
"ConfigValueTelemetryOn": "Увімк. (збережено)",
"ConfigValueTelemetryOff": "Вимк. (збережено)",
"ConfigLabelStreamTimeout": "Тайм-аут потоку",
"ConfigLabelTheme": "Тема",
"ConfigLabelLocale": "Мова",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "chọn",
"TelemetryNoticeActionConfirm": "xác nhận",
"TelemetryNoticeActionExit": "thoát",
"TelemetryNoticeReceiptEnabled": "Có, tính năng đếm ẩn danh vẫn bật.",
"TelemetryNoticeReceiptEnabled": "Thống kê sử dụng ẩn danh đang bật. Hội thoại và mã nguồn không bao giờ được thu thập. Thay đổi trong /settings; lược đồ: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "Đếm lượt sử dụng ẩn danh đã tắt. Hệ thống sẽ không hỏi lại.",
"TelemetryNoticeReceiptEnabledUnsaved": "Tính năng đếm ẩn danh vẫn bật cho phiên này. Codewhale không thể lưu lựa chọn nên sẽ hỏi lại vào lần khởi động tới.",
"TelemetryNoticeReceiptEnabledUnsaved": "Thống kê sử dụng ẩn danh đang bật cho phiên này. Codewhale không thể lưu thông báo nên sẽ hiển thị lại. Thay đổi trong /settings; lược đồ: docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "Đếm lượt sử dụng ẩn danh đang tắt cho phiên này. Codewhale không thể lưu lựa chọn nên sẽ hỏi lại vào lần khởi động tới.",
"TelemetryPreferenceEnabledNextLaunch": "Thống kê sử dụng ẩn danh sẽ bật vào lần khởi động tới. Hội thoại và mã nguồn không bao giờ được thu thập.",
"TelemetryPreferenceDisabled": "Thống kê sử dụng ẩn danh đã tắt. Dữ liệu đo từ xa cục bộ đã được xóa.",
"TelemetryPreferenceDisabledWithWarning": "Thống kê sử dụng ẩn danh đã tắt, nhưng một phần dọn dẹp cần được chú ý: {detail}",
"TelemetryPreferenceDisabledForSession": "Thống kê sử dụng ẩn danh đã tắt cho phiên này, nhưng tùy chọn chưa được lưu và có thể bật lại vào lần khởi động tới: {detail}",
"TelemetryPreferenceSaveFailed": "Không thể thay đổi tùy chọn đo từ xa một cách an toàn: {detail}",
"StatusPickerTitle": " Dòng trạng thái ",
"StatusPickerInstruction": "Chọn các thành phần bạn muốn hiển thị ở cuối màn hình:",
"StatusPickerActionToggle": "bật/tắt ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "Chế độ khởi động",
"ConfigLabelAllowShell": "Quyền truy cập shell",
"ConfigLabelManagedAllowShell": "Quyền truy cập shell (được quản lý)",
"ConfigLabelTelemetry": "Thống kê sử dụng ẩn danh",
"ConfigHintTelemetry": "Tùy chọn đã lưu cho số liệu ẩn danh về tính năng và lỗi; không bao giờ gồm hội thoại hay mã nguồn",
"ConfigValueTelemetryOn": "Bật (đã lưu)",
"ConfigValueTelemetryOff": "Tắt (đã lưu)",
"ConfigLabelStreamTimeout": "Thời gian chờ luồng",
"ConfigLabelTheme": "Chủ đề",
"ConfigLabelLocale": "Ngôn ngữ",
+11 -2
View File
@@ -100,10 +100,15 @@
"TelemetryNoticeActionChoose": "选择",
"TelemetryNoticeActionConfirm": "确认",
"TelemetryNoticeActionExit": "退出",
"TelemetryNoticeReceiptEnabled": "是,匿名统计保持开启。",
"TelemetryNoticeReceiptEnabled": "匿名使用统计已开启。绝不收集对话或代码。可在 /settings 中更改;完整结构:docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "匿名使用统计已关闭。以后不会再次询问。",
"TelemetryNoticeReceiptEnabledUnsaved": "本次会话保持匿名统计开启。Codewhale 无法保存此选择,因此下次启动时会再次询问。",
"TelemetryNoticeReceiptEnabledUnsaved": "本次会话已开启匿名使用统计。Codewhale 无法保存此提示,因此会再次显示。可在 /settings 中更改;完整结构:docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "本次会话已关闭匿名使用统计。Codewhale 无法保存此选择,因此下次启动时会再次询问。",
"TelemetryPreferenceEnabledNextLaunch": "匿名使用统计将在下次启动时开启。绝不收集对话或代码。",
"TelemetryPreferenceDisabled": "匿名使用统计已关闭。本地遥测数据已清除。",
"TelemetryPreferenceDisabledWithWarning": "匿名使用统计已关闭,但部分清理需要注意:{detail}",
"TelemetryPreferenceDisabledForSession": "本次会话已关闭匿名使用统计,但偏好未保存,下次启动时可能恢复:{detail}",
"TelemetryPreferenceSaveFailed": "无法安全更改遥测偏好:{detail}",
"StatusPickerTitle": " 状态行 ",
"StatusPickerInstruction": "选择要在底部显示的项目:",
"StatusPickerActionToggle": "切换 ",
@@ -217,6 +222,10 @@
"ConfigLabelDefaultMode": "启动模式",
"ConfigLabelAllowShell": "Shell 访问",
"ConfigLabelManagedAllowShell": "Shell 访问(受管理)",
"ConfigLabelTelemetry": "匿名使用统计",
"ConfigHintTelemetry": "匿名功能和错误计数的已保存偏好;绝不包含对话或代码",
"ConfigValueTelemetryOn": "开启(已保存)",
"ConfigValueTelemetryOff": "关闭(已保存)",
"ConfigLabelStreamTimeout": "流式超时",
"ConfigLabelTheme": "主题",
"ConfigLabelLocale": "语言",
+11 -2
View File
@@ -492,6 +492,10 @@
"ConfigLabelLocale": "語言",
"ConfigLabelLowMotion": "減少動態效果",
"ConfigLabelManagedAllowShell": "Shell 存取(受管理)",
"ConfigLabelTelemetry": "匿名使用統計",
"ConfigHintTelemetry": "匿名功能和錯誤計數的已儲存偏好;絕不包含對話或程式碼",
"ConfigValueTelemetryOn": "開啟(已儲存)",
"ConfigValueTelemetryOff": "關閉(已儲存)",
"ConfigLabelManagedApprovalPolicy": "新工作階段權限(受管理)",
"ConfigLabelMaxHistory": "輸入歷史",
"ConfigLabelMcpConfigPath": "MCP 設定路徑",
@@ -818,10 +822,15 @@
"TelemetryNoticeActionChoose": "選擇",
"TelemetryNoticeActionConfirm": "確認",
"TelemetryNoticeActionExit": "離開",
"TelemetryNoticeReceiptEnabled": "是,匿名統計保持開啟。",
"TelemetryNoticeReceiptEnabled": "匿名使用統計已開啟。絕不收集對話或程式碼。可在 /settings 中更改;完整結構:docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabled": "匿名使用統計已關閉。之後不會再次詢問。",
"TelemetryNoticeReceiptEnabledUnsaved": "此工作階段保持匿名統計開啟。Codewhale 無法儲存此選擇,因此下次啟動時會再次詢問。",
"TelemetryNoticeReceiptEnabledUnsaved": "此工作階段已開啟匿名使用統計。Codewhale 無法儲存此提示,因此會再次顯示。可在 /settings 中更改;完整結構:docs/TELEMETRY.md",
"TelemetryNoticeReceiptDisabledUnsaved": "此工作階段已關閉匿名使用統計。Codewhale 無法儲存此選擇,因此下次啟動時會再次詢問。",
"TelemetryPreferenceEnabledNextLaunch": "匿名使用統計將在下次啟動時開啟。絕不收集對話或程式碼。",
"TelemetryPreferenceDisabled": "匿名使用統計已關閉。本機遙測資料已清除。",
"TelemetryPreferenceDisabledWithWarning": "匿名使用統計已關閉,但部分清理需要注意:{detail}",
"TelemetryPreferenceDisabledForSession": "此工作階段已關閉匿名使用統計,但偏好未儲存,下次啟動時可能恢復:{detail}",
"TelemetryPreferenceSaveFailed": "無法安全更改遙測偏好:{detail}",
"HistorySearchPlaceholder": "搜尋提示歷史...",
"HistorySearchTitle": "歷史搜尋",
"HomeAgentModeReviewTip": " 用 /mode plan 來先調查搜尋與生成計畫",
@@ -505,6 +505,9 @@ fn show_single_setting(app: &App, key: &str) -> CommandResult {
"search" | "search.provider" | "search_provider" => load_command_config(app)
.ok()
.map(|config| search_provider_display(&config, app.ui_locale)),
"telemetry" => load_command_config(app)
.ok()
.map(|config| crate::telemetry_notice::saved_preference_enabled(&config).to_string()),
"prompt_suggestion" => load_command_config(app)
.ok()
.map(|config| prompt_suggestion_display(&config)),
@@ -1926,6 +1929,31 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
}
match key.as_str() {
"telemetry" => {
if !persist {
return CommandResult::error(
"Telemetry is a durable privacy preference. Change it in /settings or add --save.",
);
}
let enabled = match parse_config_bool(value) {
Ok(enabled) => enabled,
Err(err) => return CommandResult::error(err),
};
let applied = crate::telemetry_notice::apply_persistent_preference(
app.config_path.clone(),
enabled,
);
let message = applied.message(app.ui_locale);
return if applied.is_error() {
CommandResult {
message: Some(message),
action: None,
is_error: true,
}
} else {
CommandResult::message(message)
};
}
"model" => {
// Support "/model auto" — auto-select model based on request complexity
if value.trim().eq_ignore_ascii_case("auto") {
+7 -2
View File
@@ -1567,8 +1567,13 @@ mod tests {
let mut app = create_test_app();
let result = execute("/goal ship layer 2 | budget: 100", &mut app);
assert!(!result.is_error);
assert_eq!(app.goal.objective.as_deref(), Some("ship layer 2"));
assert_eq!(app.goal.token_budget, Some(100));
assert!(matches!(
result.action,
Some(AppAction::SetGoalObjective {
ref objective,
token_budget: Some(100)
}) if objective == "ship layer 2"
));
// The hunt-era alias is gone: `/hunt` must not resolve anymore.
assert!(execute("/hunt ship layer 2", &mut app).is_error);
+10 -3
View File
@@ -8502,15 +8502,22 @@ fn registry_first_guidance_is_attached_to_the_shell_fallback_once() {
let mut catalog = vec![api_tool("read_file"), api_tool("exec_shell")];
apply_registry_first_shell_guidance(&mut catalog);
let after_first = catalog
.iter()
.find(|tool| tool.name == "exec_shell")
.expect("shell tool")
.description
.clone();
apply_registry_first_shell_guidance(&mut catalog);
let description = &catalog
let after_second = &catalog
.iter()
.find(|tool| tool.name == "exec_shell")
.expect("shell tool")
.description;
assert_eq!(description.matches("call registry_sync first").count(), 1);
assert!(description.contains("start_registry_mcp_server"));
assert_eq!(after_second, &after_first);
assert!(after_second.contains("registry_sync"));
assert!(after_second.contains("start_registry_mcp_server"));
}
#[test]
+18
View File
@@ -140,6 +140,11 @@ pub enum MessageId {
TelemetryNoticeReceiptDisabled,
TelemetryNoticeReceiptEnabledUnsaved,
TelemetryNoticeReceiptDisabledUnsaved,
TelemetryPreferenceEnabledNextLaunch,
TelemetryPreferenceDisabled,
TelemetryPreferenceDisabledWithWarning,
TelemetryPreferenceDisabledForSession,
TelemetryPreferenceSaveFailed,
// StatusPicker — `/statusline` multi-select footer-item picker.
StatusPickerTitle,
StatusPickerInstruction,
@@ -260,6 +265,10 @@ pub enum MessageId {
ConfigLabelDefaultMode,
ConfigLabelAllowShell,
ConfigLabelManagedAllowShell,
ConfigLabelTelemetry,
ConfigHintTelemetry,
ConfigValueTelemetryOn,
ConfigValueTelemetryOff,
ConfigLabelStreamTimeout,
ConfigLabelTheme,
ConfigLabelLocale,
@@ -1836,6 +1845,11 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::TelemetryNoticeReceiptDisabled,
MessageId::TelemetryNoticeReceiptEnabledUnsaved,
MessageId::TelemetryNoticeReceiptDisabledUnsaved,
MessageId::TelemetryPreferenceEnabledNextLaunch,
MessageId::TelemetryPreferenceDisabled,
MessageId::TelemetryPreferenceDisabledWithWarning,
MessageId::TelemetryPreferenceDisabledForSession,
MessageId::TelemetryPreferenceSaveFailed,
MessageId::StatusPickerTitle,
MessageId::StatusPickerInstruction,
MessageId::StatusPickerActionToggle,
@@ -1954,6 +1968,10 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
MessageId::ConfigLabelDefaultMode,
MessageId::ConfigLabelAllowShell,
MessageId::ConfigLabelManagedAllowShell,
MessageId::ConfigLabelTelemetry,
MessageId::ConfigHintTelemetry,
MessageId::ConfigValueTelemetryOn,
MessageId::ConfigValueTelemetryOff,
MessageId::ConfigLabelStreamTimeout,
MessageId::ConfigLabelTheme,
MessageId::ConfigLabelLocale,
+2 -18
View File
@@ -1163,7 +1163,7 @@ fn plugin_server_ids_are_unambiguous_across_hyphenated_plugin_and_server_names()
}
#[test]
fn mixed_mcp_and_unsupported_components_activate_only_reviewed_transports() {
fn mixed_components_contribute_only_mcp_servers_to_the_mcp_catalog() {
let dir = tempfile::tempdir().unwrap();
let plugin_base = dir.path().join("plugin");
fs::create_dir_all(plugin_base.join("commands")).unwrap();
@@ -1197,22 +1197,6 @@ path = "hooks"
.unwrap();
let (plugin, authority) = active_plugin_fixture(&plugin_base);
assert!(plugin.active());
assert_eq!(
plugin.compatibility(),
crate::plugins::manifest::PluginCompatibility::Partial
);
assert!(
plugin.component_active(crate::plugins::activation::PluginActivationCapability::McpStdio)
);
assert!(
plugin.component_active(crate::plugins::activation::PluginActivationCapability::McpRemote)
);
assert!(
!plugin.component_active(crate::plugins::activation::PluginActivationCapability::Commands)
);
assert!(
!plugin.component_active(crate::plugins::activation::PluginActivationCapability::Hooks)
);
let cfg = merge_plugin_mcp_servers_from_plugins(
McpConfig::default(),
@@ -1224,7 +1208,7 @@ path = "hooks"
assert_eq!(
cfg.servers.len(),
2,
"only reviewed MCP transports may activate; commands/hooks stay out of the MCP catalog: {:?}",
"declarative components must not become MCP servers: {:?}",
cfg.servers.keys().collect::<Vec<_>>()
);
assert_eq!(
+4 -1
View File
@@ -1824,7 +1824,10 @@ mod tests {
);
// `source_path` keeps pointing at the more-specific file so the
// user knows where to edit the workspace-level override.
assert_eq!(ctx.source_path, Some(workspace.path().join("AGENTS.md")));
assert_eq!(
ctx.source_path,
Some(canonicalize_workspace_or_keep(workspace.path()).join("AGENTS.md"))
);
}
#[test]
+426 -10
View File
@@ -2,8 +2,9 @@
//!
//! Rendering belongs to the native TUI in [`crate::tui::telemetry_notice`].
//! This module owns only the privacy-sensitive state transitions: deciding
//! whether a disclosure is owed, recording the user's choice, and preserving
//! an in-memory decision even when persistence fails.
//! whether a disclosure is owed, recording that the default-on disclosure was
//! drawn, and applying an explicit Settings opt-out without weakening failure
//! posture.
use std::io::IsTerminal;
use std::path::{Path, PathBuf};
@@ -12,7 +13,7 @@ use anyhow::{Result, anyhow};
use codewhale_config::{SetupState, TELEMETRY_NOTICE_VERSION};
use codewhale_telemetry::SessionSource;
use crate::localization::MessageId;
use crate::localization::{Locale, MessageId, tr};
/// Everything the native notice needs to commit the choice against the same
/// files and session source that were resolved before the first TUI frame.
@@ -57,6 +58,57 @@ pub(crate) struct AppliedTelemetryDecision {
pub(crate) status_message_id: MessageId,
}
/// Typed result of changing the durable telemetry preference from `/settings`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum TelemetryPreferenceOutcome {
EnabledNextLaunch,
Disabled,
DisabledWithWarning(String),
DisabledForSession(String),
SaveFailed(String),
}
impl AppliedTelemetryPreference {
pub(crate) fn is_error(&self) -> bool {
matches!(
self.outcome,
TelemetryPreferenceOutcome::DisabledWithWarning(_)
| TelemetryPreferenceOutcome::DisabledForSession(_)
| TelemetryPreferenceOutcome::SaveFailed(_)
)
}
pub(crate) fn message(&self, locale: Locale) -> String {
let (id, detail) = match &self.outcome {
TelemetryPreferenceOutcome::EnabledNextLaunch => {
(MessageId::TelemetryPreferenceEnabledNextLaunch, None)
}
TelemetryPreferenceOutcome::Disabled => (MessageId::TelemetryPreferenceDisabled, None),
TelemetryPreferenceOutcome::DisabledWithWarning(detail) => (
MessageId::TelemetryPreferenceDisabledWithWarning,
Some(detail),
),
TelemetryPreferenceOutcome::DisabledForSession(detail) => (
MessageId::TelemetryPreferenceDisabledForSession,
Some(detail),
),
TelemetryPreferenceOutcome::SaveFailed(detail) => {
(MessageId::TelemetryPreferenceSaveFailed, Some(detail))
}
};
let mut message = tr(locale, id).into_owned();
if let Some(detail) = detail {
message = message.replace("{detail}", detail);
}
message
}
}
/// Return a native-notice plan when this interactive launch owes disclosure.
///
/// This is read-only. It never prints, blocks on a line read, creates telemetry
@@ -123,12 +175,13 @@ fn plan_for_store_and_state(
}
}
/// Apply the native choice without ever making telemetry a launch blocker.
/// Apply the native disclosure decision without ever making telemetry a
/// launch blocker.
///
/// Disable is durable when either the root config or setup-state write lands.
/// Keep-on is durable when the notice-version record lands. When no write can
/// land, the choice still governs this process and the notice is shown again
/// next launch.
/// Disable remains supported for the legacy notice path and is durable when
/// either the root config or setup-state write lands. Default-on is durable
/// when the notice-version record lands. When no write can land, the decision
/// still governs this process and the notice is shown again next launch.
pub(crate) fn apply_decision(
pending: &PendingTelemetryNotice,
enabled: bool,
@@ -194,6 +247,196 @@ pub(crate) fn apply_decision(
}
}
/// Return the saved telemetry preference shown by `/settings`.
///
/// A missing setup record is the documented default-on state. An existing
/// unreadable record may contain an opt-out, so the Settings row fails closed
/// and shows Off rather than guessing.
pub(crate) fn saved_preference_enabled(config: &crate::config::Config) -> bool {
let Ok(path) = SetupState::path() else {
return false;
};
saved_preference_enabled_at(config, &path)
}
fn saved_preference_enabled_at(config: &crate::config::Config, setup_state_path: &Path) -> bool {
if config.telemetry == Some(false) {
return false;
}
match codewhale_telemetry::load_setup_state_for_decision_at(setup_state_path) {
// The telemetry owner returns a fresh default state for a genuinely
// absent sidecar. `None` therefore means an existing record was
// unreadable (or the path could not be inspected) and must fail closed.
Some(state) => !state.telemetry_opted_out(),
None => false,
}
}
/// Persist the `/settings` telemetry toggle through the same two privacy
/// registers as the first-run disclosure.
///
/// Turning off is successful when either durable register records the opt-out;
/// existing local telemetry is then wiped under the telemetry ordering lock.
/// The wipe writes its tombstone first, so a partial erase still fails closed.
/// Turning on is stricter: both registers must agree before the UI reports the
/// preference as enabled. Re-enabling takes effect for new sessions so a
/// process that was disabled never starts collecting again behind the user's
/// back.
pub(crate) fn apply_persistent_preference(
config_path: Option<PathBuf>,
enabled: bool,
) -> AppliedTelemetryPreference {
let setup_state_path = match SetupState::path() {
Ok(path) => path,
Err(error) => {
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(
error.to_string(),
)),
};
}
};
let telemetry_root = codewhale_config::codewhale_home()
.ok()
.map(|home| home.join(codewhale_telemetry::TELEMETRY_DIR));
apply_persistent_preference_at(config_path, setup_state_path, telemetry_root, enabled)
}
fn apply_persistent_preference_at(
config_path: Option<PathBuf>,
setup_state_path: PathBuf,
telemetry_root: Option<PathBuf>,
enabled: bool,
) -> AppliedTelemetryPreference {
if enabled {
// Keep a durable Off floor in place until both privacy registers have
// accepted the explicit re-enable. This makes every partial failure
// resolve Off on the next launch.
let mut state = match load_notice_state_at(&setup_state_path) {
Ok(state) => state,
Err(error) => {
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(
format!("privacy record: {error}"),
)),
};
}
};
if let Err(error) = write_config_preference(config_path.clone(), false) {
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(format!(
"config: {error}"
))),
};
}
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, true);
if let Err(error) = state.save_to(&setup_state_path) {
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(format!(
"privacy record: {error}"
))),
};
}
if let Err(error) = write_config_preference(config_path.clone(), true) {
let mut failures = vec![format!("config: {error}")];
if let Err(rollback) = write_config_preference(config_path, false) {
failures.push(format!("restoring the config Off floor: {rollback}"));
}
// The config Off floor is authoritative, but put the privacy
// sidecar back in the same fail-closed state as well. Otherwise a
// later manual edit could expose the partial enable as consent.
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, false);
if let Err(rollback) = state.save_to(&setup_state_path) {
failures.push(format!("restoring the privacy opt-out: {rollback}"));
}
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(
failures.join("; "),
)),
};
}
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::EnabledNextLaunch,
};
}
let config_result = write_config_preference(config_path, false);
let state_result = load_notice_state_at(&setup_state_path).and_then(|mut state| {
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, false);
state.save_to(&setup_state_path)
});
// Wipe even if both durable writes fail: a successful tombstone stops the
// already-armed process for the remainder of this session.
let wipe_result = match telemetry_root.as_deref().filter(|root| root.is_dir()) {
Some(root) => codewhale_telemetry::buffer::wipe(root),
None => Ok(()),
};
let durable = config_result.is_ok() || state_result.is_ok();
if !durable {
let mut failures = vec![
format!("config: {}", config_result.expect_err("failed result")),
format!(
"privacy record: {}",
state_result.expect_err("failed result")
),
];
if let Err(error) = wipe_result {
failures.push(format!("local erase: {error}"));
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::SaveFailed(bounded_failure_detail(
failures.join("; "),
)),
};
}
let detail = bounded_failure_detail(failures.join("; "));
return AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::DisabledForSession(detail),
};
}
let mut warnings = Vec::new();
if let Err(error) = config_result {
warnings.push(format!("config: {error}"));
}
if let Err(error) = state_result {
warnings.push(format!("privacy record: {error}"));
}
if let Err(error) = wipe_result {
warnings.push(format!("local erase: {error}"));
}
if warnings.is_empty() {
AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::Disabled,
}
} else {
AppliedTelemetryPreference {
outcome: TelemetryPreferenceOutcome::DisabledWithWarning(bounded_failure_detail(
warnings.join("; "),
)),
}
}
}
fn bounded_failure_detail(detail: String) -> String {
const MAX_CHARS: usize = 240;
let single_line = detail.split_whitespace().collect::<Vec<_>>().join(" ");
let mut chars = single_line.chars();
let bounded = chars.by_ref().take(MAX_CHARS).collect::<String>();
if chars.next().is_some() {
format!("{bounded}")
} else {
bounded
}
}
fn write_config_preference(config_path: Option<PathBuf>, enabled: bool) -> Result<()> {
let mut store = codewhale_config::ConfigStore::load(config_path)?;
store
.config
.set_value("telemetry", if enabled { "true" } else { "false" })?;
store.save()
}
/// Load a missing sidecar as a fresh state, but distinguish it from an
/// existing unreadable/corrupt sidecar so the notice can never overwrite the
/// latter with defaults.
@@ -208,8 +451,7 @@ fn load_notice_state_at(path: &Path) -> Result<SetupState> {
.ok_or_else(|| anyhow!("{} could not be read as setup state", path.display()))
}
/// Everything that decides whether the question may be put, as opposed to how
/// it is answered.
/// Everything that decides whether the disclosure may be shown.
struct NoticeGate {
needs_notice: bool,
persisted_off: bool,
@@ -417,6 +659,180 @@ mod tests {
);
}
#[test]
fn settings_saved_preference_is_default_on_but_fails_closed_on_unreadable_state() {
let dir = tempfile::tempdir().expect("tempdir");
let state_path = dir.path().join("setup_state.json");
let config = crate::config::Config::default();
assert!(
saved_preference_enabled_at(&config, &state_path),
"a genuinely missing privacy record uses the documented default"
);
std::fs::write(&state_path, "not-json").expect("seed corrupt state");
assert!(
!saved_preference_enabled_at(&config, &state_path),
"an unreadable privacy record may contain an opt-out"
);
let explicitly_off = crate::config::Config {
telemetry: Some(false),
..crate::config::Config::default()
};
assert!(!saved_preference_enabled_at(&explicitly_off, &state_path));
}
#[test]
fn settings_off_persists_both_registers_and_stops_the_armed_buffer() {
let dir = tempfile::tempdir().expect("tempdir");
let config_path = dir.path().join("config.toml");
let state_path = dir.path().join("setup_state.json");
let telemetry_root = dir.path().join("telemetry");
std::fs::write(&config_path, "telemetry = true\n").expect("seed config");
std::fs::create_dir_all(&telemetry_root).expect("seed telemetry root");
std::fs::write(
codewhale_telemetry::buffer::buffer_path(&telemetry_root),
"queued-event\n",
)
.expect("seed buffer");
let applied = apply_persistent_preference_at(
Some(config_path.clone()),
state_path.clone(),
Some(telemetry_root.clone()),
false,
);
assert_eq!(applied.outcome, TelemetryPreferenceOutcome::Disabled);
assert!(
std::fs::read_to_string(&config_path)
.expect("read config")
.contains("telemetry = false")
);
assert!(
SetupState::load_from(&state_path)
.expect("saved state")
.telemetry_opted_out()
);
assert!(codewhale_telemetry::buffer::tombstone_present(
&telemetry_root
));
assert_eq!(
std::fs::read_to_string(codewhale_telemetry::buffer::buffer_path(&telemetry_root))
.expect("read wiped buffer"),
""
);
}
#[test]
fn settings_on_is_saved_for_next_launch_without_clearing_the_tombstone() {
let dir = tempfile::tempdir().expect("tempdir");
let config_path = dir.path().join("config.toml");
let state_path = dir.path().join("setup_state.json");
let telemetry_root = dir.path().join("telemetry");
std::fs::write(&config_path, "telemetry = true\n").expect("seed config");
std::fs::create_dir_all(&telemetry_root).expect("seed telemetry root");
let disabled = apply_persistent_preference_at(
Some(config_path.clone()),
state_path.clone(),
Some(telemetry_root.clone()),
false,
);
assert_eq!(disabled.outcome, TelemetryPreferenceOutcome::Disabled);
let enabled = apply_persistent_preference_at(
Some(config_path.clone()),
state_path.clone(),
Some(telemetry_root.clone()),
true,
);
assert_eq!(
enabled.outcome,
TelemetryPreferenceOutcome::EnabledNextLaunch
);
assert!(
std::fs::read_to_string(&config_path)
.expect("read config")
.contains("telemetry = true")
);
assert!(
SetupState::load_from(&state_path)
.expect("saved state")
.telemetry_accepted(TELEMETRY_NOTICE_VERSION)
);
assert!(
codewhale_telemetry::buffer::tombstone_present(&telemetry_root),
"the already-running process stays off; the next launch clears this after a fresh permission check"
);
let store = codewhale_config::ConfigStore::load(Some(config_path))
.expect("reload enabled config for the next launch");
let resolved = store
.config
.resolve_runtime_options(&codewhale_config::CliRuntimeOverrides::default());
let reloaded_state = SetupState::load_from(&state_path).expect("reload enabled state");
assert!(
codewhale_telemetry::decide_in_home(
Some(dir.path()),
&resolved,
&reloaded_state,
codewhale_telemetry::Surface::Tui,
)
.is_enabled(),
"a fresh launch may clear the prior tombstone only after both saved registers resolve enabled"
);
}
#[test]
fn failed_settings_enable_preserves_the_existing_opt_out() {
let dir = tempfile::tempdir().expect("tempdir");
let config_path = dir.path().to_path_buf();
let state_path = dir.path().join("setup_state.json");
let mut state = SetupState::default();
state.record_telemetry_notice(TELEMETRY_NOTICE_VERSION, false);
state.save_to(&state_path).expect("seed opt-out");
let applied =
apply_persistent_preference_at(Some(config_path), state_path.clone(), None, true);
assert!(matches!(
applied.outcome,
TelemetryPreferenceOutcome::SaveFailed(_)
));
assert!(
SetupState::load_from(&state_path)
.expect("saved state")
.telemetry_opted_out()
);
}
#[test]
fn unsaved_settings_off_still_suppresses_the_current_session() {
let dir = tempfile::tempdir().expect("tempdir");
let config_path = dir.path().to_path_buf();
let state_path = dir.path().join("setup_state.json");
let telemetry_root = dir.path().join("telemetry");
std::fs::write(&state_path, "not-json").expect("seed corrupt state");
std::fs::create_dir_all(&telemetry_root).expect("seed telemetry root");
let applied = apply_persistent_preference_at(
Some(config_path),
state_path,
Some(telemetry_root.clone()),
false,
);
assert!(matches!(
applied.outcome,
TelemetryPreferenceOutcome::DisabledForSession(_)
));
assert!(codewhale_telemetry::buffer::tombstone_present(
&telemetry_root
));
}
#[test]
fn a_non_tty_test_surface_cannot_schedule_the_native_notice() {
assert!(matches!(
+8 -3
View File
@@ -769,13 +769,18 @@ mod tests {
// The strip must not push the right-hand key hints off the row.
assert!(text.contains("keys"), "{text}");
// A 150-column idle row shares the line with the full key hints, so
// every group keeps its headline fact and sheds its second cell.
// A 150-column idle row now owns the quiet route identity requested
// for the footer. It keeps the highest-value session facts and sheds
// lower-priority cells before it crowds out the key hints.
let text = strip_text(&mut app, 150);
assert!(
text.contains("4 turns │ LLM 3.5s │ TTFT avg 400ms │ Cache hit 99% │ Input 9.3M"),
text.contains("DeepSeek · deepseek-v4-flash · max"),
"{text}"
);
assert!(text.contains("4 turns"), "{text}");
assert!(text.contains("LLM 3.5s"), "{text}");
assert!(text.contains("Cache hit 99%"), "{text}");
assert!(text.contains("Input 9.3M"), "{text}");
assert!(text.contains("keys"), "{text}");
}
+8 -1
View File
@@ -1681,7 +1681,14 @@ pub(crate) async fn apply_command_result(
}
AppAction::OpenProviderPicker => {
if app.onboarding == OnboardingState::Provider {
open_onboarding_provider_picker(app, config, engine_handle, true).await;
let recover_configured_route = app.onboarding_missing_key_recovery;
open_onboarding_provider_picker(
app,
config,
engine_handle,
recover_configured_route,
)
.await;
} else if app.view_stack.top_kind() != Some(ModalKind::ProviderPicker) {
let runtime_status = query_provider_runtime_status(engine_handle).await;
app.view_stack.push(
+16 -18
View File
@@ -514,11 +514,13 @@ pub async fn run_tui(
(handle, task)
});
// Returning users with a missing key begin directly in the same canonical
// provider setup picker as first-run. Focus their persisted route so
// recovery does not silently replace a Kimi Code bare-K3 endpoint.
// Fresh users begin with the small local-runtime list. Returning users
// recovering a missing key focus their persisted route instead, so
// recovery cannot silently replace a Kimi Code bare-K3 endpoint.
if app.onboarding == OnboardingState::Provider {
open_onboarding_provider_picker(&mut app, config, &engine_handle, true).await;
let recover_configured_route = app.onboarding_missing_key_recovery;
open_onboarding_provider_picker(&mut app, config, &engine_handle, recover_configured_route)
.await;
}
// #4605: create the dispatch completion channel before any submit path so
@@ -671,12 +673,7 @@ fn apply_telemetry_after_disclosure_draw(
let applied = crate::telemetry_notice::apply_decision(&pending, true);
if applied.status_message_id != MessageId::TelemetryNoticeReceiptEnabled {
let receipt = app.tr(applied.status_message_id);
let settings = app.tr(MessageId::CmdSettingsDescription);
app.push_status_toast(
format!("{receipt} /settings — {settings}"),
StatusToastLevel::Info,
Some(12_000),
);
app.push_status_toast(receipt.into_owned(), StatusToastLevel::Info, Some(12_000));
app.needs_redraw = true;
}
crate::apply_tui_telemetry_decision(&pending, &applied.setup_state);
@@ -785,12 +782,7 @@ pub(crate) async fn run_event_loop(
&& let Some(pending) = pending_telemetry_notice.take()
{
let receipt = app.tr(MessageId::TelemetryNoticeReceiptEnabled);
let settings = app.tr(MessageId::CmdSettingsDescription);
app.push_status_toast(
format!("{receipt} /settings — {settings}"),
StatusToastLevel::Info,
Some(12_000),
);
app.push_status_toast(receipt.into_owned(), StatusToastLevel::Info, Some(12_000));
app.needs_redraw = true;
telemetry_waiting_for_disclosure_draw = Some(pending);
}
@@ -3916,8 +3908,14 @@ pub(crate) async fn run_event_loop(
onboarding::advance_onboarding_after_language(app);
}
OnboardingState::Provider => {
open_onboarding_provider_picker(app, config, &engine_handle, false)
.await;
let recover_configured_route = app.onboarding_missing_key_recovery;
open_onboarding_provider_picker(
app,
config,
&engine_handle,
recover_configured_route,
)
.await;
}
OnboardingState::TrustDirectory => {
// Trusting a workspace is a security boundary, so it
+18
View File
@@ -822,6 +822,18 @@ pub(crate) async fn handle_config_updated(
commands::set_config_value(app, &key, &value, persist),
persist,
);
let telemetry_toast = (key == "telemetry")
.then(|| {
result.message.clone().map(|message| {
let level = if result.is_error {
StatusToastLevel::Error
} else {
StatusToastLevel::Success
};
(message, level)
})
})
.flatten();
let normalized_value = value.trim().to_ascii_lowercase().replace([' ', '_'], "-");
let cleared_root_approval = !result.is_error
&& persist
@@ -858,6 +870,12 @@ pub(crate) async fn handle_config_updated(
&key
};
refresh_config_view_if_open(app, focus_key);
if let Some((message, level)) = telemetry_toast {
// The modal stays open, so a transcript-only command receipt would be
// invisible. Keep the durable disk truth in the rebuilt row and show
// the localized result above it.
app.push_status_toast(message, level, Some(12_000));
}
Ok(false)
}
+9 -8
View File
@@ -203,17 +203,18 @@ pub(crate) async fn cycle_permission_posture(
}
}
/// Open the one canonical provider setup surface for onboarding. Fresh
/// onboarding starts at the full catalog; missing-key recovery focuses the
/// current route so an exact Kimi Code K3 configuration can expose its plan
/// route before a secret is entered. Either way the picker opens on the
/// navigable list (#4763): onboarding never drops a user straight into a
/// key/OAuth prompt for a route they were not shown.
/// Open the one canonical provider setup surface for onboarding. Fresh
/// onboarding starts with the small local-runtime list and leaves the full
/// cloud catalog one explicit `A` away. Missing-key recovery instead focuses
/// the already-configured route so an exact Kimi Code K3 configuration can
/// expose its plan route before a secret is entered. Either way the picker
/// opens on the navigable list (#4763): onboarding never drops a user straight
/// into a key/OAuth prompt for a route they were not shown.
pub(crate) async fn open_onboarding_provider_picker(
app: &mut App,
config: &Config,
engine_handle: &EngineHandle,
focus_current_route: bool,
recover_configured_route: bool,
) {
if app.onboarding != OnboardingState::Provider
|| app.view_stack.top_kind() == Some(ModalKind::ProviderPicker)
@@ -224,7 +225,7 @@ pub(crate) async fn open_onboarding_provider_picker(
app.view_stack.push(
crate::tui::provider_picker::ProviderPickerView::new_for_onboarding(
app.api_provider,
focus_current_route.then_some(app.onboarding_provider),
recover_configured_route.then_some(app.onboarding_provider),
config,
runtime_status,
)
+1
View File
@@ -983,6 +983,7 @@ fn closing_work_inspector_pager_clears_opened_row_owner() {
"Inspect me".to_string(),
crate::tools::todo::TodoStatus::InProgress,
);
let _ = render_underwater_test_app(&mut app, 100, 32);
let _ = crate::tui::work_surface::handle_key(
&mut app,
KeyEvent::new(KeyCode::Char('w'), KeyModifiers::ALT),
+15
View File
@@ -1750,6 +1750,13 @@ impl ConfigView {
scope: ConfigScope::Saved,
},
shell_row,
ConfigRow {
section: ConfigSection::Network,
key: "telemetry".to_string(),
value: crate::telemetry_notice::saved_preference_enabled(&config).to_string(),
editable: true,
scope: ConfigScope::Saved,
},
ConfigRow {
section: ConfigSection::Network,
key: "stream_chunk_timeout_secs".to_string(),
@@ -2945,6 +2952,7 @@ fn config_label_message(key: &str) -> Option<MessageId> {
"default_mode" => MessageId::ConfigLabelDefaultMode,
"allow_shell" => MessageId::ConfigLabelAllowShell,
"managed_allow_shell" => MessageId::ConfigLabelManagedAllowShell,
"telemetry" => MessageId::ConfigLabelTelemetry,
"stream_chunk_timeout_secs" => MessageId::ConfigLabelStreamTimeout,
"theme" => MessageId::ConfigLabelTheme,
"locale" => MessageId::ConfigLabelLocale,
@@ -3024,6 +3032,9 @@ fn humanize_config_key(key: &str) -> String {
}
fn config_hint_for_key(locale: Locale, key: &str) -> Cow<'static, str> {
if key == "telemetry" {
return tr(locale, MessageId::ConfigHintTelemetry);
}
if key == "provider_url" {
return tr(locale, MessageId::ConfigHintProviderUrl);
}
@@ -3052,6 +3063,7 @@ fn config_literal_hint_for_key(key: &str) -> &'static str {
"a project, profile, environment, or managed config controls shell access"
}
"allow_shell" => "on exposes shell tools in Agent mode; permission rules still apply",
"telemetry" => "anonymous usage counts only; never conversations or code",
"composer_multiline_mode" => {
"off: Enter sends, Shift+Enter adds a line; on: Enter adds a line, Shift+Enter sends"
}
@@ -3160,6 +3172,7 @@ fn config_boolean_key(key: &str) -> bool {
matches!(
key,
"allow_shell"
| "telemetry"
| "calm_mode"
| "low_motion"
| "fancy_animations"
@@ -3290,6 +3303,8 @@ fn canonical_config_choice(key: &str, value: &str) -> String {
fn config_choice_label(locale: Locale, key: &str, value: &str) -> String {
let label = match (key, value) {
("telemetry", "true") => tr(locale, MessageId::ConfigValueTelemetryOn).to_string(),
("telemetry", "false") => tr(locale, MessageId::ConfigValueTelemetryOff).to_string(),
(key, "true") if config_boolean_key(key) => "On".to_string(),
(key, "false") if config_boolean_key(key) => "Off".to_string(),
("approval_mode" | "permission_posture" | "approval_policy", "ask") => "Ask".to_string(),
+18 -16
View File
@@ -4283,13 +4283,13 @@ mod tests {
COMPOSER_PLACEHOLDER, COMPOSER_PROMPT_GUTTER_WIDTH, ChatWidget, ComposerWidget, Renderable,
SlashMenuEntry, active_entry_revision, apply_detail_target_highlight,
apply_selection_to_line, apply_send_flash, approval_palette, approval_truncation_hint,
build_empty_state_lines, composer_content_geometry, composer_height, composer_max_height,
composer_top_padding, cursor_row_col, empty_composer_visual_rows,
enclosed_composer_panel_fits, fish_flee_offset, fish_heading, fish_mark,
history_entry_revision, layout_input, layout_input_with_scroll, placeholder_visual_lines,
push_command_entry, receipt_is_settling, revision_in_domain, should_render_empty_state,
slash_completion_hints, tool_run_summary_revision, wrap_input_lines,
wrap_input_lines_for_mouse, wrap_text,
build_empty_state_lines, composer_content_geometry, composer_empty_hint_text,
composer_height, composer_max_height, composer_top_padding, cursor_row_col,
empty_composer_visual_rows, enclosed_composer_panel_fits, fish_flee_offset, fish_heading,
fish_mark, history_entry_revision, layout_input, layout_input_with_scroll,
placeholder_visual_lines, push_command_entry, receipt_is_settling, revision_in_domain,
should_render_empty_state, slash_completion_hints, tool_run_summary_revision,
wrap_input_lines, wrap_input_lines_for_mouse, wrap_text,
};
use crate::config::{ApiProvider, Config};
use crate::localization::Locale;
@@ -5542,8 +5542,8 @@ mod tests {
None,
ApiProvider::Deepseek,
);
assert!(hints.iter().any(|hint| hint.name == "/skill"));
assert!(hints.iter().any(|hint| hint.name == "/skills"));
assert!(!hints.iter().any(|hint| hint.name == "/skill"));
assert!(!hints.iter().any(|hint| hint.name == "/skills"));
assert!(!hints.iter().any(|hint| hint.is_skill));
}
@@ -6016,8 +6016,14 @@ mod tests {
panic!("empty composer should expose cursor position");
};
let rendered = buffer_text(&buf, area);
let placeholder = composer_empty_hint_text(&app).into_owned();
let first_placeholder_cell = placeholder
.chars()
.next()
.expect("composer placeholder should not be empty")
.to_string();
assert_eq!(buf[(cursor_x, cursor_y)].symbol(), "W");
assert_eq!(buf[(cursor_x, cursor_y)].symbol(), first_placeholder_cell);
assert_eq!(
buf[(cursor_x, cursor_y)].fg,
app.ui_theme.text_soft,
@@ -6030,11 +6036,11 @@ mod tests {
"the idle prompt should remain upright at distance"
);
assert!(
rendered.contains(COMPOSER_PLACEHOLDER),
rendered.contains(&placeholder),
"placeholder hint should render on the prompt row: {rendered}"
);
assert!(
row_text(&buf, area, cursor_y).contains(COMPOSER_PLACEHOLDER),
row_text(&buf, area, cursor_y).contains(&placeholder),
"prompt and hint should share one row: {rendered}"
);
assert!(
@@ -6595,10 +6601,6 @@ mod tests {
rendered.contains("><>") || rendered.contains("<><"),
"flat means a plain surface, not a lifeless ocean — idle fish must survive:\n{rendered}"
);
assert!(
(0..area.height).any(|y| (0..area.width).any(|x| buf[(x, y)].symbol() == "F")),
"Fleet setup remains available in flat mode"
);
}
#[test]
@@ -775,6 +775,26 @@ fn expect_visible(tui: &mut Harness, needle: &str, label: &str) {
}
}
#[cfg(all(unix, feature = "long-running-tests"))]
fn wait_for_composer_ready(tui: &mut Harness) {
if tui
.wait_for(
|frame| {
let (row, _) = frame.cursor();
frame.any_visible_text() && row >= frame.rows().saturating_sub(3)
},
BINARY_ACCEPTANCE_TIMEOUT,
)
.is_err()
{
panic!(
"TUI did not paint a focused composer within {:?}\n{}",
qa_harness::harness::ci_scaled(BINARY_ACCEPTANCE_TIMEOUT),
short_diagnostics(tui, None)
);
}
}
#[cfg(all(unix, feature = "long-running-tests"))]
fn wait_for_log(tui: &mut Harness, path: &std::path::Path, needle: &str) {
let budget = qa_harness::harness::ci_scaled(BINARY_ACCEPTANCE_TIMEOUT);
@@ -829,8 +849,9 @@ async fn plugin_toml_binary_lifecycle_skill_and_stdio_mcp_acceptance() {
.size(52, 200)
.spawn()
.expect("start distributed TUI binary");
expect_visible(&mut tui, "Write a task", "TUI composer");
// Readiness is a painted, focused composer—not localized placeholder copy.
wait_for_composer_ready(&mut tui);
submit_tui_command(&mut tui, "/plugin show demo");
expect_visible(
&mut tui,
+11 -7
View File
@@ -77,13 +77,17 @@ isolation by itself.
settings` are answered by Codewhale itself from the run journal and the live
run state — they never spend a model turn, so a status check is free and a
cancel lands even while the model is busy. `/workflow cancel` with no id stops
the only running workflow. `/workflow run <path/to/x.workflow.js>` launches a
checked-in workflow as-is; `/workflow <objective>` and bare `/workflow` ask
the model to author and run one. The `[workflow]` table above is read from
your `config.toml` for every launch decision (auto-start, write-approval
card, child limits); `/workflow settings` prints the effective values with
what each one does. Reloading `config.toml` refreshes that table for both
settings and the workflow tool.
the only running workflow.
Starting work is review-first. `/workflow <objective>` and bare `/workflow`
ask the model for a bounded, tool-less proposal; `/workflow run
<path/to/x.workflow.js>` prepares a review of that exact checked-in source.
Neither form executes anything. After reviewing the proposal, run `/workflow
confirm` to launch the latest reviewed draft. The `[workflow]` table above is
read from your `config.toml` for every launch decision (auto-start,
write-approval card, child limits); `/workflow settings` prints the effective
values with what each one does. Reloading `config.toml` refreshes that table
for both settings and the workflow tool.
`/workflows` opens the run dashboard: every run this workspace's journal
keeps for the session — running and finished — newest first. Each row shows
+4 -4
View File
@@ -40,10 +40,10 @@ controls do not blur together.
### Managing the user-global constitution (`/setup` and `/constitution`)
On first launch Codewhale offers a recommended **working agreement**. Press
Enter to accept the balanced bundled/default law, or press `C` to customize a
full constitution. Provider/model readiness and runtime posture stay separate
from this guidance, and you can reopen the setup hub any time with `/setup`.
The bundled **working agreement** is the safe default and no longer adds a
required first-run screen. Customize it later through `/constitution` or the
progressive `/setup` guide. Provider/model readiness, workspace trust, and
runtime posture stay separate from this guidance.
On the **Constitution** step:
+12 -5
View File
@@ -79,11 +79,18 @@ Launch Codewhale from the repository or directory you want it to work in:
codewhale
```
On first launch, Codewhale opens with a recommended working agreement. Press
Enter to accept the balanced, safety-conscious default and start working, or
press `C` to customize the full constitution. This guidance never changes
runtime permissions. Provider and API-key setup stays separate and can be
completed later; revisit the setup hub with `/setup` at any time.
On first launch, Codewhale asks only for decisions this installation still
needs: language when it cannot infer one, a provider when no usable route is
configured, and workspace trust when the folder requires a decision. The
provider step includes an explicit offline route. The ready screen then opens
the real composer, preserving a task supplied on the command line or suggesting
a first task for the current folder.
Everything optional stays available after that. Use `/setup` for the
progressive setup and repair guide, `/settings` for the full typed editor, and
`/constitution` when you want to customize the bundled working agreement.
The localized telemetry choice appears only after the workspace is ready and
does not block the composer.
DeepSeek is the default provider. If you want to configure its key before or
after the first launch, the most direct setup path is:
+7 -7
View File
@@ -85,13 +85,13 @@ The fast `deepseek-v4-flash` / thinking-off path is called Fin in the product
language. Fin is a seam for routing, summaries, cheap child calls, and
coordination work; it does not change approval behavior.
The orchestration trio is the first selection from a blank prompt (press `/`,
then Enter):
`/auto` turns on Auto-Review so the agent just works, `/goal` keeps one
objective across turns, and `/workflow` runs a repeatable ordered or fan-out
workflow. They also
sit on the idle welcome, the idle footer, and the first three Hotbar slots
when the bar is enabled.
The orchestration controls remain available without taking over the starting
screen: `/auto` turns on Auto-Review so the agent just works, `/goal` keeps one
objective across turns, and `/workflow` prepares a repeatable ordered or
fan-out workflow. They are directly callable and searchable through the full
command palette, but are not pinned to the starter slash menu, idle welcome,
footer, or default Hotbar. A bare `/` instead opens the small task-oriented
starter set; use `/help` or the command palette for the complete inventory.
`/goal <objective>` sets a session objective with an optional token budget and
keeps active objectives visible as Work context. The agent may also create the
+3
View File
@@ -57,6 +57,9 @@ generic checklist does not enumerate.
(the recent-releases slice embedded in the binary for `/change`). Do
not edit that file by hand, and do not copy the full root changelog
into it — older entries live in `docs/CHANGELOG_ARCHIVE.md`.
- [ ] Run `scripts/release/check-feature-release-notes.sh vPREV HEAD`. Every
issue-linked `feat` commit must leave a receipt in `CHANGELOG.md` or the
archive; the Version drift CI gate runs the same check with full history.
## 2. Version pins are in sync
+27 -21
View File
@@ -1,13 +1,13 @@
# Codewhale product telemetry
**Status for 0.9.6: anonymous usage counting is on by default and can be
disabled immediately.** The first interactive launch summarizes what is counted,
links the exact field-by-field schema, and preselects "Keep on" in a native
startup modal. Arrow keys or Tab choose, Enter confirms, and `Y`/`N` are direct
shortcuts. Telemetry remains unarmed until that choice. Headless surfaces follow
the same documented default without pretending an interactive notice was shown.
Every decline recorded by the former 0.9.4 opt-in notice remains off after
upgrade.
**Status for 0.9.10: anonymous usage counting is on by default and can be
disabled immediately.** The first interactive launch shows one localized,
nonblocking notice after the terminal is ready. It says what is never collected,
points to this field-by-field schema, and links the ordinary `/settings` toggle.
Telemetry remains unarmed until that notice has actually been drawn. Headless
surfaces follow the same documented default without pretending an interactive
notice was shown. Every decline recorded by the former 0.9.4 opt-in notice
remains off after upgrade.
Codewhale does not collect conversations, code, prompts, files, file/repo/branch
names, model content, or credentials. It sends no per-turn or per-tool timeline.
@@ -68,9 +68,9 @@ want the erasing kind, use the config file.
A value this list cannot read also resolves to off — a typo in a kill switch
must never resolve to "on".
The first-run notice is not shown at all when either switch is already set:
it never asks a question this environment would override, and answering it
never rewrites a `telemetry = false` you put there yourself.
The first-run notice is not shown when telemetry is already persistently off or
a run-scoped kill switch is active. The notice never rewrites a
`telemetry = false` you put there yourself.
A repo-local `.codewhale/config.toml` can set neither `telemetry` nor
`telemetry_endpoint`, and a workspace `.env` can set neither. Someone else's
@@ -107,12 +107,18 @@ claim one.
## When anything is sent, and where
Nothing is sent when the persistent opt-out or a run-scoped kill switch is in
force. Otherwise there is exactly one flush point: an attempt during shutdown,
bounded at three seconds. There is no
startup flush, mid-session flush, per-turn flush, or per-tool-call flush. The
shutdown flush re-resolves your setting from disk immediately beforehand, so
`codewhale config set telemetry false` written from another terminal stops the
flush of a session that is already running.
force. TUI and `exec` sessions have exactly one network flush point: an attempt
during shutdown, bounded at three seconds. Short CLI commands such as `config`,
`doctor`, and `auth` do not wait for that network request. They record
`session_end`, seal the event to the local buffer with a much shorter bound, and
return; a configured endpoint sends those buffered events with the next
interactive shutdown flush. An explicitly empty endpoint finalizes its local
dry-run batch immediately.
There is no startup flush, mid-session flush, per-turn flush, or per-tool-call
flush. Every network shutdown flush re-resolves your setting from disk
immediately beforehand, so `codewhale config set telemetry false` written from
another terminal stops the flush of a session that is already running.
A flush is **one `POST`** to the resolved endpoint — by default
`https://telemetry.codewhale.net/v1/telemetry`. The request carries a
@@ -147,7 +153,7 @@ most once per flush point and never grows a queue.
"sent_at": "2026-08-03T18:04:11Z", // RFC3339 UTC, second precision
"install_id": "3f2a…", // uuid v4, rotates every 90 days
"app_version": "0.9.4",
"git_sha": null, // non-null only for release-CI builds
"git_sha": null, // non-null only for SHA-stamped builds
"surface": "tui",
"os": "macos",
"arch": "aarch64",
@@ -163,7 +169,7 @@ most once per flush point and never grows a queue.
| `sent_at` | RFC3339 | `chrono::Utc::now()` | Second precision. Per-**batch** only — events carry no timestamps at all. |
| `install_id` | uuid v4 | `crates/telemetry/src/envelope.rs` | Random, never derived, rotated every 90 days. See "Where it lives" above. |
| `app_version` | string | `env!("CARGO_PKG_VERSION")`, as at `crates/telemetry/src/lib.rs:112` | Must match `^\d+\.\d+\.\d+(-[0-9A-Za-z.]+)?$`. |
| `git_sha` | string \| null | `option_env!("CODEWHALE_RELEASE_BUILD_SHA")` — a **new** rustc-env | First 12 hex chars. Emitted **only** when `codewhale_build_support::release_build_sha` saw `DEEPSEEK_BUILD_SHA` or `GITHUB_SHA` in the build environment, i.e. only for release-CI builds. `null` for every locally built binary, unconditionally, with no runtime lookup of any kind. **Never** `CODEWHALE_BUILD_COMMIT` — that falls back to `git_commit` and is the builder's private HEAD. **Never** `Thread.git_sha` (`crates/state/src/lib.rs:93`) — that is the user's workspace commit and a red line, one identifier away by name. |
| `git_sha` | string \| null | `option_env!("CODEWHALE_RELEASE_BUILD_SHA")` — a **new** rustc-env | First 12 hex chars. Emitted **only** when `codewhale_build_support::release_build_sha` saw a valid full SHA in `CODEWHALE_BUILD_SHA`, the legacy `DEEPSEEK_BUILD_SHA`, or `GITHUB_SHA`, in that precedence order. `null` for every unstamped build, with no runtime lookup of any kind. **Never** `CODEWHALE_BUILD_COMMIT` — that falls back to `git_commit` and is the builder's private HEAD. **Never** `Thread.git_sha` (`crates/state/src/lib.rs:93`) — that is the user's workspace commit and a red line, one identifier away by name. |
| `surface` | enum | set explicitly at each subcommand dispatch | `tui \| exec \| cli \| app-server \| mcp-server \| serve`. **Not derivable from the executable**: `codewhale-tui` serves at least five surfaces, and app-server runs *in-process* inside `codewhale` (`crates/cli/src/lib.rs:4225`), so `current_exe()` would report every app-server session as CLI. `desktop` is omitted — no desktop surface exists. Which of these can emit is governed by the opt-out policy, not by the surface: see "Which surfaces emit" below. |
| `os` | enum | `std::env::consts::OS`, as at `crates/cli/src/update.rs:41` | Whitelist: `linux \| macos \| windows \| freebsd \| android \| other`. |
| `arch` | enum | `std::env::consts::ARCH` | `x86_64 \| aarch64 \| other`. |
@@ -178,8 +184,8 @@ most once per flush point and never grows a queue.
A surface emits by default unless the machine has a persistent opt-out or the
run has a kill switch. The notice is only rendered on a TTY. So:
- **`tui`** — enters the native TUI first, shows the disclosure as a startup
modal, stays unarmed until the first interactive choice, then follows it.
- **`tui`** — enters the native TUI first, draws the localized nonblocking
disclosure, and stays unarmed until that frame is visible.
- **`exec`, `cli`, `app-server`, `mcp-server`, `serve`** — follow the documented
default on a fresh home and every persistent/run-scoped opt-out on any home.
- **Fleet workers never emit**, on any surface, by construction (`crates/tui/src/fleet/host.rs:1386-1388`).
+71
View File
@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# Require issue-linked feature commits in a release range to leave a durable
# changelog receipt. This catches shipped user-visible features that otherwise
# disappear when the GitHub Release body is generated from CHANGELOG.md.
set -euo pipefail
base_ref="${1:?usage: $0 <base-ref> <head-ref> [notes-file ...]}"
head_ref="${2:?usage: $0 <base-ref> <head-ref> [notes-file ...]}"
shift 2
repo_root="${RELEASE_NOTE_REPO_ROOT:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
cd "${repo_root}"
if [[ "$#" -gt 0 ]]; then
notes_files=("$@")
else
notes_files=(CHANGELOG.md docs/CHANGELOG_ARCHIVE.md)
fi
for ref in "${base_ref}" "${head_ref}"; do
if ! git rev-parse --verify --quiet "${ref}^{commit}" >/dev/null; then
echo "error: release-note check cannot resolve ${ref}" >&2
exit 2
fi
done
for notes_file in "${notes_files[@]}"; do
if [[ ! -f "${notes_file}" ]]; then
echo "error: release-note check cannot read ${notes_file}" >&2
exit 2
fi
done
fail=0
checked=0
feature_subject_pattern='^feat(\([^)]*\))?!?:[[:space:]]'
while IFS= read -r -d '' sha &&
IFS= read -r -d '' subject &&
IFS= read -r -d '' body; do
[[ "${subject}" =~ ${feature_subject_pattern} ]] || continue
feature_text="${subject}"$'\n'"${body}"
while IFS= read -r issue; do
[[ -n "${issue}" ]] || continue
checked=$((checked + 1))
found=0
for notes_file in "${notes_files[@]}"; do
if grep -Eq "#${issue}([^[:alnum:]_]|$)" "${notes_file}"; then
found=1
break
fi
done
if [[ "${found}" -eq 0 ]]; then
echo "::error::Feature commit ${sha:0:12} references #${issue}, but no release-note receipt exists in ${notes_files[*]}." >&2
echo " ${subject}" >&2
fail=1
fi
done < <(
printf '%s\n' "${feature_text}" \
| grep -oE '#[0-9]+([^[:alnum:]_]|$)' \
| sed -E 's/^#([0-9]+).*/\1/' \
| sort -u \
|| true
)
done < <(git log -z --no-merges --format='%H%x00%s%x00%b' "${base_ref}..${head_ref}")
if [[ "${fail}" -ne 0 ]]; then
echo "Add the missing user-visible feature to the appropriate changelog section before release." >&2
exit 1
fi
echo "Feature release-note receipts OK: ${checked} linked issue reference(s) checked in ${base_ref}..${head_ref}."
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
checker="${repo_root}/scripts/release/check-feature-release-notes.sh"
fixture="$(mktemp -d)"
trap 'rm -rf "${fixture}"' EXIT
git -C "${fixture}" init -q
git -C "${fixture}" config user.name "Release Test"
git -C "${fixture}" config user.email "release-test@example.com"
mkdir -p "${fixture}/docs"
printf '# Changelog\n' >"${fixture}/CHANGELOG.md"
printf '# Archive\n' >"${fixture}/docs/CHANGELOG_ARCHIVE.md"
git -C "${fixture}" add CHANGELOG.md docs/CHANGELOG_ARCHIVE.md
git -C "${fixture}" commit -qm "chore: seed release notes"
git -C "${fixture}" tag v1.0.0
printf 'feature\n' >"${fixture}/feature.txt"
git -C "${fixture}" add feature.txt
git -C "${fixture}" commit -qm "feat(tui): add multiline composer (#123)"
if RELEASE_NOTE_REPO_ROOT="${fixture}" "${checker}" v1.0.0 HEAD >"${fixture}/missing.out" 2>&1; then
echo "missing feature receipt unexpectedly passed" >&2
exit 1
fi
grep -Fq 'references #123, but no release-note receipt exists' "${fixture}/missing.out"
printf '\n- Multiline composer mode (#123).\n' >>"${fixture}/CHANGELOG.md"
git -C "${fixture}" add CHANGELOG.md
git -C "${fixture}" commit -qm "docs: record multiline composer"
RELEASE_NOTE_REPO_ROOT="${fixture}" "${checker}" v1.0.0 HEAD >"${fixture}/present.out"
grep -Fq '1 linked issue reference(s) checked' "${fixture}/present.out"
printf 'color\n' >"${fixture}/color.txt"
git -C "${fixture}" add color.txt
git -C "${fixture}" commit -qm 'feat(tui): use brand cyan #48D7FF'
RELEASE_NOTE_REPO_ROOT="${fixture}" "${checker}" v1.0.0 HEAD >"${fixture}/color.out"
grep -Fq '1 linked issue reference(s) checked' "${fixture}/color.out"
printf 'reconciled\n' >"${fixture}/reconciled.txt"
git -C "${fixture}" add reconciled.txt
git -C "${fixture}" commit -qm 'feat(tui): reconcile earlier feature (#456)'
printf '\n- Earlier release already recorded this feature (#456).\n' >>"${fixture}/docs/CHANGELOG_ARCHIVE.md"
RELEASE_NOTE_REPO_ROOT="${fixture}" "${checker}" v1.0.0 HEAD >"${fixture}/archive.out"
grep -Fq '2 linked issue reference(s) checked' "${fixture}/archive.out"
echo "check-feature-release-notes tests passed"
+6 -2
View File
@@ -21,7 +21,8 @@
# 10. Public install and version snippets point at the current release.
# 11. `codewhale-app-server` stays library-only; the shipped app-server
# entrypoint belongs to `codewhale-cli`.
# 12. `Cargo.lock` is in sync with the manifests (`cargo metadata --locked`
# 12. Issue-linked feature commits have a durable changelog receipt.
# 13. `Cargo.lock` is in sync with the manifests (`cargo metadata --locked`
# fails if not).
set -euo pipefail
@@ -192,6 +193,9 @@ if [[ -n "${previous_tag}" ]]; then
git fetch --quiet --depth=1 origin "refs/tags/${previous_tag}:refs/tags/${previous_tag}" || true
fi
if git rev-parse -q --verify "refs/tags/${previous_tag}" >/dev/null; then
if ! ./scripts/release/check-feature-release-notes.sh "${previous_tag}" HEAD; then
fail=1
fi
while IFS= read -r line; do
[[ -z "${line}" ]] && continue
handle="$(sed -E 's#.*github.com/([^)/]+).*#\1#' <<<"${line}")"
@@ -296,7 +300,7 @@ if [[ -n "${app_server_bins}" ]]; then
fail=1
fi
# 12) Cargo.lock in sync.
# 13) Cargo.lock in sync.
if ! cargo metadata --locked --format-version 1 --no-deps >/dev/null 2>&1; then
echo "::error::Cargo.lock is out of sync with the manifests. Run 'cargo update -p codewhale-tui' or 'cargo build' and commit the result." >&2
fail=1