mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-10 01:51:32 -10:00
feat(tui,runtime): one permission dial on Shift+Tab; Accept Edits by default (CW-7890, CW-7849)
Founder D1/D2/D4 (modes-postures FOUNDER-DECISIONS, 2026-11-07): Shift+Tab
steps one dial, Ask -> Accept Edits -> Plan -> Auto-Review -> Full Access
-> Ask. Plan is AppMode::Plan with the posture kept dormant; every other
stop is Work with that posture. App::permission_dial_next/step_permission_dial
compute the stop from (mode, posture) and reuse select_mode and the existing
posture persist paths (select_approval_posture / select_root_approval_posture,
formerly cycle_*). When config or managed policy owns the posture, the dial
still alternates between that posture and Plan instead of going dead.
Tab only completes (slash menu, mention menu, commands, file mentions,
prompt suggestion): ShellBindingId::ModeCycle, is_mode_cycle_shortcut,
App::cycle_mode, TabDispatch::ModeCycled, the MODE_CYCLE footer hint and
AppMode::next/previous are gone; AppMode::CYCLE is renamed ALL (tests only
enumerate it). The footer carries one dial chip ("plan" in Plan, else the
posture word) with "Shift+Tab to change"; the separate mode chip and its
Tab key left TidelineFooter and the config preview.
Mid-turn: select_mode never refused while busy, and the engine already
applies a queued ChangeMode at the next step from the live authority
snapshot, so crossing Plan mid-turn needs no new path. Prompt and tool
catalog stay mode-agnostic; no KV-cache effect.
D2 default Accept Edits: unset TUI posture (init, root-policy release),
the settings schema default, the config preview, the "Normal agent" setup
preset, and new Runtime requests with no permission_posture
(RuntimePolicyProjection::from_request). ApprovalMode's #[default] stays
Suggest on purpose: engine/automation code uses it as the conservative
fallback, and a persisted thread with no posture still fails closed to Ask.
A saved permission_posture = "ask" migrates once to accept-edits at settings
load; ask_to_accept_edits_migrated records it so a later explicit Ask is
kept (one new test pins that data-migration contract). ACP still folds
Accept Edits to Ask.
D3 Operate (finishing c50612d6ab): CommandMode::Operate is removed and
operate spellings parse as Work; /mode operate says Operate was retired.
A Runtime request with mode "operate" runs as agent + default posture and
logs one notice per process (never an error). Forked child context drops
legacy Operate contract messages, so a child of a restored pre-0.10.3
Operate conversation never inherits it.
Full Access copy (D4): ConfigChoiceDetailFullAccess now reads "Tools run
without asking first, and commands can reach anything on this computer."
in all 15 packs (14 from codewhale-app fdb7898d, zh-Hans new).
KbCyclePermissions / HomeAgentModeYoloTip name Plan on the dial;
KbCompleteCycleModes is renamed KbTabComplete.
Wire values unchanged: permission_posture ask / accept_edits / auto_review /
full_access, mode plan / agent (checked against
codewhale-ops/design/ADE-PLAN-20261109.md section 1.3 and section 5).
Checks: see the follow-up docs commit and the hand-off report for counts.
Co-Authored-By: Claude Fable 5.2 <[email protected]>
This commit is contained in:
1 parent
7d6779d346
commit
29665bfa61
51 files changed
+431
-600
No files matched your search
+26
-3
@@ -334,7 +334,7 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
- **The transcript shows the work, not the decoration.** The idle footer no longer carries a `Work bar` label (its key still opens the dock); fish, bubbles and the whale cameo swim only on the empty start screen and never behind transcript text; a streaming reasoning block is one `Thinking…` line instead of `Thinking…` above `reasoning...`, and the hidden-reasoning line and window title say thinking too; the tool-card hint reads `⌥V details` in dim text instead of `⌥V:details`.
|
||||
- **Sub-agents and goals by default.** The constitution now tells the agent to start independent parts of a request as parallel sub-agents in one message, keep working while their results arrive on their own, and end the turn when only waiting remains; to give each child a complete prompt and its own files; to inspect a write-capable child's evidence, review the diff and run the checks itself; to have an independent reviewer try to refute a non-trivial change before calling it done; and to start a goal for any request, in any language, whose verifiable end state needs more than one turn. Sub-agents, workflows and goals within the current request are in scope; spending real money (purchases, paid providers, billing) still needs the user's express authorization. Cost alone is not a reason to stop; only limits the user or configuration sets apply. **This changes the session-pinned prompt and tool descriptions, so each session re-pins its prompt cache once after upgrade.**
|
||||
- **Operate's contract is retired.** The Engine no longer appends the Operate contract to a session; its review, cost and evidence rules now live in the constitution for every session. Saved sessions that contain it still replay.
|
||||
- **The Operate mode is gone; Work and Plan are the two modes.** Tab cycles between them, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
|
||||
- **The Operate mode is gone; Work and Plan are the two modes.** The Shift+Tab permission dial reaches both, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
|
||||
- **Clearer tool text.** The `agent` description leads with when to delegate, gives one trigger per role and an example child brief, and drops Operate and legacy wording; the start receipt says results arrive automatically. `create_goal` and `update_goal` say when to start a goal and what complete, blocked and yield mean (blocked means the same obstacle persists; difficulty is not blocked). `tool_search` lists the deferred tool groups. The eager tool schemas shrink by 681 bytes.
|
||||
- **Workflow fan-out default 16 → 64.** `[workflow] max_concurrent` now defaults to 64 live agents per run, matching sub-agents; the per-run ceiling rises from 16 to 128. No spend caps were added.
|
||||
- **Bundled skill `goals` is now `personal-goals`.** It tracks personal resolutions in `goals.md` and no longer collides with goal mode; `goals` still resolves to it. An untouched installed `goals` copy becomes `personal-goals` on upgrade; an edited one stays as `goals`.
|
||||
@@ -347,12 +347,35 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
an edit whose every target stays inside the workspace git work tree runs
|
||||
without a card, while `.git`, `.codewhale`, `.env*`, credential and key
|
||||
files, folders without git, commands, network and connected apps still ask.
|
||||
Shift+Tab cycles Ask → Accept Edits → Auto-Review → Full Access, so one
|
||||
press restores the previous Ask behaviour. `/permissions`, `/status`,
|
||||
Accept Edits is the default (see the permission dial entry below).
|
||||
`/permissions`, `/status`,
|
||||
`/config` and the Plan handoff offer it; the Runtime wire spells it
|
||||
`permission_posture: "accept_edits"`, settings and `config.toml`
|
||||
`accept-edits`. Project config may tighten Accept Edits to Ask but never
|
||||
loosen Ask to it. ACP sessions run it as Ask.
|
||||
- **One permission dial; Tab no longer changes the mode (CW-7890):** Shift+Tab
|
||||
steps Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask. Plan is the
|
||||
Plan mode; every other stop is Work with that posture. Tab only completes
|
||||
commands, file mentions and suggestions. The footer shows one chip for the
|
||||
current stop with `Shift+Tab to change`; the separate mode chip and its Tab
|
||||
hint are gone. The dial moves mid-turn too, into or out of Plan, and applies
|
||||
at the agent's next step without restarting a response or changing the pinned
|
||||
prompt. When config or managed policy owns the posture, the dial still
|
||||
alternates between that posture and Plan.
|
||||
- **Accept Edits is the default posture:** new installs, unset settings and new
|
||||
Runtime threads that send no `permission_posture` start on Accept Edits. A
|
||||
saved `permission_posture = "ask"` from an earlier build moves to Accept Edits
|
||||
once (recorded as `ask_to_accept_edits_migrated`), so an Ask you pick
|
||||
afterwards is kept. The "Normal agent" setup preset now writes Accept Edits.
|
||||
ACP still runs Accept Edits as Ask.
|
||||
- **Full Access copy says what it does:** its settings description now reads
|
||||
"Tools run without asking first, and commands can reach anything on this
|
||||
computer." in every language, instead of implying a folder fence.
|
||||
- **Operate compatibility (CW-7849):** `/mode operate` selects Work and says
|
||||
Operate was retired; a Runtime API request with `mode: "operate"` runs as
|
||||
`agent` with the default posture and logs one notice per process, never an
|
||||
error. Agents forked from a conversation saved by an earlier Operate build no
|
||||
longer inherit its contract message.
|
||||
- **Brighter terminal working states:** the Ocean palette has stronger accents,
|
||||
and active work uses a restrained left-to-right text sweep. Quiet and
|
||||
reduced-motion settings keep stable working indicators. The shared
|
||||
|
||||
@@ -17,7 +17,6 @@ pub struct CommandProviderId(pub String);
|
||||
pub enum CommandMode {
|
||||
Agent,
|
||||
Plan,
|
||||
Operate,
|
||||
}
|
||||
|
||||
impl CommandMode {
|
||||
@@ -27,12 +26,20 @@ impl CommandMode {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"agent" | "act" | "work" | "auto" | "1" => Some(Self::Agent),
|
||||
"plan" | "2" => Some(Self::Plan),
|
||||
"operate" | "operation" | "ops" | "3" => Some(Self::Operate),
|
||||
other if Self::is_retired_operate_alias(other) => Some(Self::Agent),
|
||||
other if Self::is_legacy_bypass_alias(other) => Some(Self::Agent),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn is_retired_operate_alias(value: &str) -> bool {
|
||||
matches!(
|
||||
value.trim().to_ascii_lowercase().as_str(),
|
||||
"operate" | "operation" | "ops" | "3"
|
||||
)
|
||||
}
|
||||
|
||||
/// Legacy bypass spellings (`yolo`, `4`, `bypass`, …) that carry full access.
|
||||
#[must_use]
|
||||
pub fn is_legacy_bypass_alias(value: &str) -> bool {
|
||||
@@ -48,7 +55,6 @@ impl CommandMode {
|
||||
match self {
|
||||
Self::Agent => "Work",
|
||||
Self::Plan => "Plan",
|
||||
Self::Operate => "Operate",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ pub enum AppMode {
|
||||
}
|
||||
|
||||
impl AppMode {
|
||||
pub const CYCLE: [Self; 2] = [Self::Plan, Self::Agent];
|
||||
pub const ALL: [Self; 2] = [Self::Plan, Self::Agent];
|
||||
|
||||
#[must_use]
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
@@ -109,22 +109,6 @@ impl AppMode {
|
||||
AppMode::Plan => "Plan mode - research and design before implementing",
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn next(self) -> Self {
|
||||
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
|
||||
return Self::Agent;
|
||||
};
|
||||
Self::CYCLE[(index + 1) % Self::CYCLE.len()]
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn previous(self) -> Self {
|
||||
let Some(index) = Self::CYCLE.iter().position(|mode| *mode == self) else {
|
||||
return Self::Agent;
|
||||
};
|
||||
Self::CYCLE[(index + Self::CYCLE.len() - 1) % Self::CYCLE.len()]
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -132,7 +116,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn app_mode_helpers_centralize_parse_labels_and_cycle_order() {
|
||||
fn app_mode_helpers_centralize_parse_and_labels() {
|
||||
assert_eq!(AppMode::parse("agent"), Some(AppMode::Agent));
|
||||
assert_eq!(AppMode::parse("act"), Some(AppMode::Agent));
|
||||
assert_eq!(AppMode::parse("work"), Some(AppMode::Agent));
|
||||
@@ -169,12 +153,7 @@ mod tests {
|
||||
assert_eq!(AppMode::Agent.as_setting(), "agent");
|
||||
assert_eq!(AppMode::Plan.display_name(), "Plan");
|
||||
assert_eq!(AppMode::Agent.number(), '1');
|
||||
assert_eq!(AppMode::CYCLE, [AppMode::Plan, AppMode::Agent]);
|
||||
|
||||
assert_eq!(AppMode::Plan.next(), AppMode::Agent);
|
||||
assert_eq!(AppMode::Agent.next(), AppMode::Plan);
|
||||
assert_eq!(AppMode::Plan.previous(), AppMode::Agent);
|
||||
assert_eq!(AppMode::Agent.previous(), AppMode::Plan);
|
||||
assert_eq!(AppMode::ALL, [AppMode::Plan, AppMode::Agent]);
|
||||
}
|
||||
|
||||
/// The durable form of a mode is the `as_setting()` string persisted into
|
||||
@@ -182,7 +161,7 @@ mod tests {
|
||||
/// round-trip that has to hold is string -> mode -> string.
|
||||
#[test]
|
||||
fn setting_strings_round_trip_for_every_mode() {
|
||||
for mode in AppMode::CYCLE {
|
||||
for mode in AppMode::ALL {
|
||||
let setting = mode.as_setting();
|
||||
assert_eq!(
|
||||
AppMode::from_setting(setting),
|
||||
@@ -214,7 +193,7 @@ mod tests {
|
||||
assert_eq!(AppMode::Agent.display_name(), "Work");
|
||||
assert_eq!(AppMode::Plan.display_name(), "Plan");
|
||||
|
||||
for mode in AppMode::CYCLE {
|
||||
for mode in AppMode::ALL {
|
||||
assert!(!mode.description().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1247,7 +1247,7 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
def(
|
||||
"permission_posture",
|
||||
SettingKind::Enum(PERMISSION_POSTURE),
|
||||
"ask",
|
||||
"accept-edits",
|
||||
ui(
|
||||
TAB_TRUST,
|
||||
"permissions",
|
||||
@@ -1552,6 +1552,12 @@ pub const SETTINGS_SCHEMA: &[SettingDef] = &[
|
||||
"false",
|
||||
None,
|
||||
),
|
||||
def(
|
||||
"ask_to_accept_edits_migrated",
|
||||
SettingKind::Bool(ON_OFF),
|
||||
"false",
|
||||
None,
|
||||
),
|
||||
def("behavioral_tip_impressions", SettingKind::String, "", None),
|
||||
// Footer key-hint use counts: a hint retires to its bare state once its
|
||||
// binding has been used enough times. Written by the footer, never edited.
|
||||
|
||||
@@ -6347,8 +6347,11 @@ impl Engine {
|
||||
&self.session.id,
|
||||
)
|
||||
.await;
|
||||
let mut messages = self.messages_with_turn_metadata();
|
||||
messages
|
||||
.retain(|message| !crate::runtime_handoff::is_operate_contract_message(message));
|
||||
Some(SubAgentForkContext {
|
||||
messages: self.messages_with_turn_metadata(),
|
||||
messages,
|
||||
structured_state_block: state.to_system_block(),
|
||||
// Resolve at spawn time so a todo_write earlier in this turn
|
||||
// reaches the child rather than freezing turn-start state.
|
||||
|
||||
@@ -324,7 +324,7 @@ pub const KEYBINDINGS: &[KeybindingEntry] = &[
|
||||
// --- Modes ---
|
||||
KeybindingEntry {
|
||||
chord: "Tab",
|
||||
description_id: codewhale_localization::MessageId::KbCompleteCycleModes,
|
||||
description_id: codewhale_localization::MessageId::KbTabComplete,
|
||||
section: KeybindingSection::Modes,
|
||||
},
|
||||
KeybindingEntry {
|
||||
@@ -516,7 +516,7 @@ mod tests {
|
||||
|
||||
let tab_copy = codewhale_localization::tr(
|
||||
codewhale_localization::Locale::En,
|
||||
codewhale_localization::MessageId::KbCompleteCycleModes,
|
||||
codewhale_localization::MessageId::KbTabComplete,
|
||||
);
|
||||
assert!(!tab_copy.to_ascii_lowercase().contains("queue"));
|
||||
let stash_copy = codewhale_localization::tr(
|
||||
|
||||
@@ -20143,8 +20143,8 @@ fn mode_only_override_preserves_legacy_full_access_posture() -> Result<()> {
|
||||
assert_eq!(policy.mode, AppMode::Plan);
|
||||
assert_eq!(policy.permission_wire(), "full_access");
|
||||
|
||||
let ask = runtime_policy_with_overrides(&thread, Some("act"), None, Some(false))?;
|
||||
assert_eq!(ask.permission_wire(), "ask");
|
||||
let default = runtime_policy_with_overrides(&thread, Some("act"), None, Some(false))?;
|
||||
assert_eq!(default.permission_wire(), "accept_edits");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Obre l'esborrany del redactor en un editor extern",
|
||||
"KbLiveTranscript": "Obre la superposició de transcripció en viu (Ctrl+Shift+T; desplaçament automàtic fixat al final)",
|
||||
"KbBacktrackMessage": "Torna enrere fins a un missatge anterior de l'usuari (Esquerra/Dreta per anar pas a pas, Enter per rebobinar)",
|
||||
"KbCompleteCycleModes": "Completa /command o canvia de mode quan el redactor és buit (Plan → Work)",
|
||||
"KbTabComplete": "Completa /command, menció @fitxer o suggeriment",
|
||||
"KbCycleThinking": "Canvia l'esforç de raonament del proveïdor actiu (Ctrl+T)",
|
||||
"KbCyclePermissions": "Canvia l'accés: Pregunta → Accepta edicions → Revisió automàtica → Accés complet (Maj+Tab)",
|
||||
"KbCyclePermissions": "Canvia l'accés: Pregunta → Accepta edicions → Plan → Revisió automàtica → Accés complet (Maj+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Activa les ranures de la Hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Salta a Plan / Work o demana Full Access (àlies heretat)",
|
||||
"KbFocusSidebar": "Enfoca el panell de la barra de treball Tasks / Agents / Context / Pinned",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Consells de mode",
|
||||
"HomeAgentModeTip": "Work — treball directe a la sessió actual amb eines",
|
||||
"HomeAgentModeReviewTip": " /mode plan per investigar i presentar un pla primer",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab canvia el permís: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab canvia el permís: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — les eines s'executen sense demanar aprovació",
|
||||
"HomeYoloModeCaution": " Les operacions destructives poden executar-se a l'instant; prefereix Ask si tens dubtes",
|
||||
"HomePlanModeTip": "Plan — investiga i dissenya abans d'implementar",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Les edicions dins d'aquest repositori git s'executen sense preguntar; les ordres continuen preguntant.",
|
||||
"ConfigChoiceDetailAutoReview": "Revisa el risc de les eines automàticament i pregunta quan una decisió et necessita.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Treu la sobreescriptura del config arrel i usa la tria de permís desada a la TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Executa eines sense demanar aprovació; les regles de l'espai de treball continuen aplicant-se.",
|
||||
"ConfigChoiceDetailFullAccess": "Les eines s’executen sense preguntar, i les ordres poden accedir a tot el que hi ha en aquest ordinador.",
|
||||
"ConfigChoiceDetailNever": "Bloqueja tota eina que requereixi aprovació.",
|
||||
"ConfigChoiceDetailModeAgent": "Comença a punt per col·laborar i usar eines.",
|
||||
"ConfigChoiceDetailModePlan": "Comença en un espai de planificació de només lectura.",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Composer-Entwurf in externem Editor öffnen",
|
||||
"KbLiveTranscript": "Live-Transkript-Overlay öffnen (Ctrl+Shift+T; Sticky-Tail-Auto-Scroll)",
|
||||
"KbBacktrackMessage": "Zu einer früheren Benutzernachricht zurückspringen (Links/Rechts schrittweise, Enter zum Zurückspulen)",
|
||||
"KbCompleteCycleModes": "/Befehl vervollständigen oder Modi wechseln, wenn der Composer leer ist (Plan → Work)",
|
||||
"KbTabComplete": "/Befehl, @Datei-Erwähnung oder Vorschlag vervollständigen",
|
||||
"KbCycleThinking": "Reasoning-Stufe des aktiven Providers wechseln (Ctrl+T)",
|
||||
"KbCyclePermissions": "Zugriff wechseln: Nachfragen → Änderungen annehmen → Auto-Review → Voller Zugriff (Umschalt+Tab)",
|
||||
"KbCyclePermissions": "Zugriff wechseln: Nachfragen → Änderungen annehmen → Plan → Auto-Review → Voller Zugriff (Umschalt+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Hotbar-Slots auslösen",
|
||||
"KbAltJumpPlanAgentYolo": "Zu Plan / Work springen oder Full Access anfordern (Legacy-Alias)",
|
||||
"KbFocusSidebar": "Leisten-Panel Tasks / Agents / Context / Pinned fokussieren",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Modus-Tipps",
|
||||
"HomeAgentModeTip": "Work — direkte Arbeit in der aktuellen Sitzung mit Tools",
|
||||
"HomeAgentModeReviewTip": " /mode plan, um zuerst zu recherchieren und einen Plan vorzulegen",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab wechselt Berechtigung: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab wechselt Berechtigung: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — Tools laufen ohne Freigabeaufforderungen",
|
||||
"HomeYoloModeCaution": " Destruktive Operationen können sofort laufen; im Zweifel Ask bevorzugen",
|
||||
"HomePlanModeTip": "Plan — recherchieren und entwerfen vor dem Implementieren",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Änderungen in diesem git-Repository laufen ohne Nachfrage; Befehle fragen weiterhin nach.",
|
||||
"ConfigChoiceDetailAutoReview": "Prüft das Werkzeugrisiko automatisch und fragt, wenn eine Entscheidung dich braucht.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Entfernt den Override der Root-Konfiguration und nutzt die gespeicherte TUI-Berechtigungswahl.",
|
||||
"ConfigChoiceDetailFullAccess": "Führt Werkzeuge ohne Freigabeabfragen aus; Arbeitsbereichsregeln gelten weiterhin.",
|
||||
"ConfigChoiceDetailFullAccess": "Werkzeuge laufen ohne Rückfrage, und Befehle können auf alles auf diesem Computer zugreifen.",
|
||||
"ConfigChoiceDetailNever": "Blockiert jedes Werkzeug, das eine Freigabe braucht.",
|
||||
"ConfigChoiceDetailModeAgent": "Startet bereit zur Zusammenarbeit und Werkzeugnutzung.",
|
||||
"ConfigChoiceDetailModePlan": "Startet in einem schreibgeschützten Planungsbereich.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "Edit the draft externally",
|
||||
"KbLiveTranscript": "Open the live transcript overlay",
|
||||
"KbBacktrackMessage": "Rewind to an earlier message (Left/Right steps, Enter rewinds)",
|
||||
"KbCompleteCycleModes": "Complete commands, or cycle modes on empty input (Plan → Work)",
|
||||
"KbTabComplete": "Complete a /command, @file mention or suggestion",
|
||||
"KbCycleThinking": "Cycle reasoning effort (Ctrl+T)",
|
||||
"KbCyclePermissions": "Cycle Access: Ask → Accept Edits → Auto-Review → Full Access (Shift+Tab)",
|
||||
"KbCyclePermissions": "Cycle Access: Ask → Accept Edits → Plan → Auto-Review → Full Access (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Trigger hotbar slots",
|
||||
"KbAltJumpPlanAgentYolo": "Jump to Plan/Work, or request Full Access (legacy alias)",
|
||||
"KbFocusSidebar": "Focus the Tasks / Agents / Context / Pinned workbar panel",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "Mode Tips",
|
||||
"HomeAgentModeTip": "Work — makes the changes you ask for, with tools",
|
||||
"HomeAgentModeReviewTip": " /mode plan to research and present a plan first",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cycles permission: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cycles permission: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — tools run without asking first",
|
||||
"HomeYoloModeCaution": " Destructive operations can run immediately; prefer Ask when unsure",
|
||||
"HomePlanModeTip": "Plan — research and design first",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Edits inside this git repository run without asking; commands still ask.",
|
||||
"ConfigChoiceDetailAutoReview": "Review tool risk automatically and ask when a decision needs you.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Drop the raw config override; use the saved choice.",
|
||||
"ConfigChoiceDetailFullAccess": "Run tools without approval prompts; workspace rules still apply.",
|
||||
"ConfigChoiceDetailFullAccess": "Tools run without asking first, and commands can reach anything on this computer.",
|
||||
"ConfigChoiceDetailNever": "Block every tool that requires approval.",
|
||||
"ConfigChoiceDetailModeAgent": "Start ready to work with tools.",
|
||||
"ConfigChoiceDetailModePlan": "Start in a read-only planning workspace.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "Abrir borrador en el editor externo",
|
||||
"KbLiveTranscript": "Abrir superposición de transcripción en vivo (auto-scroll)",
|
||||
"KbBacktrackMessage": "Retroceder al mensaje anterior del usuario (izquierda/derecha, Enter para rebobinar)",
|
||||
"KbCompleteCycleModes": "Completar /command o ciclar modos cuando el compositor está vacío (Plan → Work)",
|
||||
"KbTabComplete": "Completar /command, mención @archivo o sugerencia",
|
||||
"KbCycleThinking": "Alternar el esfuerzo de razonamiento del proveedor activo (Ctrl+T)",
|
||||
"KbCyclePermissions": "Cambiar acceso: Preguntar → Aceptar ediciones → Revisión automática → Acceso total (Shift+Tab)",
|
||||
"KbCyclePermissions": "Cambiar acceso: Preguntar → Aceptar ediciones → Plan → Revisión automática → Acceso total (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Activar ranuras de la hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Saltar a Plan / Work o solicitar acceso total (alias heredado)",
|
||||
"KbFocusSidebar": "Enfocar el panel de la barra de trabajo Tasks / Agents / Context / Pinned",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "Tips de Modo",
|
||||
"HomeAgentModeTip": "Work — trabajo directo en la sesión actual con herramientas",
|
||||
"HomeAgentModeReviewTip": " /mode plan para investigar y presentar un plan primero",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cicla permiso: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cicla permiso: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + acceso total — herramientas sin pedir aprobación",
|
||||
"HomeYoloModeCaution": " Las operaciones destructivas pueden correr al instante; usa Ask si dudas",
|
||||
"HomePlanModeTip": "Plan — investiga y diseña antes de implementar",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Las ediciones dentro de este repositorio git se ejecutan sin preguntar; los comandos siguen preguntando.",
|
||||
"ConfigChoiceDetailAutoReview": "Revisa el riesgo de las herramientas automáticamente y pregunta cuando una decisión te necesita.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Quita la anulación del config raíz y usa la elección de permiso guardada en la TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Ejecuta herramientas sin pedir aprobación; las reglas del workspace siguen aplicando.",
|
||||
"ConfigChoiceDetailFullAccess": "Las herramientas se ejecutan sin preguntar y los comandos pueden acceder a todo lo que hay en esta computadora.",
|
||||
"ConfigChoiceDetailNever": "Bloquea toda herramienta que requiera aprobación.",
|
||||
"ConfigChoiceDetailModeAgent": "Empieza listo para colaborar y usar herramientas.",
|
||||
"ConfigChoiceDetailModePlan": "Empieza en un espacio de planificación de solo lectura.",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Ouvrir le brouillon du composer dans un éditeur externe",
|
||||
"KbLiveTranscript": "Ouvrir la transcription en direct (Ctrl+Shift+T ; défilement automatique)",
|
||||
"KbBacktrackMessage": "Revenir à un message utilisateur précédent (Gauche/Droite pour naviguer, Enter pour rembobiner)",
|
||||
"KbCompleteCycleModes": "Compléter une /commande ou cycler les modes quand le composer est vide (Plan → Work)",
|
||||
"KbTabComplete": "Compléter une /commande, une mention @fichier ou une suggestion",
|
||||
"KbCycleThinking": "Cycler l'effort de raisonnement du fournisseur actif (Ctrl+T)",
|
||||
"KbCyclePermissions": "Changer l'accès : Demander → Accepter les modifications → Revue auto → Accès complet (Maj+Tab)",
|
||||
"KbCyclePermissions": "Changer l'accès : Demander → Accepter les modifications → Plan → Revue auto → Accès complet (Maj+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Déclencher les slots de la hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Aller à Plan / Work ou demander Full Access (alias hérité)",
|
||||
"KbFocusSidebar": "Cibler le panneau de la barre de travail Tasks / Agents / Context / Pinned",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Conseils de mode",
|
||||
"HomeAgentModeTip": "Work — travail direct dans la session en cours avec les outils",
|
||||
"HomeAgentModeReviewTip": " /mode plan pour d'abord rechercher et présenter un plan",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cycle la permission : Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cycle la permission : Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — les outils s'exécutent sans demande d'approbation",
|
||||
"HomeYoloModeCaution": " Les opérations destructives peuvent s'exécuter immédiatement ; préférez Ask en cas de doute",
|
||||
"HomePlanModeTip": "Plan — recherche et conception avant l'implémentation",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Les modifications dans ce dépôt git s'exécutent sans demander ; les commandes demandent toujours.",
|
||||
"ConfigChoiceDetailAutoReview": "Évalue automatiquement le risque des outils et demande quand une décision vous revient.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Retire la surcharge de la config racine et utilise le choix de permission enregistré dans la TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Exécute les outils sans demande d'approbation ; les règles de l'espace de travail s'appliquent toujours.",
|
||||
"ConfigChoiceDetailFullAccess": "Les outils s’exécutent sans demander, et les commandes peuvent accéder à tout ce qui se trouve sur cet ordinateur.",
|
||||
"ConfigChoiceDetailNever": "Bloque tout outil qui requiert une approbation.",
|
||||
"ConfigChoiceDetailModeAgent": "Démarre prêt à collaborer et à utiliser des outils.",
|
||||
"ConfigChoiceDetailModePlan": "Démarre dans un espace de planification en lecture seule.",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "कम्पोज़र मसौदा बाहरी संपादक में खोलें",
|
||||
"KbLiveTranscript": "लाइव ट्रांसक्रिप्ट ओवरले खोलें (Ctrl+Shift+T; स्टिकी-टेल ऑटो-स्क्रॉल)",
|
||||
"KbBacktrackMessage": "पिछले उपयोगकर्ता संदेश पर वापस जाएँ (Left/Right कदम, रिवाइंड के लिए Enter)",
|
||||
"KbCompleteCycleModes": "कम्पोज़र खाली होने पर /command पूरा करें या मोड बदलें (Plan → Work)",
|
||||
"KbTabComplete": "/command, @file उल्लेख या सुझाव पूरा करें",
|
||||
"KbCycleThinking": "सक्रिय प्रोवाइडर के लिए रीज़निंग स्तर बदलें (Ctrl+T)",
|
||||
"KbCyclePermissions": "एक्सेस बदलें: पूछें → बदलाव स्वीकारें → स्वतः समीक्षा → पूर्ण एक्सेस (Shift+Tab)",
|
||||
"KbCyclePermissions": "एक्सेस बदलें: पूछें → बदलाव स्वीकारें → Plan → स्वतः समीक्षा → पूर्ण एक्सेस (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Hotbar स्लॉट ट्रिगर करें",
|
||||
"KbAltJumpPlanAgentYolo": "Plan / Work पर जाएँ या Full Access का अनुरोध करें (लेगेसी उपनाम)",
|
||||
"KbFocusSidebar": "Tasks / Agents / Context / Pinned वर्क बार पैनल फ़ोकस करें",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "मोड सुझाव",
|
||||
"HomeAgentModeTip": "Work — वर्तमान सत्र में टूल के साथ सीधा काम",
|
||||
"HomeAgentModeReviewTip": " /mode plan से पहले शोध कर योजना प्रस्तुत करें",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab अनुमति बदलता है: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab अनुमति बदलता है: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — टूल बिना अनुमति प्रॉम्प्ट के चलते हैं",
|
||||
"HomeYoloModeCaution": " विनाशकारी कार्रवाइयाँ तुरंत चल सकती हैं; संदेह हो तो Ask चुनें",
|
||||
"HomePlanModeTip": "Plan — कार्यान्वयन से पहले शोध और डिज़ाइन",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "इस git रिपॉज़िटरी के भीतर के बदलाव बिना पूछे चलते हैं; कमांड अब भी पूछते हैं।",
|
||||
"ConfigChoiceDetailAutoReview": "टूल के जोखिम की स्वतः समीक्षा करता है और निर्णय आपकी ज़रूरत होने पर पूछता है।",
|
||||
"ConfigChoiceDetailUseTuiDefault": "रूट config का अधिभाव हटाकर TUI में सहेजी गई अनुमति का विकल्प उपयोग करता है।",
|
||||
"ConfigChoiceDetailFullAccess": "अनुमोदन के संकेत के बिना टूल चलाता है; कार्यक्षेत्र के नियम फिर भी लागू रहते हैं।",
|
||||
"ConfigChoiceDetailFullAccess": "टूल बिना पूछे चलेंगे, और कमांड इस कंप्यूटर पर किसी भी चीज़ तक पहुँच सकती हैं।",
|
||||
"ConfigChoiceDetailNever": "अनुमोदन चाहने वाले हर टूल को रोकता है।",
|
||||
"ConfigChoiceDetailModeAgent": "सहयोग और टूल उपयोग के लिए तैयार अवस्था में शुरू होता है।",
|
||||
"ConfigChoiceDetailModePlan": "केवल पढ़ने योग्य योजना क्षेत्र में शुरू होता है।",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Buka draf komposer di editor eksternal",
|
||||
"KbLiveTranscript": "Buka overlay transkrip langsung (Ctrl+Shift+T; auto-scroll mengikuti ekor)",
|
||||
"KbBacktrackMessage": "Mundur ke pesan pengguna sebelumnya (Left/Right berpindah, Enter untuk memutar balik)",
|
||||
"KbCompleteCycleModes": "Lengkapi /command atau ganti mode saat komposer kosong (Plan → Work)",
|
||||
"KbTabComplete": "Lengkapi /command, sebutan @file, atau saran",
|
||||
"KbCycleThinking": "Ganti upaya penalaran untuk penyedia aktif (Ctrl+T)",
|
||||
"KbCyclePermissions": "Ubah akses: Tanya → Terima suntingan → Tinjauan otomatis → Akses penuh (Shift+Tab)",
|
||||
"KbCyclePermissions": "Ubah akses: Tanya → Terima suntingan → Plan → Tinjauan otomatis → Akses penuh (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Picu slot hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Lompat ke Plan / Work atau minta akses penuh (alias lama)",
|
||||
"KbFocusSidebar": "Fokuskan panel bilah kerja Tasks / Agents / Context / Pinned",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Kiat Mode",
|
||||
"HomeAgentModeTip": "Work — kerja langsung di sesi saat ini dengan alat",
|
||||
"HomeAgentModeReviewTip": " /mode plan untuk meneliti dan menyajikan rencana dulu",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab mengganti izin: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab mengganti izin: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — alat berjalan tanpa prompt persetujuan",
|
||||
"HomeYoloModeCaution": " Operasi destruktif dapat langsung berjalan; pilih Ask bila ragu",
|
||||
"HomePlanModeTip": "Plan — teliti dan rancang sebelum mengimplementasikan",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Suntingan di dalam repositori git ini berjalan tanpa bertanya; perintah tetap bertanya.",
|
||||
"ConfigChoiceDetailAutoReview": "Meninjau risiko alat secara otomatis dan bertanya saat keputusan membutuhkan Anda.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Menghapus penimpaan config akar dan memakai pilihan izin TUI yang tersimpan.",
|
||||
"ConfigChoiceDetailFullAccess": "Menjalankan alat tanpa permintaan persetujuan; aturan ruang kerja tetap berlaku.",
|
||||
"ConfigChoiceDetailFullAccess": "Alat berjalan tanpa bertanya dulu, dan perintah bisa menjangkau apa pun di komputer ini.",
|
||||
"ConfigChoiceDetailNever": "Memblokir setiap alat yang memerlukan persetujuan.",
|
||||
"ConfigChoiceDetailModeAgent": "Mulai dalam keadaan siap berkolaborasi dan memakai alat.",
|
||||
"ConfigChoiceDetailModePlan": "Mulai di ruang perencanaan hanya-baca.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "作成中の下書きを外部エディターで開く",
|
||||
"KbLiveTranscript": "ライブ会話履歴オーバーレイを開く(自動追尾スクロール)",
|
||||
"KbBacktrackMessage": "前のユーザーメッセージに戻る(左右でステップ、Enter で巻き戻し)",
|
||||
"KbCompleteCycleModes": "/command を補完、または入力欄が空のときモードを切り替え(Plan → Work)",
|
||||
"KbTabComplete": "/command・@ファイル参照・候補を補完",
|
||||
"KbCycleThinking": "現在のプロバイダーの推論強度を順に切り替え (Ctrl+T)",
|
||||
"KbCyclePermissions": "アクセスを切り替え:確認 → 編集を承認 → 自動レビュー → フルアクセス(Shift+Tab)",
|
||||
"KbCyclePermissions": "アクセスを切り替え:確認 → 編集を承認 → Plan → 自動レビュー → フルアクセス(Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "ホットバースロットを起動",
|
||||
"KbAltJumpPlanAgentYolo": "Plan / Work へ移動、またはフルアクセスを要求(旧エイリアス)",
|
||||
"KbFocusSidebar": "Tasks / Agents / Context / Pinned ワークバーパネルにフォーカス",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "モードヒント",
|
||||
"HomeAgentModeTip": "Work — 現在のセッションでツールを使って直接作業",
|
||||
"HomeAgentModeReviewTip": " 先に計画するなら /mode plan",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab で権限: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab で権限: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + フルアクセス — 承認なしでツール実行",
|
||||
"HomeYoloModeCaution": " 破壊的操作が即実行されることがあります。迷ったら Ask を",
|
||||
"HomePlanModeTip": "Plan — 実装前に調査と設計",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "この git リポジトリ内の編集は確認なしで実行します。コマンドは引き続き確認します。",
|
||||
"ConfigChoiceDetailAutoReview": "ツールのリスクを自動で確認し、判断が必要なときだけ尋ねます。",
|
||||
"ConfigChoiceDetailUseTuiDefault": "ルート設定の上書きを外し、保存済みの TUI 権限設定を使います。",
|
||||
"ConfigChoiceDetailFullAccess": "承認プロンプトなしでツールを実行します。ワークスペースのルールは引き続き適用されます。",
|
||||
"ConfigChoiceDetailFullAccess": "ツールは確認なしで実行され、コマンドはこのコンピューター上のあらゆるものにアクセスできます。",
|
||||
"ConfigChoiceDetailNever": "承認が必要なツールをすべてブロックします。",
|
||||
"ConfigChoiceDetailModeAgent": "ツールを使って協働できる状態で開始します。",
|
||||
"ConfigChoiceDetailModePlan": "読み取り専用の計画ワークスペースで開始します。",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "작성 중인 초안을 외부 편집기에서 열기",
|
||||
"KbLiveTranscript": "실시간 대화 기록 오버레이 열기 (Ctrl+Shift+T; 하단 고정 자동 스크롤)",
|
||||
"KbBacktrackMessage": "이전 사용자 메시지로 되돌아가기 (Left/Right로 이동, Enter로 되감기)",
|
||||
"KbCompleteCycleModes": "/명령 완성 또는 입력창이 비어 있을 때 모드 순환 (계획 → 실행)",
|
||||
"KbTabComplete": "/명령, @파일 멘션 또는 제안 완성",
|
||||
"KbCycleThinking": "현재 프로바이더의 추론 강도 순환 전환 (Ctrl+T)",
|
||||
"KbCyclePermissions": "액세스 전환: 확인 → 편집 수락 → 자동 검토 → 전체 액세스 (Shift+Tab)",
|
||||
"KbCyclePermissions": "액세스 전환: 확인 → 편집 수락 → 계획 → 자동 검토 → 전체 액세스 (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "핫바 슬롯 실행",
|
||||
"KbAltJumpPlanAgentYolo": "계획 / 실행 모드로 이동하거나 전체 권한 요청 (레거시 별칭)",
|
||||
"KbFocusSidebar": "Tasks / Agents / Context / Pinned 작업 바 패널에 포커스",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "모드 팁",
|
||||
"HomeAgentModeTip": "실행 — 도구를 사용해 현재 세션에서 바로 작업",
|
||||
"HomeAgentModeReviewTip": " /mode plan으로 먼저 조사하고 계획을 제시",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab으로 권한 순환: 확인 → 편집 수락 → 자동 검토 → 전체 권한",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab으로 권한 순환: 확인 → 편집 수락 → 계획 → 자동 검토 → 전체 권한",
|
||||
"HomeYoloModeTip": "실행 + 전체 권한 — 승인 없이 도구 실행",
|
||||
"HomeYoloModeCaution": " 파괴적인 작업이 즉시 실행될 수 있습니다. 확실하지 않으면 확인 모드를 사용하세요",
|
||||
"HomePlanModeTip": "계획 — 구현 전에 조사하고 설계",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "이 git 저장소 안의 편집은 묻지 않고 실행되며, 명령은 계속 확인합니다.",
|
||||
"ConfigChoiceDetailAutoReview": "도구 위험을 자동으로 검토하고 결정이 필요할 때만 묻습니다.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "루트 설정 재정의를 제거하고 저장된 TUI 권한 선택을 사용합니다.",
|
||||
"ConfigChoiceDetailFullAccess": "승인 프롬프트 없이 도구를 실행합니다. 작업 공간 규칙은 계속 적용됩니다.",
|
||||
"ConfigChoiceDetailFullAccess": "도구가 묻지 않고 실행되고, 명령은 이 컴퓨터의 모든 것에 접근할 수 있어요.",
|
||||
"ConfigChoiceDetailNever": "승인이 필요한 모든 도구를 차단합니다.",
|
||||
"ConfigChoiceDetailModeAgent": "협업하고 도구를 사용할 준비 상태로 시작합니다.",
|
||||
"ConfigChoiceDetailModePlan": "읽기 전용 계획 작업 공간에서 시작합니다.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "Abrir rascunho no editor externo",
|
||||
"KbLiveTranscript": "Abrir sobreposição de transcrição ao vivo (auto-scroll)",
|
||||
"KbBacktrackMessage": "Retroceder para mensagem anterior do usuário (esquerda/direita, Enter para rebobinar)",
|
||||
"KbCompleteCycleModes": "Completar /command ou ciclar modos quando o compositor estiver vazio (Plan → Work)",
|
||||
"KbTabComplete": "Completar /command, menção @arquivo ou sugestão",
|
||||
"KbCycleThinking": "Alternar o esforço de raciocínio do provedor ativo (Ctrl+T)",
|
||||
"KbCyclePermissions": "Alternar acesso: Perguntar → Aceitar edições → Revisão automática → Acesso total (Shift+Tab)",
|
||||
"KbCyclePermissions": "Alternar acesso: Perguntar → Aceitar edições → Plan → Revisão automática → Acesso total (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Acionar slots da hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Ir para Plan / Work ou solicitar acesso total (apelido herdado)",
|
||||
"KbFocusSidebar": "Focar o painel da barra de trabalho Tasks / Agents / Context / Pinned",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "Dicas de Modo",
|
||||
"HomeAgentModeTip": "Work — trabalho direto na sessão atual com ferramentas",
|
||||
"HomeAgentModeReviewTip": " /mode plan para pesquisar e apresentar um plano primeiro",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cicla permissão: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab cicla permissão: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + acesso total — ferramentas sem pedidos de aprovação",
|
||||
"HomeYoloModeCaution": " Operações destrutivas podem rodar na hora; prefira Ask se duvidar",
|
||||
"HomePlanModeTip": "Plan — pesquise e desenhe antes de implementar",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Edições dentro deste repositório git rodam sem perguntar; comandos continuam perguntando.",
|
||||
"ConfigChoiceDetailAutoReview": "Revisa o risco das ferramentas automaticamente e pergunta quando uma decisão precisa de você.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Remove a sobreposição do config raiz e usa a escolha de permissão salva na TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Executa ferramentas sem pedir aprovação; as regras do workspace continuam valendo.",
|
||||
"ConfigChoiceDetailFullAccess": "As ferramentas rodam sem perguntar, e os comandos podem acessar tudo neste computador.",
|
||||
"ConfigChoiceDetailNever": "Bloqueia toda ferramenta que exige aprovação.",
|
||||
"ConfigChoiceDetailModeAgent": "Começa pronto para colaborar e usar ferramentas.",
|
||||
"ConfigChoiceDetailModePlan": "Começa em um espaço de planejamento somente leitura.",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Открыть черновик во внешнем редакторе",
|
||||
"KbLiveTranscript": "Открыть живую ленту поверх (Ctrl+Shift+T; автопрокрутка с привязкой к концу)",
|
||||
"KbBacktrackMessage": "Вернуться к предыдущему сообщению пользователя (Left/Right — шаг, Enter — откат)",
|
||||
"KbCompleteCycleModes": "Дополнить /command или переключить режим при пустом поле ввода (Plan → Work)",
|
||||
"KbTabComplete": "Дополнить /command, упоминание @файла или подсказку",
|
||||
"KbCycleThinking": "Перебор уровня рассуждений активного провайдера (Ctrl+T)",
|
||||
"KbCyclePermissions": "Сменить доступ: Спрашивать → Принимать правки → Авто-проверка → Полный доступ (Shift+Tab)",
|
||||
"KbCyclePermissions": "Сменить доступ: Спрашивать → Принимать правки → Plan → Авто-проверка → Полный доступ (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Активировать слоты Hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Переход к Plan / Work или запрос Full Access (устаревший псевдоним)",
|
||||
"KbFocusSidebar": "Фокус на рабочей панели Tasks / Agents / Context / Pinned",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Советы по режимам",
|
||||
"HomeAgentModeTip": "Work — прямая работа в текущей сессии с инструментами",
|
||||
"HomeAgentModeReviewTip": " /mode plan — сначала исследовать и представить план",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab переключает разрешения: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab переключает разрешения: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — инструменты работают без запросов на одобрение",
|
||||
"HomeYoloModeCaution": " Деструктивные операции могут выполниться сразу; если сомневаетесь, выбирайте Ask",
|
||||
"HomePlanModeTip": "Plan — исследование и проектирование до реализации",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Правки внутри этого git-репозитория выполняются без вопросов; команды по-прежнему спрашивают.",
|
||||
"ConfigChoiceDetailAutoReview": "Автоматически оценивает риск инструментов и спрашивает, когда решение за вами.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Снимает переопределение корневого config и использует сохранённый выбор разрешения TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Запускает инструменты без запросов на одобрение; правила рабочей области по-прежнему действуют.",
|
||||
"ConfigChoiceDetailFullAccess": "Инструменты будут запускаться без вопросов, а команды получат доступ ко всему на этом компьютере.",
|
||||
"ConfigChoiceDetailNever": "Блокирует каждый инструмент, требующий одобрения.",
|
||||
"ConfigChoiceDetailModeAgent": "Начинает в готовности сотрудничать и использовать инструменты.",
|
||||
"ConfigChoiceDetailModePlan": "Начинает в пространстве планирования только для чтения.",
|
||||
|
||||
@@ -862,9 +862,9 @@
|
||||
"KbExternalEditor": "Відкрити чернетку композера у зовнішньому редакторі",
|
||||
"KbLiveTranscript": "Відкрити накладення живого транскрипту (Ctrl+Shift+T; автопрокручування до кінця)",
|
||||
"KbBacktrackMessage": "Повернутися до попереднього повідомлення користувача (Left/Right — крок, Enter — перемотати)",
|
||||
"KbCompleteCycleModes": "Доповнити /command або перемикати режими, коли композер порожній (Plan → Work)",
|
||||
"KbTabComplete": "Доповнити /command, згадку @файлу або пропозицію",
|
||||
"KbCycleThinking": "Перемикати рівень міркувань для активного провайдера (Ctrl+T)",
|
||||
"KbCyclePermissions": "Змінити доступ: Питати → Приймати правки → Автоогляд → Повний доступ (Shift+Tab)",
|
||||
"KbCyclePermissions": "Змінити доступ: Питати → Приймати правки → Plan → Автоогляд → Повний доступ (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Активувати слоти Hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Перехід до Plan / Work або запит Full Access (застарілий псевдонім)",
|
||||
"KbFocusSidebar": "Фокус на робочій панелі Tasks / Agents / Context / Pinned",
|
||||
@@ -997,7 +997,7 @@
|
||||
"HomeModeTips": "Поради щодо режимів",
|
||||
"HomeAgentModeTip": "Work — безпосередня робота в поточному сеансі з інструментами",
|
||||
"HomeAgentModeReviewTip": " /mode plan, щоб спершу дослідити й подати план",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab перемикає дозволи: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab перемикає дозволи: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + Full Access — інструменти виконуються без запитів на схвалення",
|
||||
"HomeYoloModeCaution": " Руйнівні операції можуть виконатися одразу; якщо сумніваєтеся — використовуйте Ask",
|
||||
"HomePlanModeTip": "Plan — дослідження та проєктування перед реалізацією",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Правки всередині цього git-репозиторію виконуються без запитань; команди й надалі запитують.",
|
||||
"ConfigChoiceDetailAutoReview": "Автоматично оцінює ризик інструментів і запитує, коли рішення за вами.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Знімає перевизначення кореневого config і використовує збережений вибір дозволу TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Запускає інструменти без запитів на схвалення; правила робочого простору й далі діють.",
|
||||
"ConfigChoiceDetailFullAccess": "Інструменти запускатимуться без запитань, а команди матимуть доступ до всього на цьому комп’ютері.",
|
||||
"ConfigChoiceDetailNever": "Блокує кожен інструмент, який потребує схвалення.",
|
||||
"ConfigChoiceDetailModeAgent": "Починає в готовності співпрацювати та використовувати інструменти.",
|
||||
"ConfigChoiceDetailModePlan": "Починає в просторі планування лише для читання.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "Mở bản nháp trong trình soạn thảo bên ngoài",
|
||||
"KbLiveTranscript": "Mở lớp phủ bản ghi trực tiếp (tự động cuộn theo đuôi)",
|
||||
"KbBacktrackMessage": "Quay lại tin nhắn trước đó của người dùng (nhấn Trái/Phải để chuyển bước, Enter để lùi lại)",
|
||||
"KbCompleteCycleModes": "Hoàn thành /command hoặc chuyển chế độ khi khung soạn thảo trống (Plan → Work)",
|
||||
"KbTabComplete": "Hoàn thành /command, nhắc @tệp hoặc gợi ý",
|
||||
"KbCycleThinking": "Luân phiên mức suy luận của nhà cung cấp đang hoạt động (Ctrl+T)",
|
||||
"KbCyclePermissions": "Chuyển quyền truy cập: Hỏi → Chấp nhận chỉnh sửa → Tự động xem lại → Toàn quyền (Shift+Tab)",
|
||||
"KbCyclePermissions": "Chuyển quyền truy cập: Hỏi → Chấp nhận chỉnh sửa → Plan → Tự động xem lại → Toàn quyền (Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "Kích hoạt các ô hotbar",
|
||||
"KbAltJumpPlanAgentYolo": "Nhảy sang Plan / Work hoặc yêu cầu toàn quyền (bí danh cũ)",
|
||||
"KbFocusSidebar": "Focus vào bảng thanh làm việc Tasks / Agents / Context / Pinned",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "Mẹo về Chế độ",
|
||||
"HomeAgentModeTip": "Work — làm việc trực tiếp trong phiên với công cụ",
|
||||
"HomeAgentModeReviewTip": " /mode plan để nghiên cứu và trình kế hoạch trước",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab xoay quyền: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab xoay quyền: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + toàn quyền — công cụ chạy không cần phê duyệt",
|
||||
"HomeYoloModeCaution": " Thao tác phá hủy có thể chạy ngay; dùng Ask khi không chắc",
|
||||
"HomePlanModeTip": "Plan — nghiên cứu và thiết kế trước khi triển khai",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "Chỉnh sửa trong kho git này chạy mà không cần hỏi; lệnh vẫn hỏi.",
|
||||
"ConfigChoiceDetailAutoReview": "Tự động xem xét rủi ro của công cụ và chỉ hỏi khi cần bạn quyết định.",
|
||||
"ConfigChoiceDetailUseTuiDefault": "Bỏ ghi đè của config gốc và dùng lựa chọn quyền đã lưu trong TUI.",
|
||||
"ConfigChoiceDetailFullAccess": "Chạy công cụ mà không hỏi phê duyệt; quy tắc của workspace vẫn áp dụng.",
|
||||
"ConfigChoiceDetailFullAccess": "Công cụ sẽ chạy mà không hỏi trước, và lệnh có thể truy cập mọi thứ trên máy tính này.",
|
||||
"ConfigChoiceDetailNever": "Chặn mọi công cụ cần phê duyệt.",
|
||||
"ConfigChoiceDetailModeAgent": "Bắt đầu ở trạng thái sẵn sàng cộng tác và dùng công cụ.",
|
||||
"ConfigChoiceDetailModePlan": "Bắt đầu trong không gian lập kế hoạch chỉ đọc.",
|
||||
|
||||
@@ -879,9 +879,9 @@
|
||||
"KbExternalEditor": "在外部编辑器中打开撰写中的草稿",
|
||||
"KbLiveTranscript": "打开实时对话覆盖层(Ctrl+Shift+T;尾部自动滚动)",
|
||||
"KbBacktrackMessage": "回退到之前的用户消息(左右键步进,Enter 回退)",
|
||||
"KbCompleteCycleModes": "补全 /command,或在输入框为空时切换模式(Plan → Work)",
|
||||
"KbTabComplete": "补全 /command、@文件引用或建议",
|
||||
"KbCycleThinking": "轮换当前提供商的思考强度 (Ctrl+T)",
|
||||
"KbCyclePermissions": "切换访问级别:询问 → 接受编辑 → 自动审核 → 完全访问(Shift+Tab)",
|
||||
"KbCyclePermissions": "切换访问级别:询问 → 接受编辑 → Plan → 自动审核 → 完全访问(Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "触发快捷栏槽位",
|
||||
"KbAltJumpPlanAgentYolo": "跳转到 Plan / Work,或请求 Full Access(旧版别名)",
|
||||
"KbFocusSidebar": "聚焦 Tasks / Agents / Context / Pinned 任务面板",
|
||||
@@ -1014,7 +1014,7 @@
|
||||
"HomeModeTips": "模式提示",
|
||||
"HomeAgentModeTip": "Work — 使用工具完成你要求的更改",
|
||||
"HomeAgentModeReviewTip": " 用 /mode plan 先调研并提出计划",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab 循环权限: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab 循环权限: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + 完全访问 — 工具无需先询问即可运行",
|
||||
"HomeYoloModeCaution": " 破坏性操作可能立即执行;不确定时用 Ask",
|
||||
"HomePlanModeTip": "Plan — 实现前先调研与设计",
|
||||
@@ -2406,7 +2406,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "此 git 仓库内的编辑无需询问即可运行;命令仍会询问。",
|
||||
"ConfigChoiceDetailAutoReview": "自动审核工具风险,需要你决定时才询问。",
|
||||
"ConfigChoiceDetailUseTuiDefault": "移除根配置覆盖,使用已保存的 TUI 权限选择。",
|
||||
"ConfigChoiceDetailFullAccess": "无需审批提示即可运行工具;工作区规则仍然适用。",
|
||||
"ConfigChoiceDetailFullAccess": "工具会不经询问直接运行,命令可以访问这台电脑上的任何内容。",
|
||||
"ConfigChoiceDetailNever": "阻止所有需要审批的工具。",
|
||||
"ConfigChoiceDetailModeAgent": "以可协作并使用工具的状态开始。",
|
||||
"ConfigChoiceDetailModePlan": "在只读的规划工作区中开始。",
|
||||
|
||||
@@ -877,9 +877,9 @@
|
||||
"KbExternalEditor": "在外部編輯器中開啟撰寫中的草稿",
|
||||
"KbLiveTranscript": "開啟實時對話涵蓋層(Ctrl+Shift+T;尾部自動捲動)",
|
||||
"KbBacktrackMessage": "回退到之前的使用者訊息(左右鍵步進,Enter 回退)",
|
||||
"KbCompleteCycleModes": "補全 /command,或在輸入框為空時切換模式(Plan → Work)",
|
||||
"KbTabComplete": "補全 /command、@檔案引用或建議",
|
||||
"KbCycleThinking": "輪換目前供應商的思考強度 (Ctrl+T)",
|
||||
"KbCyclePermissions": "切換存取層級:詢問 → 接受編輯 → 自動審核 → 完整存取(Shift+Tab)",
|
||||
"KbCyclePermissions": "切換存取層級:詢問 → 接受編輯 → Plan → 自動審核 → 完整存取(Shift+Tab)",
|
||||
"KbJumpPlanAgentYolo": "觸發 Hotbar 槽位",
|
||||
"KbAltJumpPlanAgentYolo": "跳轉到 Plan / Work,或請求 Full Access(舊版別名)",
|
||||
"KbFocusSidebar": "聚焦 Tasks / Agents / Context / Pinned 任務面板",
|
||||
@@ -1012,7 +1012,7 @@
|
||||
"HomeModeTips": "模式提示",
|
||||
"HomeAgentModeTip": "Work — 使用工具完成你要求的變更",
|
||||
"HomeAgentModeReviewTip": " 用 /mode plan 先調研並提出計畫",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab 循環權限: Ask → Accept Edits → Auto-Review → Full Access",
|
||||
"HomeAgentModeYoloTip": " Shift+Tab 循環權限: Ask → Accept Edits → Plan → Auto-Review → Full Access",
|
||||
"HomeYoloModeTip": "Work + 完整存取 — 工具無需先詢問即可執行",
|
||||
"HomeYoloModeCaution": " 破壞性操作可能立即執行;不確定時用 Ask",
|
||||
"HomePlanModeTip": "Plan — 實現前先調研與設計",
|
||||
@@ -2404,7 +2404,7 @@
|
||||
"ConfigChoiceDetailAcceptEdits": "此 git 儲存庫內的編輯無需詢問即可執行;指令仍會詢問。",
|
||||
"ConfigChoiceDetailAutoReview": "自動審核工具風險,需要你決定時才詢問。",
|
||||
"ConfigChoiceDetailUseTuiDefault": "移除根設定覆寫,使用已儲存的 TUI 權限選擇。",
|
||||
"ConfigChoiceDetailFullAccess": "不經核准提示即可執行工具;工作區規則仍然適用。",
|
||||
"ConfigChoiceDetailFullAccess": "工具會不經詢問直接執行,指令可以存取這台電腦上的任何內容。",
|
||||
"ConfigChoiceDetailNever": "封鎖所有需要核准的工具。",
|
||||
"ConfigChoiceDetailModeAgent": "以可協作並使用工具的狀態開始。",
|
||||
"ConfigChoiceDetailModePlan": "在唯讀的規劃工作區中開始。",
|
||||
|
||||
@@ -1145,7 +1145,7 @@ pub enum MessageId {
|
||||
KbExternalEditor,
|
||||
KbLiveTranscript,
|
||||
KbBacktrackMessage,
|
||||
KbCompleteCycleModes,
|
||||
KbTabComplete,
|
||||
KbCycleThinking,
|
||||
KbCyclePermissions,
|
||||
KbJumpPlanAgentYolo,
|
||||
@@ -4082,7 +4082,7 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[
|
||||
MessageId::KbExternalEditor,
|
||||
MessageId::KbLiveTranscript,
|
||||
MessageId::KbBacktrackMessage,
|
||||
MessageId::KbCompleteCycleModes,
|
||||
MessageId::KbTabComplete,
|
||||
MessageId::KbCycleThinking,
|
||||
MessageId::KbCyclePermissions,
|
||||
MessageId::KbJumpPlanAgentYolo,
|
||||
|
||||
@@ -68,6 +68,15 @@ impl RuntimePolicyProjection {
|
||||
let parsed_mode = parse_runtime_mode(mode).ok_or_else(|| {
|
||||
anyhow::anyhow!("unsupported Runtime mode {mode:?}; expected plan or agent")
|
||||
})?;
|
||||
if AppMode::is_retired_operate_alias(mode) {
|
||||
static OPERATE_RETIRED_NOTICE: std::sync::Once = std::sync::Once::new();
|
||||
OPERATE_RETIRED_NOTICE.call_once(|| {
|
||||
tracing::warn!(
|
||||
target: "runtime_policy",
|
||||
"Runtime mode {mode:?} is retired: Operate is no longer a mode, so this runs as agent with the default permission posture; use /fleet for parallel agents"
|
||||
);
|
||||
});
|
||||
}
|
||||
let permission = match permission_posture {
|
||||
Some(value) => ApprovalMode::from_config_value(value).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
@@ -75,7 +84,7 @@ impl RuntimePolicyProjection {
|
||||
)
|
||||
})?,
|
||||
None if AppMode::is_legacy_bypass_alias(mode) || auto_approve.unwrap_or(false) => ApprovalMode::Bypass,
|
||||
None => ApprovalMode::Suggest,
|
||||
None => ApprovalMode::AcceptEdits,
|
||||
};
|
||||
if permission == ApprovalMode::Never {
|
||||
bail!("permission posture 'never' is not part of the Runtime product contract");
|
||||
|
||||
@@ -499,6 +499,8 @@ pub struct Settings {
|
||||
/// `bottom` migration ran, so a user who picks `top` afterwards keeps it.
|
||||
#[serde(default)]
|
||||
pub work_surface_bottom_migrated: bool,
|
||||
#[serde(default)]
|
||||
pub ask_to_accept_edits_migrated: bool,
|
||||
/// Persisted impression counts for action-triggered, ephemeral product
|
||||
/// guidance. Keys are stable tip identifiers; values are bounded by the
|
||||
/// behavioral-tip engine and omitted entirely before the first sighting.
|
||||
@@ -619,6 +621,7 @@ impl Default for Settings {
|
||||
operate_deprecation_shown: false,
|
||||
legacy_operate_default: false,
|
||||
work_surface_bottom_migrated: false,
|
||||
ask_to_accept_edits_migrated: false,
|
||||
behavioral_tip_impressions: std::collections::BTreeMap::new(),
|
||||
dismissed_plugin_suggestions: std::collections::BTreeSet::new(),
|
||||
footer_hint_uses: std::collections::BTreeMap::new(),
|
||||
@@ -1015,6 +1018,12 @@ impl Settings {
|
||||
if legacy_yolo_default && s.permission_posture.is_none() {
|
||||
s.permission_posture = Some("full-access".to_string());
|
||||
}
|
||||
if !s.ask_to_accept_edits_migrated {
|
||||
if s.permission_posture.as_deref() == Some("ask") {
|
||||
s.permission_posture = Some("accept-edits".to_string());
|
||||
}
|
||||
s.ask_to_accept_edits_migrated = true;
|
||||
}
|
||||
s.sandbox_mode = s.sandbox_mode.as_deref().and_then(normalize_sandbox_mode);
|
||||
s
|
||||
};
|
||||
@@ -3598,6 +3607,28 @@ mod tests {
|
||||
assert!(settings.set("work_surface_side_width", "25").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_ask_migrates_once_to_accept_edits_and_a_later_ask_is_kept() {
|
||||
let _g = config_path_test_guard();
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let settings_path = tmp.path().join("settings.toml");
|
||||
let _config_override =
|
||||
EnvVarRestore::set("DEEPSEEK_CONFIG_PATH", tmp.path().join("config.toml"));
|
||||
|
||||
std::fs::write(&settings_path, "permission_posture = \"ask\"\n").expect("settings");
|
||||
let loaded = Settings::load().expect("load settings");
|
||||
assert_eq!(loaded.permission_posture.as_deref(), Some("accept-edits"));
|
||||
assert!(loaded.ask_to_accept_edits_migrated);
|
||||
|
||||
Settings::transact(|settings| settings.set("permission_posture", "ask")).expect("pick Ask");
|
||||
let reloaded = Settings::load().expect("reload settings");
|
||||
assert_eq!(reloaded.permission_posture.as_deref(), Some("ask"));
|
||||
|
||||
std::fs::write(&settings_path, "permission_posture = \"auto-review\"\n").expect("settings");
|
||||
let other = Settings::load().expect("load settings");
|
||||
assert_eq!(other.permission_posture.as_deref(), Some("auto-review"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_load_keeps_top_placement_chosen_after_the_bottom_migration() {
|
||||
let _g = config_path_test_guard();
|
||||
|
||||
+26
-3
@@ -334,7 +334,7 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
- **The transcript shows the work, not the decoration.** The idle footer no longer carries a `Work bar` label (its key still opens the dock); fish, bubbles and the whale cameo swim only on the empty start screen and never behind transcript text; a streaming reasoning block is one `Thinking…` line instead of `Thinking…` above `reasoning...`, and the hidden-reasoning line and window title say thinking too; the tool-card hint reads `⌥V details` in dim text instead of `⌥V:details`.
|
||||
- **Sub-agents and goals by default.** The constitution now tells the agent to start independent parts of a request as parallel sub-agents in one message, keep working while their results arrive on their own, and end the turn when only waiting remains; to give each child a complete prompt and its own files; to inspect a write-capable child's evidence, review the diff and run the checks itself; to have an independent reviewer try to refute a non-trivial change before calling it done; and to start a goal for any request, in any language, whose verifiable end state needs more than one turn. Sub-agents, workflows and goals within the current request are in scope; spending real money (purchases, paid providers, billing) still needs the user's express authorization. Cost alone is not a reason to stop; only limits the user or configuration sets apply. **This changes the session-pinned prompt and tool descriptions, so each session re-pins its prompt cache once after upgrade.**
|
||||
- **Operate's contract is retired.** The Engine no longer appends the Operate contract to a session; its review, cost and evidence rules now live in the constitution for every session. Saved sessions that contain it still replay.
|
||||
- **The Operate mode is gone; Work and Plan are the two modes.** Tab cycles between them, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
|
||||
- **The Operate mode is gone; Work and Plan are the two modes.** The Shift+Tab permission dial reaches both, `/mode` and the config editor offer only those two, and a saved `default_mode = "operate"` loads as Work with a one-time notice that `/fleet` runs parallel agents. `operate`, `operation`, `ops` and `3` still parse, as Work, so an old setting or script never errors. Fan-out is unchanged — 64 live sub-agents per message, workflows at a 64 default with a 128 ceiling — while the Operate-only launch-concurrency floor and its top-level child auto-accept posture are gone; a child's edits follow the permission posture like every other action. "Plan stays Read Only; this applies in Work" now says exactly that.
|
||||
- **Clearer tool text.** The `agent` description leads with when to delegate, gives one trigger per role and an example child brief, and drops Operate and legacy wording; the start receipt says results arrive automatically. `create_goal` and `update_goal` say when to start a goal and what complete, blocked and yield mean (blocked means the same obstacle persists; difficulty is not blocked). `tool_search` lists the deferred tool groups. The eager tool schemas shrink by 681 bytes.
|
||||
- **Workflow fan-out default 16 → 64.** `[workflow] max_concurrent` now defaults to 64 live agents per run, matching sub-agents; the per-run ceiling rises from 16 to 128. No spend caps were added.
|
||||
- **Bundled skill `goals` is now `personal-goals`.** It tracks personal resolutions in `goals.md` and no longer collides with goal mode; `goals` still resolves to it. An untouched installed `goals` copy becomes `personal-goals` on upgrade; an edited one stays as `goals`.
|
||||
@@ -347,12 +347,35 @@ tag. Items moved to 0.10.4 by name are marked below.
|
||||
an edit whose every target stays inside the workspace git work tree runs
|
||||
without a card, while `.git`, `.codewhale`, `.env*`, credential and key
|
||||
files, folders without git, commands, network and connected apps still ask.
|
||||
Shift+Tab cycles Ask → Accept Edits → Auto-Review → Full Access, so one
|
||||
press restores the previous Ask behaviour. `/permissions`, `/status`,
|
||||
Accept Edits is the default (see the permission dial entry below).
|
||||
`/permissions`, `/status`,
|
||||
`/config` and the Plan handoff offer it; the Runtime wire spells it
|
||||
`permission_posture: "accept_edits"`, settings and `config.toml`
|
||||
`accept-edits`. Project config may tighten Accept Edits to Ask but never
|
||||
loosen Ask to it. ACP sessions run it as Ask.
|
||||
- **One permission dial; Tab no longer changes the mode (CW-7890):** Shift+Tab
|
||||
steps Ask → Accept Edits → Plan → Auto-Review → Full Access → Ask. Plan is the
|
||||
Plan mode; every other stop is Work with that posture. Tab only completes
|
||||
commands, file mentions and suggestions. The footer shows one chip for the
|
||||
current stop with `Shift+Tab to change`; the separate mode chip and its Tab
|
||||
hint are gone. The dial moves mid-turn too, into or out of Plan, and applies
|
||||
at the agent's next step without restarting a response or changing the pinned
|
||||
prompt. When config or managed policy owns the posture, the dial still
|
||||
alternates between that posture and Plan.
|
||||
- **Accept Edits is the default posture:** new installs, unset settings and new
|
||||
Runtime threads that send no `permission_posture` start on Accept Edits. A
|
||||
saved `permission_posture = "ask"` from an earlier build moves to Accept Edits
|
||||
once (recorded as `ask_to_accept_edits_migrated`), so an Ask you pick
|
||||
afterwards is kept. The "Normal agent" setup preset now writes Accept Edits.
|
||||
ACP still runs Accept Edits as Ask.
|
||||
- **Full Access copy says what it does:** its settings description now reads
|
||||
"Tools run without asking first, and commands can reach anything on this
|
||||
computer." in every language, instead of implying a folder fence.
|
||||
- **Operate compatibility (CW-7849):** `/mode operate` selects Work and says
|
||||
Operate was retired; a Runtime API request with `mode: "operate"` runs as
|
||||
`agent` with the default posture and logs one notice per process, never an
|
||||
error. Agents forked from a conversation saved by an earlier Operate build no
|
||||
longer inherit its contract message.
|
||||
- **Brighter terminal working states:** the Ocean palette has stronger accents,
|
||||
and active work uses a restrained left-to-right text sweep. Quiet and
|
||||
reduced-motion settings keep stable working indicators. The shared
|
||||
|
||||
@@ -169,7 +169,6 @@ fn to_app_mode(mode: CommandMode) -> AppMode {
|
||||
match mode {
|
||||
CommandMode::Agent => AppMode::Agent,
|
||||
CommandMode::Plan => AppMode::Plan,
|
||||
CommandMode::Operate => AppMode::Agent,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1991,7 +1991,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) ->
|
||||
.and_then(|settings| settings.permission_posture)
|
||||
.as_deref()
|
||||
.and_then(ApprovalMode::from_config_value)
|
||||
.unwrap_or(ApprovalMode::Suggest);
|
||||
.unwrap_or(ApprovalMode::AcceptEdits);
|
||||
app.set_agent_approval_posture(saved_mode);
|
||||
app.clear_saved_approval_policy_lock();
|
||||
config_action_reply(
|
||||
|
||||
@@ -130,7 +130,6 @@ pub(crate) fn from_command_mode(mode: CommandMode) -> AppMode {
|
||||
match mode {
|
||||
CommandMode::Agent => AppMode::Agent,
|
||||
CommandMode::Plan => AppMode::Plan,
|
||||
CommandMode::Operate => AppMode::Agent,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ use codewhale_localization::MessageId;
|
||||
pub(super) fn resolve(key: &str) -> Option<MessageId> {
|
||||
Some(match key {
|
||||
"app_mode_agent" => MessageId::AppModeAgent,
|
||||
"app_mode_operate" => MessageId::AppModeOperate,
|
||||
"app_mode_plan" => MessageId::AppModePlan,
|
||||
"permissions_applies_here" => MessageId::PermissionsAppliesHere,
|
||||
"permissions_file_empty" => MessageId::PermissionsFileEmpty,
|
||||
|
||||
@@ -570,8 +570,8 @@ pub fn settings(contexts: CommandContexts<'_>, arg: Option<&str>) -> ConfigComma
|
||||
}
|
||||
}
|
||||
|
||||
/// `/mode [act|plan|operate|1|2|3]` and the `/jihua`/`/zidong` aliases.
|
||||
/// `/mode [act|plan|operate|1|2|3]` (aliases `/jihua`, `/zidong`).
|
||||
/// `/mode [work|plan|1|2]` (aliases `/jihua`, `/zidong`). A retired Operate
|
||||
/// spelling selects Work and says so.
|
||||
///
|
||||
/// The command owns the alias handling, parse and message composition; the
|
||||
/// mode-policy facet applies the selection and reports the outcome.
|
||||
@@ -599,18 +599,24 @@ pub fn mode(contexts: CommandContexts<'_>, arg: Option<&str>) -> ConfigCommandRe
|
||||
}
|
||||
};
|
||||
}
|
||||
let retired = if CommandMode::is_retired_operate_alias(arg) {
|
||||
" Operate mode was retired; /fleet runs parallel agents."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
match CommandMode::parse(arg) {
|
||||
Some(mode) => {
|
||||
let selection = facet.select_mode(mode);
|
||||
let label = facet.mode_label(mode);
|
||||
match selection {
|
||||
CommandModeSelection::Changed => ConfigCommandResult::with_message_and_action(
|
||||
format!("Switched to {label} mode."),
|
||||
format!("Switched to {label} mode.{retired}"),
|
||||
ConfigAction::ModeChanged(mode),
|
||||
),
|
||||
CommandModeSelection::PersistedSame => {
|
||||
ConfigCommandResult::message(facet.mode_startup_default_receipt(mode))
|
||||
}
|
||||
CommandModeSelection::PersistedSame => ConfigCommandResult::message(format!(
|
||||
"{}{retired}",
|
||||
facet.mode_startup_default_receipt(mode)
|
||||
)),
|
||||
CommandModeSelection::Refused => {
|
||||
ConfigCommandResult::message(facet.setting_locked_mode_message())
|
||||
}
|
||||
|
||||
@@ -178,7 +178,6 @@ impl PermissionsMessages {
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum StatusText {
|
||||
AppModeAgent,
|
||||
AppModeOperate,
|
||||
AppModePlan,
|
||||
SessionMetricsCache,
|
||||
SessionMetricsInput,
|
||||
@@ -261,7 +260,6 @@ pub enum StatusText {
|
||||
|
||||
pub struct StatusMessages {
|
||||
app_mode_agent: String,
|
||||
app_mode_operate: String,
|
||||
app_mode_plan: String,
|
||||
session_metrics_cache: String,
|
||||
session_metrics_input: String,
|
||||
@@ -346,7 +344,6 @@ impl StatusMessages {
|
||||
pub fn load(presentation: &dyn CommandPresentationContext) -> Result<Self, String> {
|
||||
Ok(Self {
|
||||
app_mode_agent: presentation.translate("app_mode_agent", &[])?,
|
||||
app_mode_operate: presentation.translate("app_mode_operate", &[])?,
|
||||
app_mode_plan: presentation.translate("app_mode_plan", &[])?,
|
||||
session_metrics_cache: presentation.translate("session_metrics_cache", &[])?,
|
||||
session_metrics_input: presentation.translate("session_metrics_input", &[])?,
|
||||
@@ -528,7 +525,6 @@ impl StatusMessages {
|
||||
Cow::Owned(
|
||||
match id {
|
||||
StatusText::AppModeAgent => &self.app_mode_agent,
|
||||
StatusText::AppModeOperate => &self.app_mode_operate,
|
||||
StatusText::AppModePlan => &self.app_mode_plan,
|
||||
StatusText::SessionMetricsCache => &self.session_metrics_cache,
|
||||
StatusText::SessionMetricsInput => &self.session_metrics_input,
|
||||
|
||||
@@ -301,7 +301,6 @@ fn posture_summary(view: &ConfigStatusView, locale: &Messages) -> String {
|
||||
match view.mode {
|
||||
CommandMode::Agent => MessageId::AppModeAgent,
|
||||
CommandMode::Plan => MessageId::AppModePlan,
|
||||
CommandMode::Operate => MessageId::AppModeOperate,
|
||||
},
|
||||
);
|
||||
let approval = approval_summary(view.approval_mode, locale);
|
||||
|
||||
@@ -150,7 +150,6 @@ fn render_overview(
|
||||
"home_agent_mode_review_tip",
|
||||
"home_agent_mode_yolo_tip",
|
||||
],
|
||||
CommandMode::Operate => &["home_operate_mode_tip", "home_operate_mode_fleet_tip"],
|
||||
CommandMode::Plan => &["home_plan_mode_tip", "home_plan_mode_checklist_tip"],
|
||||
};
|
||||
for key in tips {
|
||||
|
||||
+52
-52
@@ -78,9 +78,9 @@ pub(crate) enum RedactionGateNotice {
|
||||
}
|
||||
pub use types::{
|
||||
AppAction, AppModeUi, AutomationAction, ComposerDensity, ComposerSubmitAction,
|
||||
ComposerSubmitChord, InflightSteer, InitialInput, McpUiAction, QueuedMessage, ScreenMode,
|
||||
SettingSelection, ShellJobAction, SubmitDisposition, TaskPanelEntry, TaskPanelEntryKind,
|
||||
ToolCollapseMode, ToolDetailRecord, TranscriptSpacing, TuiOptions, VimMode,
|
||||
ComposerSubmitChord, DialStep, InflightSteer, InitialInput, McpUiAction, QueuedMessage,
|
||||
ScreenMode, SettingSelection, ShellJobAction, SubmitDisposition, TaskPanelEntry,
|
||||
TaskPanelEntryKind, ToolCollapseMode, ToolDetailRecord, TranscriptSpacing, TuiOptions, VimMode,
|
||||
};
|
||||
pub(crate) use types::{
|
||||
CacheReplayTarget, GoalControlIntent, PendingGoalControl, UnansweredSubmission,
|
||||
@@ -3443,7 +3443,7 @@ impl App {
|
||||
}
|
||||
|
||||
/// Show the one-time Fleet intro as a status line, the first time the
|
||||
/// user opens `/fleet` or enters Operate — never as a first-run push.
|
||||
/// user opens `/fleet` — never as a first-run push.
|
||||
/// It inserts no transcript message: a synthetic history cell would hide
|
||||
/// the empty launch surface before the user sends anything.
|
||||
pub fn maybe_show_feature_intro(&mut self) {
|
||||
@@ -3644,8 +3644,8 @@ impl App {
|
||||
/// application use it because they are re-installing a mode the user
|
||||
/// already chose elsewhere, and re-persisting there would let a restored
|
||||
/// session silently rewrite the startup default. Every interactive
|
||||
/// selector (Tab/Shift+Tab cycling, the Alt+A/P shortcuts, the hotbar
|
||||
/// mode actions) goes through here instead, so "I switched to Operate"
|
||||
/// selector (the Shift+Tab permission dial, the Alt+A/P shortcuts, the
|
||||
/// hotbar mode actions) goes through here instead, so "I switched to Plan"
|
||||
/// survives a restart (reported by Hunter against v0.9.1). The legacy
|
||||
/// YOLO entry points go through [`Self::select_yolo_compat`] instead.
|
||||
///
|
||||
@@ -3674,8 +3674,8 @@ impl App {
|
||||
pub fn select_mode(&mut self, mode: AppMode) -> SettingSelection {
|
||||
let changed = self.set_mode(mode);
|
||||
// Persist an explicit selection even when it matches the live mode.
|
||||
// A restored session can be Operate while the startup default remains
|
||||
// Act; choosing Operate again is a request to make the visible state
|
||||
// A restored session can be Plan while the startup default remains
|
||||
// Work; choosing Plan again is a request to make the visible state
|
||||
// durable, not a no-op.
|
||||
self.startup_defaults
|
||||
.spawn(crate::tui::startup_defaults::StartupDefaults::mode(
|
||||
@@ -3835,21 +3835,6 @@ impl App {
|
||||
.replace("{setting}", self.tr(subject).as_ref())
|
||||
}
|
||||
|
||||
/// Cycle between the two modes: Plan → Work → Plan.
|
||||
pub fn cycle_mode(&mut self) {
|
||||
let next = self.mode.next();
|
||||
let outcome = self.select_mode(next);
|
||||
self.report_mode_selection(next, outcome);
|
||||
}
|
||||
|
||||
/// Cycle through modes in reverse.
|
||||
#[cfg(test)]
|
||||
pub fn cycle_mode_reverse(&mut self) {
|
||||
let next = self.mode.previous();
|
||||
let outcome = self.select_mode(next);
|
||||
self.report_mode_selection(next, outcome);
|
||||
}
|
||||
|
||||
/// Show the startup-default receipt for a selection that did not move live
|
||||
/// mode. `Changed` and `Refused` already have their own messaging (the mode
|
||||
/// chip, and `reject_setting_change_while_busy` respectively).
|
||||
@@ -3974,12 +3959,48 @@ impl App {
|
||||
self.needs_redraw = true;
|
||||
}
|
||||
|
||||
/// Cycle the durable Agent permission posture: Ask → Accept Edits →
|
||||
/// Auto-Review → Full Access.
|
||||
pub fn cycle_approval_posture(&mut self) -> bool {
|
||||
let Some(next) = self.next_approval_posture(false) else {
|
||||
return false;
|
||||
#[must_use]
|
||||
pub fn permission_dial_next(&self) -> (AppMode, ApprovalMode) {
|
||||
if self.mode == AppMode::Plan {
|
||||
return (AppMode::Agent, ApprovalMode::Auto);
|
||||
}
|
||||
match self.approval_mode {
|
||||
ApprovalMode::AcceptEdits => (AppMode::Plan, ApprovalMode::AcceptEdits),
|
||||
current => (AppMode::Agent, current.cycle_permission_next()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn step_permission_dial(&mut self, root_control: bool) -> DialStep {
|
||||
let (mode, posture) = self.permission_dial_next();
|
||||
let current_posture = if self.mode == AppMode::Plan {
|
||||
self.mode_prefs.agent_approval_mode
|
||||
} else {
|
||||
self.approval_mode
|
||||
};
|
||||
let mut step = DialStep::default();
|
||||
if mode != self.mode {
|
||||
step.mode_changed = self.select_mode(mode) == SettingSelection::Changed;
|
||||
}
|
||||
if posture != current_posture {
|
||||
step.posture_changed = if root_control {
|
||||
self.select_root_approval_posture(posture)
|
||||
} else {
|
||||
self.select_approval_posture(posture)
|
||||
};
|
||||
let posture_locked = if root_control {
|
||||
!self.approval_policy_root_editable
|
||||
} else {
|
||||
self.approval_policy_locked()
|
||||
};
|
||||
if !step.posture_changed && !step.mode_changed && posture_locked {
|
||||
step.mode_changed = self.select_mode(AppMode::Plan) == SettingSelection::Changed;
|
||||
}
|
||||
}
|
||||
step
|
||||
}
|
||||
|
||||
/// Set the durable Agent permission posture.
|
||||
pub fn select_approval_posture(&mut self, next: ApprovalMode) -> bool {
|
||||
if self.approval_policy_locked() {
|
||||
self.push_status_toast(
|
||||
"Permissions are controlled by config or managed requirements".to_string(),
|
||||
@@ -4002,14 +4023,11 @@ impl App {
|
||||
true
|
||||
}
|
||||
|
||||
/// Cycle permissions when the only controlling source is the user's
|
||||
/// Set permissions when the only controlling source is the user's
|
||||
/// editable root `config.toml` key. Shift+Tab is an explicit request to
|
||||
/// adopt the TUI posture, so persist the next setting first, then remove
|
||||
/// the shadowing root key. Roll back the setting if that removal fails.
|
||||
pub fn cycle_root_approval_posture(&mut self) -> bool {
|
||||
let Some(next) = self.next_approval_posture(true) else {
|
||||
return false;
|
||||
};
|
||||
pub fn select_root_approval_posture(&mut self, next: ApprovalMode) -> bool {
|
||||
if !self.approval_policy_root_editable {
|
||||
self.push_status_toast(
|
||||
"Permissions are controlled by a non-editable policy source".to_string(),
|
||||
@@ -4103,24 +4121,6 @@ impl App {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn next_approval_posture(&mut self, allow_root_policy: bool) -> Option<ApprovalMode> {
|
||||
// Plan used to refuse the change outright, which welded the two axes
|
||||
// together on the keyboard: Tab cycles the mode, Shift+Tab cycles the
|
||||
// posture, and in Plan the second key silently did nothing. They are
|
||||
// independent settings and both must stay settable.
|
||||
//
|
||||
// Nothing is weakened by allowing it. Plan's read-only guarantee is
|
||||
// derived from the mode, not from the posture: `authority` maps
|
||||
// `(Plan, _, Bypass)` to `SandboxPolicy::ReadOnly` (there is a test
|
||||
// pinning exactly that), and `tool_catalog` gates every write tool on
|
||||
// `mode != AppMode::Plan`. Setting the posture here records the
|
||||
// preference that takes effect on the next Act/Operate turn.
|
||||
if allow_root_policy && !self.approval_policy_root_editable {
|
||||
return None;
|
||||
}
|
||||
Some(self.mode_prefs.agent_approval_mode.cycle_permission_next())
|
||||
}
|
||||
|
||||
fn approval_posture_setting(mode: ApprovalMode) -> &'static str {
|
||||
match mode {
|
||||
ApprovalMode::Suggest => "ask",
|
||||
@@ -4133,7 +4133,7 @@ impl App {
|
||||
|
||||
/// Persist the Shift+Tab permission posture.
|
||||
///
|
||||
/// Synchronous on purpose: `cycle_approval_posture` only moves the live
|
||||
/// Synchronous on purpose: `select_approval_posture` only moves the live
|
||||
/// posture if this succeeded, so the keystroke already required the write.
|
||||
/// It runs inside [`Settings::transact`] so it cannot interleave with a
|
||||
/// queued mode/thinking write — the two used to load the same bytes and the
|
||||
|
||||
@@ -600,7 +600,7 @@ impl App {
|
||||
};
|
||||
let configured_approval_mode = explicit_approval_mode
|
||||
.or(saved_permission_posture)
|
||||
.unwrap_or_default();
|
||||
.unwrap_or(ApprovalMode::AcceptEdits);
|
||||
let configured_trust_mode = configured_approval_mode == ApprovalMode::Bypass;
|
||||
let mode_prefs = ModeSessionPrefs {
|
||||
agent_allow_shell: if yolo_compat {
|
||||
|
||||
@@ -668,7 +668,7 @@ fn thinking_is_locked_but_mode_moves_while_a_turn_is_running() {
|
||||
app.status_message
|
||||
);
|
||||
|
||||
app.cycle_mode();
|
||||
app.select_mode(AppMode::Plan);
|
||||
assert_ne!(app.mode, AppMode::Agent, "mode changes mid-turn");
|
||||
}
|
||||
|
||||
@@ -3782,35 +3782,6 @@ fn entering_operate_preserves_user_rail_panel() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_scenario() {
|
||||
// Scenario consolidation of: test_cycle_mode_transitions, test_cycle_mode_reverse_transitions
|
||||
// from test_cycle_mode_transitions
|
||||
{
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
let initial_mode = app.mode;
|
||||
app.cycle_mode();
|
||||
// Mode should have changed
|
||||
assert_ne!(app.mode, initial_mode);
|
||||
}
|
||||
// from test_cycle_mode_reverse_transitions
|
||||
{
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
|
||||
app.mode = AppMode::Plan;
|
||||
app.cycle_mode_reverse();
|
||||
assert_eq!(app.mode, AppMode::Agent);
|
||||
|
||||
app.mode = AppMode::Agent;
|
||||
app.cycle_mode_reverse();
|
||||
assert_eq!(app.mode, AppMode::Plan);
|
||||
|
||||
app.mode = AppMode::Agent;
|
||||
app.cycle_mode_reverse();
|
||||
assert_eq!(app.mode, AppMode::Plan);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn effective_route_display_tracks_inflight_and_last_auto_provider() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
@@ -3836,8 +3807,8 @@ fn test_mode_scenario() {
|
||||
// from test_mode_switch_does_not_emit_redundant_toast
|
||||
{
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
let first_mode = app.mode.next();
|
||||
let second_mode = first_mode.next();
|
||||
let first_mode = AppMode::Plan;
|
||||
let second_mode = AppMode::Agent;
|
||||
|
||||
app.set_mode(first_mode);
|
||||
app.sync_status_message_to_toasts();
|
||||
@@ -4051,8 +4022,8 @@ fn base_policy_for_mode_projects_the_mode_permission_table() {
|
||||
|
||||
#[test]
|
||||
fn cycle_approval_scenario() {
|
||||
// Scenario consolidation of: cycle_approval_posture_cycles_suggest_auto_bypass, cycle_approval_posture_emits_rebinding_notice_once
|
||||
// from cycle_approval_posture_cycles_suggest_auto_bypass
|
||||
// Scenario consolidation of: the permission dial walk, cycle_approval_posture_emits_rebinding_notice_once
|
||||
// from the permission dial walk
|
||||
{
|
||||
let _env_lock = lock_test_env();
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
@@ -4062,19 +4033,22 @@ fn cycle_approval_scenario() {
|
||||
options.start_in_agent_mode = true;
|
||||
options.config_path = Some(config_path);
|
||||
let mut app = App::new(options, &Config::default());
|
||||
app.approval_mode = ApprovalMode::Suggest;
|
||||
app.set_agent_approval_posture(ApprovalMode::Suggest);
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert_eq!(app.approval_mode, ApprovalMode::AcceptEdits);
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Auto);
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Bypass);
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Suggest);
|
||||
for (mode, posture) in [
|
||||
(AppMode::Agent, ApprovalMode::AcceptEdits),
|
||||
(AppMode::Plan, ApprovalMode::AcceptEdits),
|
||||
(AppMode::Agent, ApprovalMode::Auto),
|
||||
(AppMode::Agent, ApprovalMode::Bypass),
|
||||
(AppMode::Agent, ApprovalMode::Suggest),
|
||||
] {
|
||||
assert!(app.step_permission_dial(false).changed());
|
||||
assert_eq!(app.mode, mode);
|
||||
assert_eq!(app.mode_prefs.agent_approval_mode, posture);
|
||||
if mode == AppMode::Agent {
|
||||
assert_eq!(app.approval_mode, posture);
|
||||
}
|
||||
}
|
||||
let persisted =
|
||||
std::fs::read_to_string(tmp.path().join("settings.toml")).expect("settings");
|
||||
assert!(persisted.contains("permission_posture = \"ask\""));
|
||||
@@ -4090,7 +4064,7 @@ fn cycle_approval_scenario() {
|
||||
options.config_path = Some(config_path);
|
||||
let mut app = App::new(options, &Config::default());
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert!(app.select_approval_posture(ApprovalMode::Auto));
|
||||
let notices = app
|
||||
.status_toasts
|
||||
.iter()
|
||||
@@ -4098,7 +4072,7 @@ fn cycle_approval_scenario() {
|
||||
.count();
|
||||
assert_eq!(notices, 1, "first cycle posts the rebinding notice");
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert!(app.select_approval_posture(ApprovalMode::Bypass));
|
||||
let notices = app
|
||||
.status_toasts
|
||||
.iter()
|
||||
@@ -4130,8 +4104,8 @@ fn plan_permission_cycle_moves_the_baseline_and_leaves_plan_read_only() {
|
||||
app.set_mode(AppMode::Plan);
|
||||
|
||||
assert!(
|
||||
app.cycle_approval_posture(),
|
||||
"Shift+Tab must still change permissions while in Plan"
|
||||
app.select_approval_posture(ApprovalMode::Bypass),
|
||||
"permissions must still change while in Plan"
|
||||
);
|
||||
assert_eq!(
|
||||
app.mode_prefs.agent_approval_mode,
|
||||
@@ -4178,11 +4152,11 @@ fn busy_permission_cycle_moves_the_live_posture() {
|
||||
let mut options = test_options(false);
|
||||
options.config_path = Some(config_path);
|
||||
let mut app = App::new(options, &Config::default());
|
||||
let before = app.approval_mode;
|
||||
let before = (app.mode, app.mode_prefs.agent_approval_mode);
|
||||
app.is_loading = true;
|
||||
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert_ne!(app.mode_prefs.agent_approval_mode, before);
|
||||
assert!(app.step_permission_dial(false).changed());
|
||||
assert_ne!((app.mode, app.mode_prefs.agent_approval_mode), before);
|
||||
assert!(
|
||||
!app.status_message
|
||||
.as_deref()
|
||||
@@ -4193,11 +4167,11 @@ fn busy_permission_cycle_moves_the_live_posture() {
|
||||
#[test]
|
||||
fn permission_postures_persist_across_restart() {
|
||||
let _env_lock = lock_test_env();
|
||||
for (cycles, expected) in [
|
||||
(1, ApprovalMode::AcceptEdits),
|
||||
(2, ApprovalMode::Auto),
|
||||
(3, ApprovalMode::Bypass),
|
||||
(4, ApprovalMode::Suggest),
|
||||
for expected in [
|
||||
ApprovalMode::AcceptEdits,
|
||||
ApprovalMode::Auto,
|
||||
ApprovalMode::Bypass,
|
||||
ApprovalMode::Suggest,
|
||||
] {
|
||||
let tmp = tempfile::tempdir().expect("tempdir");
|
||||
let path = tmp.path().join("config.toml");
|
||||
@@ -4206,9 +4180,10 @@ fn permission_postures_persist_across_restart() {
|
||||
options.start_in_agent_mode = true;
|
||||
options.config_path = Some(path.clone());
|
||||
let mut app = App::new(options.clone(), &Config::default());
|
||||
for _ in 0..cycles {
|
||||
assert!(app.cycle_approval_posture());
|
||||
if app.approval_mode == expected {
|
||||
assert!(app.select_approval_posture(ApprovalMode::Auto));
|
||||
}
|
||||
assert!(app.select_approval_posture(expected));
|
||||
assert_eq!(app.approval_mode, expected);
|
||||
assert_eq!(app.trust_mode, expected == ApprovalMode::Bypass);
|
||||
|
||||
@@ -4241,7 +4216,7 @@ fn shift_tab_migrates_user_root_policy_to_durable_tui_posture() {
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Suggest);
|
||||
assert!(app.approval_policy_locked());
|
||||
|
||||
assert!(app.cycle_root_approval_posture());
|
||||
assert!(app.step_permission_dial(true).posture_changed);
|
||||
assert_eq!(app.approval_mode, ApprovalMode::AcceptEdits);
|
||||
assert!(!app.approval_policy_locked());
|
||||
let saved_config = std::fs::read_to_string(&config_path).expect("saved config");
|
||||
@@ -4380,13 +4355,20 @@ fn managed_requirements_ignore_saved_full_access_and_lock_changes() {
|
||||
assert!(app.approval_policy_locked());
|
||||
assert!(app.approval_policy_requirements_managed());
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Suggest);
|
||||
assert!(!app.cycle_approval_posture());
|
||||
assert!(!app.select_approval_posture(ApprovalMode::AcceptEdits));
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Suggest);
|
||||
assert!(
|
||||
app.status_toasts
|
||||
.iter()
|
||||
.any(|toast| toast.text.contains("controlled"))
|
||||
);
|
||||
let step = app.step_permission_dial(false);
|
||||
assert!(!step.posture_changed);
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
AppMode::Plan,
|
||||
"a locked posture still reaches Plan"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4423,7 +4405,7 @@ fn sandbox_requirements_prevent_full_access_overrides() {
|
||||
assert!(!app.trust_mode);
|
||||
assert!(!app.yolo);
|
||||
assert!(app.approval_policy_requirements_managed());
|
||||
assert!(!app.cycle_approval_posture());
|
||||
assert!(!app.select_approval_posture(ApprovalMode::AcceptEdits));
|
||||
assert_eq!(app.select_yolo_compat(), SettingSelection::Refused);
|
||||
assert!(matches!(
|
||||
codewhale_engine::core::authority::sandbox_policy_for_turn(
|
||||
@@ -6865,9 +6847,14 @@ fn interactive_mode_cycle_persists_the_startup_default() {
|
||||
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.mode = AppMode::Agent;
|
||||
app.cycle_mode();
|
||||
app.approval_mode = ApprovalMode::AcceptEdits;
|
||||
assert!(app.step_permission_dial(false).mode_changed);
|
||||
|
||||
assert_eq!(app.mode, AppMode::Plan, "Work -> Plan is the Tab cycle");
|
||||
assert_eq!(
|
||||
app.mode,
|
||||
AppMode::Plan,
|
||||
"Accept Edits -> Plan is the dial's next stop"
|
||||
);
|
||||
let reloaded = Settings::load().expect("reload settings");
|
||||
assert_eq!(
|
||||
reloaded.default_mode, "plan",
|
||||
@@ -6962,10 +6949,11 @@ fn mode_change_while_a_turn_runs_is_live_and_persisted() {
|
||||
let mut app = App::new(test_options(false), &Config::default());
|
||||
app.select_mode(AppMode::Plan);
|
||||
app.is_loading = true;
|
||||
app.cycle_mode();
|
||||
assert!(app.step_permission_dial(false).mode_changed);
|
||||
app.startup_defaults.flush();
|
||||
|
||||
assert_eq!(app.mode, AppMode::Plan.next());
|
||||
assert_eq!(app.mode, AppMode::Agent);
|
||||
assert_eq!(app.approval_mode, ApprovalMode::Auto);
|
||||
assert_eq!(
|
||||
Settings::load().expect("reload").default_mode,
|
||||
app.mode.as_setting(),
|
||||
@@ -7254,7 +7242,7 @@ async fn mode_and_permission_posture_writes_do_not_clobber_each_other() {
|
||||
"mode selection must change mode"
|
||||
);
|
||||
assert!(
|
||||
app.cycle_approval_posture(),
|
||||
app.select_approval_posture(app.mode_prefs.agent_approval_mode.cycle_permission_next()),
|
||||
"the posture write must succeed, or the assertion below is vacuous"
|
||||
);
|
||||
}
|
||||
@@ -7358,7 +7346,9 @@ async fn rapid_mixed_writes_settle_on_the_last_value_for_every_field() {
|
||||
);
|
||||
app.apply_reasoning_effort_cycle();
|
||||
// Synchronous direct writers.
|
||||
assert!(app.cycle_approval_posture());
|
||||
assert!(
|
||||
app.select_approval_posture(app.mode_prefs.agent_approval_mode.cycle_permission_next())
|
||||
);
|
||||
Settings::transact(|settings| settings.set("max_history", &(200 + index).to_string()))
|
||||
.expect("the direct write must land");
|
||||
}
|
||||
|
||||
@@ -32,6 +32,19 @@ pub enum SettingSelection {
|
||||
Refused,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct DialStep {
|
||||
pub mode_changed: bool,
|
||||
pub posture_changed: bool,
|
||||
}
|
||||
|
||||
impl DialStep {
|
||||
#[must_use]
|
||||
pub fn changed(self) -> bool {
|
||||
self.mode_changed || self.posture_changed
|
||||
}
|
||||
}
|
||||
|
||||
impl SettingSelection {
|
||||
/// Whether live state moved — i.e. whether the engine needs resyncing.
|
||||
#[must_use]
|
||||
|
||||
@@ -19,7 +19,6 @@ pub(crate) const USES_TO_RETIRE: u8 = 2;
|
||||
/// Stable hint keys. The footer shows a hint until its key reaches
|
||||
/// [`USES_TO_RETIRE`] uses, then renders the bare state.
|
||||
pub(crate) const PERMISSION_CYCLE: &str = "permission_cycle";
|
||||
pub(crate) const MODE_CYCLE: &str = "mode_cycle";
|
||||
pub(crate) const ESC_INTERRUPT: &str = "esc_interrupt";
|
||||
pub(crate) const ENTER_AGAIN: &str = "enter_again";
|
||||
pub(crate) const AGENT_ARROWS: &str = "agent_arrows";
|
||||
@@ -90,13 +89,7 @@ mod tests {
|
||||
fn hints_retire_after_two_uses() {
|
||||
let uses = BTreeMap::new();
|
||||
assert!(!retired(&uses, PERMISSION_CYCLE));
|
||||
for key in [
|
||||
PERMISSION_CYCLE,
|
||||
MODE_CYCLE,
|
||||
ESC_INTERRUPT,
|
||||
ENTER_AGAIN,
|
||||
AGENT_ARROWS,
|
||||
] {
|
||||
for key in [PERMISSION_CYCLE, ESC_INTERRUPT, ENTER_AGAIN, AGENT_ARROWS] {
|
||||
let mut uses = BTreeMap::new();
|
||||
uses.insert(key.to_string(), 1);
|
||||
assert!(!retired(&uses, key), "{key} at 1 use still shows");
|
||||
@@ -118,6 +111,6 @@ mod tests {
|
||||
app.note_footer_hint_used(PERMISSION_CYCLE);
|
||||
assert!(retired(&app.footer_hint_uses, PERMISSION_CYCLE));
|
||||
// Other keys are unaffected.
|
||||
assert!(!retired(&app.footer_hint_uses, MODE_CYCLE));
|
||||
assert!(!retired(&app.footer_hint_uses, ESC_INTERRUPT));
|
||||
}
|
||||
}
|
||||
@@ -614,17 +614,17 @@ mod tests {
|
||||
// band — phase·cost on the left, the notice/keys slot on the right.
|
||||
// Wired into `ui/frame.rs` as the shell's single footer row: the classic
|
||||
// activity band (slot 6) and identity band (slot 8) collapsed into it, with
|
||||
// the old header's mode/permission chips carried in the left half per §3.
|
||||
// the permission dial chip carried in the left half.
|
||||
// ---------------------------------------------------------------------------
|
||||
// The posture bar (SHELL-DESIGN-20260901 §2.0 item 3, §2.3b; founder
|
||||
// direction 2026-09-02): the first row under the composer, in Claude Code's
|
||||
// grammar —
|
||||
//
|
||||
// ● full access Shift+Tab to change work (Tab) 2 agents, 1 task Esc to interrupt rc connected
|
||||
// ● full access Shift+Tab to change 2 agents, 1 task Esc to interrupt rc connected
|
||||
//
|
||||
// permission chip first (never sheds, #5796), marked `●` so the current
|
||||
// permission reads without color (experience mark 8) and followed by what
|
||||
// its key does, then the mode, the turn clock, the
|
||||
// the permission dial chip first (never sheds, #5796), marked `●` so the
|
||||
// current stop reads without color (experience mark 8) and followed by what
|
||||
// its key does, then the turn clock, the
|
||||
// live counts, the session clock, then the one hint that applies right now;
|
||||
// the remote-control state or a live notice pinned right. No cost: the
|
||||
// roster owns per-agent elapsed and the metrics line owns the price. The
|
||||
@@ -635,7 +635,7 @@ mod tests {
|
||||
// classic footer until `146ab7f756` deleted that path, and the phase band's
|
||||
// `working_detail` until `329960fcbf` (the 0.9.12 mega shell) merged the
|
||||
// bands and left the elapsed reading to the transcript's active row. A
|
||||
// multi-hour operate session scrolls that row out of sight, so the founder
|
||||
// multi-hour session scrolls that row out of sight, so the founder
|
||||
// looking straight at the screen had no way to tell how long the session had
|
||||
// been working or whether the current turn was stuck. The fixed row is where
|
||||
// a glancing user looks; the clock lives here.
|
||||
@@ -648,17 +648,14 @@ const DEPTH_WARN: &str = "surface soon — /compact";
|
||||
/// What the caller owes the posture bar. All injected, deterministic.
|
||||
pub struct TidelineFooter<'a> {
|
||||
pub theme: &'a codewhale_palette::UiTheme,
|
||||
/// Permission chip (`ask` / `auto` / `full access`, plus the filesystem
|
||||
/// scope notice when it deviates) in its Permission ink. Never sheds.
|
||||
/// Permission dial chip (`ask` / `accept edits` / `plan` / `auto` /
|
||||
/// `Full Access`, plus the filesystem scope notice when it deviates) in
|
||||
/// its ink. Never sheds.
|
||||
pub permission_chip: (&'a str, codewhale_palette::ChromeInk),
|
||||
/// What the permission key does, when the binding is live for the
|
||||
/// current focus and not yet learned (`Shift+Tab to change`). Painted
|
||||
/// after the chip in hint ink.
|
||||
pub permission_key: Option<&'a str>,
|
||||
/// Mode chip (`work` / `plan` / `operate`) in its Policy ink.
|
||||
pub mode_chip: Option<(&'a str, codewhale_palette::ChromeInk)>,
|
||||
/// The chord that cycles the mode, when the binding is live (`Tab`).
|
||||
pub mode_key: Option<&'a str>,
|
||||
/// The turn half of the working clock (`working 1m 15s`): what the
|
||||
/// session is doing right now and for how long. `None` between turns.
|
||||
pub turn_clock: Option<(&'a str, codewhale_palette::ChromeInk)>,
|
||||
@@ -683,7 +680,7 @@ pub struct TidelineFooter<'a> {
|
||||
pub ascii_safe: bool,
|
||||
/// `tui.posture_bar = "compact"` (#5950): start the shed ladder at
|
||||
/// the kit's compact rung instead of rung 0, so the row states its posture —
|
||||
/// the permission and mode chips, and the cap warning when it is owed —
|
||||
/// the permission dial chip, and the cap warning when it is owed —
|
||||
/// and nothing live. Width sheds the rest exactly as it always did.
|
||||
pub compact: bool,
|
||||
working_glow: Option<(
|
||||
@@ -703,8 +700,6 @@ impl<'a> TidelineFooter<'a> {
|
||||
theme,
|
||||
permission_chip,
|
||||
permission_key: None,
|
||||
mode_chip: None,
|
||||
mode_key: None,
|
||||
turn_clock: None,
|
||||
counts: &[],
|
||||
session_clock: None,
|
||||
@@ -723,18 +718,6 @@ impl<'a> TidelineFooter<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mode_chip(mut self, chip: Option<(&'a str, codewhale_palette::ChromeInk)>) -> Self {
|
||||
self.mode_chip = chip;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn mode_key(mut self, key: Option<&'a str>) -> Self {
|
||||
self.mode_key = key;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn turn_clock(mut self, clock: Option<(&'a str, codewhale_palette::ChromeInk)>) -> Self {
|
||||
self.turn_clock = clock;
|
||||
@@ -812,8 +795,6 @@ impl<'a> TidelineFooter<'a> {
|
||||
.cap_warning(DEPTH_WARN)
|
||||
.compact(self.compact);
|
||||
bar.permission_key = self.permission_key.map(Into::into);
|
||||
bar.mode = self.mode_chip.map(posture_fact);
|
||||
bar.mode_key = self.mode_key.map(Into::into);
|
||||
bar.turn_clock = self.turn_clock.map(posture_fact);
|
||||
bar.counts = self
|
||||
.counts
|
||||
@@ -881,8 +862,6 @@ pub(crate) struct TidelineFooterFacts {
|
||||
/// `Shift+Tab to change`, localized, while the binding is live and not
|
||||
/// yet learned.
|
||||
pub permission_key: Option<String>,
|
||||
pub mode_chip: Option<(String, codewhale_palette::ChromeInk)>,
|
||||
pub mode_key: Option<&'static str>,
|
||||
pub turn_clock: ClockReading,
|
||||
pub counts: Vec<(String, ChromeInk)>,
|
||||
pub session_clock: ClockReading,
|
||||
@@ -909,8 +888,6 @@ impl TidelineFooterFacts {
|
||||
(self.permission_chip.0.as_str(), self.permission_chip.1),
|
||||
)
|
||||
.permission_key(self.permission_key.as_deref())
|
||||
.mode_chip(borrow(&self.mode_chip))
|
||||
.mode_key(self.mode_key)
|
||||
.turn_clock(borrow(&self.turn_clock))
|
||||
.counts(&self.counts)
|
||||
.session_clock(borrow(&self.session_clock))
|
||||
@@ -1079,20 +1056,8 @@ pub(crate) fn tideline_footer_from_app(app: &mut App, width: u16) -> TidelineFoo
|
||||
let tier = ShellTier::for_chrome_width(width);
|
||||
let focus = app.focus();
|
||||
|
||||
let (mode_chip, permission_chip) = crate::tui::underwater::posture_chips(app);
|
||||
let permission_chip = permission_chip
|
||||
.map(|(text, ink)| (text.into_owned(), ink))
|
||||
.unwrap_or_else(|| (String::new(), ChromeInk::PermissionAsk));
|
||||
// The mode chip is the one posture fact `/statusline` composes (#5950):
|
||||
// its `StatusItem::Mode` toggle used to be inert. The permission chip,
|
||||
// the working clocks (#5914) and the live counts are the bar's own
|
||||
// posture statement and stay unconditional.
|
||||
let mode_chip = mode_chip
|
||||
.filter(|_| {
|
||||
app.status_items
|
||||
.contains(&codewhale_runtime::config::StatusItem::Mode)
|
||||
})
|
||||
.map(|(text, ink)| (text.into_owned(), ink));
|
||||
let (dial_text, dial_ink) = crate::tui::underwater::dial_chip(app);
|
||||
let permission_chip = (dial_text.into_owned(), dial_ink);
|
||||
// Cycle keys come from the binding table and only when that binding is
|
||||
// live for the current focus.
|
||||
let live_chord = |id: ShellBindingId| -> Option<&'static str> {
|
||||
@@ -1222,13 +1187,6 @@ pub(crate) fn tideline_footer_from_app(app: &mut App, width: u16) -> TidelineFoo
|
||||
.map(|chord| {
|
||||
tr(app.ui_locale, MessageId::FooterPermissionKeyHint).replace("{key}", chord)
|
||||
}),
|
||||
mode_chip,
|
||||
mode_key: live_chord(ShellBindingId::ModeCycle).filter(|_| {
|
||||
!crate::tui::footer_hints::retired(
|
||||
&app.footer_hint_uses,
|
||||
crate::tui::footer_hints::MODE_CYCLE,
|
||||
)
|
||||
}),
|
||||
turn_clock,
|
||||
counts,
|
||||
session_clock,
|
||||
|
||||
@@ -52,8 +52,6 @@ fn posture_retains_every_host_ink_including_the_pinned_right_fact() {
|
||||
let counts = [("C (Ctrl+])".into(), ink)];
|
||||
let footer = TidelineFooter::new(&theme, ("P", ink))
|
||||
.permission_key(Some("K"))
|
||||
.mode_chip(Some(("M", ink)))
|
||||
.mode_key(Some("Tab"))
|
||||
.turn_clock(Some(("W", ink)))
|
||||
.counts(&counts)
|
||||
.session_clock(Some(("S", ink)))
|
||||
@@ -77,7 +75,7 @@ fn posture_retains_every_host_ink_including_the_pinned_right_fact() {
|
||||
.find(|x| buf[(*x, area.y)].symbol() == symbol)
|
||||
.unwrap_or_else(|| panic!("{ink:?}: {symbol} did not paint"))
|
||||
};
|
||||
for symbol in ["P", "M", "W", "C", "S", "H", "R"] {
|
||||
for symbol in ["P", "W", "C", "S", "H", "R"] {
|
||||
let cell = &buf[(column(symbol), area.y)];
|
||||
assert_eq!(cell.fg, ink.color(&theme), "{ink:?}: {symbol}");
|
||||
assert_eq!(cell.bg, Color::Rgb(30, 40, 50));
|
||||
@@ -85,10 +83,9 @@ fn posture_retains_every_host_ink_including_the_pinned_right_fact() {
|
||||
assert_eq!(cell.modifier.contains(Modifier::BOLD), symbol == "P");
|
||||
}
|
||||
assert_eq!(buf[(column("K"), area.y)].fg, theme.text_hint);
|
||||
assert_eq!(buf[(column("M") + 2, area.y)].fg, theme.text_hint);
|
||||
assert_eq!(targets, [(0, Rect::new(column("C"), area.y, 10, 1))]);
|
||||
assert_eq!(buf[(column("C") + 2, area.y)].fg, theme.text_hint);
|
||||
assert_eq!(buf[(column("M") - 1, area.y)].fg, theme.text_dim);
|
||||
assert_eq!(buf[(column("W") - 1, area.y)].fg, theme.text_dim);
|
||||
for position in [(3, 2), (102, 2), (60, 2), (3, 3), (109, 4)] {
|
||||
assert_eq!(buf[position], before[position], "{ink:?}: {position:?}");
|
||||
}
|
||||
|
||||
@@ -13,8 +13,6 @@ use codewhale_palette::UI_THEME;
|
||||
struct Fixture {
|
||||
permission: (&'static str, ChromeInk),
|
||||
permission_key: Option<&'static str>,
|
||||
mode: Option<(&'static str, ChromeInk)>,
|
||||
mode_key: Option<&'static str>,
|
||||
turn_clock: Option<(&'static str, ChromeInk)>,
|
||||
counts: Vec<(String, ChromeInk)>,
|
||||
session_clock: Option<(&'static str, ChromeInk)>,
|
||||
@@ -23,13 +21,11 @@ struct Fixture {
|
||||
right: Option<(&'static str, ChromeInk)>,
|
||||
}
|
||||
|
||||
/// A working turn with two sub-agents, ask posture, work mode.
|
||||
/// A working turn with two sub-agents on the Ask dial stop.
|
||||
fn working() -> Fixture {
|
||||
Fixture {
|
||||
permission: ("ask", ChromeInk::PermissionAsk),
|
||||
permission_key: Some("Shift+Tab to change"),
|
||||
mode: Some(("work", ChromeInk::PolicyAct)),
|
||||
mode_key: Some("Tab"),
|
||||
turn_clock: Some(("working 1m 15s", ChromeInk::Active)),
|
||||
counts: vec![("2 agents".to_string(), ChromeInk::Active)],
|
||||
session_clock: Some(("worked 41m 12s", ChromeInk::Active)),
|
||||
@@ -43,8 +39,6 @@ impl Fixture {
|
||||
fn widget<'a>(&'a self, theme: &'a codewhale_palette::UiTheme) -> TidelineFooter<'a> {
|
||||
TidelineFooter::new(theme, self.permission)
|
||||
.permission_key(self.permission_key)
|
||||
.mode_chip(self.mode)
|
||||
.mode_key(self.mode_key)
|
||||
.turn_clock(self.turn_clock)
|
||||
.counts(&self.counts)
|
||||
.session_clock(self.session_clock)
|
||||
@@ -74,16 +68,15 @@ fn footer_matches_goldens_at_blocker_sizes() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission chip with its cycle key, mode
|
||||
/// with its cycle key, the working clock, live counts, then the one hint
|
||||
/// that applies now.
|
||||
/// The dial chip with its key, the working clock, live counts, then the one
|
||||
/// hint that applies now.
|
||||
#[test]
|
||||
fn posture_bar_reads_permission_mode_clock_counts_hint() {
|
||||
fn posture_bar_reads_dial_clock_counts_hint() {
|
||||
let text = draw(120, 30, &working().widget(&UI_THEME));
|
||||
let band = text.lines().last().unwrap_or_default().trim_end();
|
||||
assert_eq!(
|
||||
band,
|
||||
" ● ask Shift+Tab to change work (Tab) working 1m 15s 2 agents worked 41m 12s Esc to interrupt"
|
||||
" ● ask Shift+Tab to change working 1m 15s 2 agents worked 41m 12s Esc to interrupt"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,12 +174,12 @@ fn posture_bar_pins_notice_or_remote_control_right() {
|
||||
|
||||
/// Shed ladder, most expendable first: the turn clock, the session clock,
|
||||
/// the permission key (a binding reminder, and the widest optional item),
|
||||
/// the hint, the counts, mode key, mode. The permission chip
|
||||
/// the hint, the counts. The permission chip
|
||||
/// never sheds (#5796); the clock is what a glance wants and the hint and
|
||||
/// counts are what a keystroke wants, so on a row too narrow for both the
|
||||
/// clock goes (#5914).
|
||||
#[test]
|
||||
fn posture_bar_sheds_the_clocks_then_the_hint_counts_and_posture_chips() {
|
||||
fn posture_bar_sheds_the_clocks_then_the_hint_and_counts() {
|
||||
let fixture = working();
|
||||
let narrowest_showing = |needle: &str| -> u16 {
|
||||
(8..=160u16)
|
||||
@@ -198,19 +191,13 @@ fn posture_bar_sheds_the_clocks_then_the_hint_counts_and_posture_chips() {
|
||||
let session_clock = narrowest_showing("worked 41m 12s");
|
||||
let hint = narrowest_showing("Esc to interrupt");
|
||||
let counts = narrowest_showing("2 agents");
|
||||
// `work` alone would also match `working`, so measure the mode chip by
|
||||
// a needle only the mode paints.
|
||||
let mode_key = narrowest_showing("work (Tab)");
|
||||
let mode = narrowest_showing(" work ");
|
||||
let permission_key = narrowest_showing("Shift+Tab to change");
|
||||
assert!(
|
||||
turn_clock > session_clock
|
||||
&& session_clock > permission_key
|
||||
&& permission_key > hint
|
||||
&& hint > counts
|
||||
&& counts > mode_key
|
||||
&& mode_key > mode,
|
||||
"turn_clock@{turn_clock} session_clock@{session_clock} hint@{hint} counts@{counts} mode_key@{mode_key} mode@{mode} permission_key@{permission_key}"
|
||||
&& hint > counts,
|
||||
"turn_clock@{turn_clock} session_clock@{session_clock} hint@{hint} counts@{counts} permission_key@{permission_key}"
|
||||
);
|
||||
for w in 8..=160u16 {
|
||||
let text = draw(w, 3, &fixture.widget(&UI_THEME));
|
||||
@@ -218,17 +205,12 @@ fn posture_bar_sheds_the_clocks_then_the_hint_counts_and_posture_chips() {
|
||||
text.contains("ask"),
|
||||
"{w}: the permission chip never sheds: {text}"
|
||||
);
|
||||
// Whole chips or none — a clipped posture word is worse than none.
|
||||
assert!(
|
||||
!text.contains("(Ta") || text.contains("(Tab)"),
|
||||
"{w}: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `tui.posture_bar = "compact"` (#5950) starts the ladder past the clocks,
|
||||
/// the hint and the counts at any width: the row states its posture — the
|
||||
/// permission and mode chips, and the cap warning when it is owed — and
|
||||
/// dial chip, and the cap warning when it is owed — and
|
||||
/// nothing live. Width still sheds from there, and the right slot is
|
||||
/// untouched.
|
||||
#[test]
|
||||
@@ -236,7 +218,7 @@ fn compact_posture_bar_states_posture_and_nothing_live() {
|
||||
let mut fixture = working();
|
||||
fixture.right = Some(("/rc connected", ChromeInk::Info));
|
||||
let wide = draw(160, 3, &fixture.widget(&UI_THEME).compact(true));
|
||||
for kept in [" ● ask", " work (Tab)", "/rc connected"] {
|
||||
for kept in [" ● ask", "/rc connected"] {
|
||||
assert!(wide.contains(kept), "compact keeps {kept}: {wide}");
|
||||
}
|
||||
for gone in [
|
||||
@@ -271,39 +253,14 @@ fn compact_posture_bar_states_posture_and_nothing_live() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Permission outranks mode when only one posture chip fits: the longest
|
||||
/// mode word must never displace `full access`.
|
||||
#[test]
|
||||
fn posture_bar_permission_outranks_mode_when_only_one_fits() {
|
||||
let mut fixture = working();
|
||||
fixture.permission = ("full access", ChromeInk::PermissionFullAccess);
|
||||
fixture.mode = Some(("operate", ChromeInk::PolicyOperate));
|
||||
let mut saw_permission_alone = false;
|
||||
for width in 8..=120u16 {
|
||||
let text = draw(width, 12, &fixture.widget(&UI_THEME));
|
||||
let has_mode = text.contains("operate");
|
||||
let has_permission = text.contains("full access");
|
||||
assert!(
|
||||
!(has_mode && !has_permission),
|
||||
"width {width} kept the mode word and shed the permission phrase: {text}"
|
||||
);
|
||||
if has_permission && !has_mode {
|
||||
saw_permission_alone = true;
|
||||
}
|
||||
}
|
||||
assert!(saw_permission_alone, "no width shed the mode word alone");
|
||||
}
|
||||
|
||||
/// The cycle keys print only when the caller says the binding is live —
|
||||
/// the launch stage's Tab moves focus, so the mode chip there has no key.
|
||||
/// The dial key prints only when the caller says the binding is live.
|
||||
#[test]
|
||||
fn posture_bar_prints_cycle_keys_only_when_live() {
|
||||
let mut fixture = working();
|
||||
fixture.mode_key = None;
|
||||
fixture.permission_key = None;
|
||||
let text = draw(120, 30, &fixture.widget(&UI_THEME));
|
||||
assert!(
|
||||
text.contains(" ● ask work working 1m 15s 2 agents worked 41m 12s"),
|
||||
text.contains(" ● ask working 1m 15s 2 agents worked 41m 12s"),
|
||||
"{text}"
|
||||
);
|
||||
assert!(!text.contains('('), "{text}");
|
||||
@@ -341,9 +298,7 @@ fn posture_bar_degenerate_sizes_do_not_panic() {
|
||||
|
||||
use super::tideline_footer_from_app;
|
||||
use crate::tui::app::{App, OnboardingState};
|
||||
use crate::tui::footer_hints::{
|
||||
AGENT_ARROWS, ENTER_AGAIN, ESC_INTERRUPT, MODE_CYCLE, PERMISSION_CYCLE,
|
||||
};
|
||||
use crate::tui::footer_hints::{AGENT_ARROWS, ENTER_AGAIN, ESC_INTERRUPT, PERMISSION_CYCLE};
|
||||
|
||||
fn session_app() -> App {
|
||||
let mut app = crate::tui::test_support::test_app_with_options(
|
||||
@@ -362,44 +317,31 @@ fn set_uses(app: &mut App, key: &str, uses: u8) {
|
||||
}
|
||||
}
|
||||
|
||||
/// The cycle chords print while their bindings are fresh, and the chips go
|
||||
/// bare — never away — once each binding has been used twice.
|
||||
/// The dial chord prints while its binding is fresh, and the chip goes
|
||||
/// bare — never away — once it has been used twice. The Plan stop is the
|
||||
/// chip itself: there is no separate mode chip or Tab hint.
|
||||
#[test]
|
||||
fn cycle_keys_show_at_zero_and_one_use_and_go_bare_at_two() {
|
||||
let mut app = session_app();
|
||||
app.approval_mode = codewhale_execpolicy::ApprovalMode::Suggest;
|
||||
let facts = tideline_footer_from_app(&mut app, 120);
|
||||
assert_eq!(facts.permission_key.as_deref(), Some("Shift+Tab to change"));
|
||||
assert_eq!(facts.mode_key, Some("Tab"));
|
||||
|
||||
for key in [PERMISSION_CYCLE, MODE_CYCLE] {
|
||||
set_uses(&mut app, key, 1);
|
||||
}
|
||||
set_uses(&mut app, PERMISSION_CYCLE, 1);
|
||||
let facts = tideline_footer_from_app(&mut app, 120);
|
||||
assert_eq!(facts.permission_key.as_deref(), Some("Shift+Tab to change"));
|
||||
assert_eq!(facts.mode_key, Some("Tab"));
|
||||
|
||||
set_uses(&mut app, PERMISSION_CYCLE, 2);
|
||||
let facts = tideline_footer_from_app(&mut app, 120);
|
||||
assert_eq!(facts.permission_key, None);
|
||||
assert_eq!(
|
||||
facts.mode_key,
|
||||
Some("Tab"),
|
||||
"the mode key retires on its own count"
|
||||
);
|
||||
|
||||
set_uses(&mut app, MODE_CYCLE, 2);
|
||||
let facts = tideline_footer_from_app(&mut app, 120);
|
||||
assert_eq!(facts.permission_key, None);
|
||||
assert_eq!(facts.mode_key, None);
|
||||
assert!(
|
||||
!facts.permission_chip.0.is_empty(),
|
||||
"the permission chip never retires with its key"
|
||||
);
|
||||
assert!(
|
||||
facts.mode_chip.is_some(),
|
||||
"the mode chip never retires with its key"
|
||||
);
|
||||
|
||||
app.mode = codewhale_config::AppMode::Plan;
|
||||
let facts = tideline_footer_from_app(&mut app, 120);
|
||||
assert_eq!(facts.permission_chip.0, "plan");
|
||||
}
|
||||
|
||||
/// A running turn advertises the interrupt affordance until Esc has been
|
||||
|
||||
@@ -632,14 +632,16 @@ impl SetupRuntimePreset {
|
||||
|
||||
pub fn permission_posture(self) -> &'static str {
|
||||
match self {
|
||||
Self::AskFirst | Self::NormalAgent => "ask",
|
||||
Self::AskFirst => "ask",
|
||||
Self::NormalAgent => "accept-edits",
|
||||
Self::HighTrustLocal => "full-access",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn approval_policy(self) -> Option<&'static str> {
|
||||
match self {
|
||||
Self::AskFirst | Self::NormalAgent => Some("on-request"),
|
||||
Self::AskFirst => Some("on-request"),
|
||||
Self::NormalAgent => Some("accept-edits"),
|
||||
// Full Access lives in TUI settings; it is intentionally not a
|
||||
// top-level approval_policy value.
|
||||
Self::HighTrustLocal => None,
|
||||
|
||||
@@ -150,9 +150,7 @@ pub enum ShellBindingId {
|
||||
Help,
|
||||
ExternalEditor,
|
||||
Settings,
|
||||
/// Tab: cycle the session mode.
|
||||
ModeCycle,
|
||||
/// Shift+Tab: cycle the permission posture.
|
||||
/// Shift+Tab: step the permission dial.
|
||||
PermissionCycle,
|
||||
/// Ctrl+Tab / Ctrl+]: the next bottom-dock view, or the next pet view
|
||||
/// while the pet main view is showing.
|
||||
@@ -249,7 +247,6 @@ impl ShellBinding {
|
||||
ShellBindingId::Help => is_help_shortcut(key),
|
||||
ShellBindingId::ExternalEditor => key_shortcuts::is_external_editor_shortcut(key),
|
||||
ShellBindingId::Settings => is_settings_shortcut(key),
|
||||
ShellBindingId::ModeCycle => is_mode_cycle_shortcut(key),
|
||||
ShellBindingId::PermissionCycle => is_permission_cycle_shortcut(key),
|
||||
ShellBindingId::ViewCycle => is_view_cycle_shortcut(key),
|
||||
ShellBindingId::ViewCycleBack => is_view_cycle_back_shortcut(key),
|
||||
@@ -270,11 +267,7 @@ pub fn route(focus: Focus, key: &KeyEvent) -> Option<ShellBindingId> {
|
||||
.find(|binding| {
|
||||
binding.focus.admits(focus)
|
||||
&& binding.matches(key)
|
||||
&& !(focus == Focus::TerminalPanel
|
||||
&& matches!(
|
||||
binding.id,
|
||||
ShellBindingId::ModeCycle | ShellBindingId::PermissionCycle
|
||||
))
|
||||
&& !(focus == Focus::TerminalPanel && binding.id == ShellBindingId::PermissionCycle)
|
||||
})
|
||||
.map(|binding| binding.id)
|
||||
}
|
||||
@@ -520,17 +513,6 @@ pub const SHELL_BINDINGS: &[ShellBinding] = &[
|
||||
// disappears inside onboarding or a modal is not a route.
|
||||
focus: FocusScope::Everywhere,
|
||||
},
|
||||
ShellBinding {
|
||||
id: ShellBindingId::ModeCycle,
|
||||
catalog_chord: "Tab",
|
||||
footer_chord: "Tab",
|
||||
// Tab is the shell's mode cycle. The composer's own completions
|
||||
// get the key first, but *having typed* never disables it. Live on
|
||||
// the launch screen too: the card's rows are arrowed, the composer
|
||||
// is always focused, so Tab had no focus left to move and read as
|
||||
// dead (0.9.12 defect #5).
|
||||
focus: FocusScope::AnyShell,
|
||||
},
|
||||
ShellBinding {
|
||||
id: ShellBindingId::PermissionCycle,
|
||||
catalog_chord: "Shift+Tab",
|
||||
@@ -721,18 +703,7 @@ pub fn is_settings_shortcut(key: &KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::F(2)) && key.modifiers.is_empty()
|
||||
}
|
||||
|
||||
/// Tab cycles the session mode. Terminal chords that mean something else to
|
||||
/// the host (Ctrl/Alt/Cmd+Tab) are not ours, and Shift+Tab is the permission
|
||||
/// cycle below.
|
||||
#[must_use]
|
||||
pub fn is_mode_cycle_shortcut(key: &KeyEvent) -> bool {
|
||||
matches!(key.code, KeyCode::Tab)
|
||||
&& !key.modifiers.intersects(
|
||||
KeyModifiers::CONTROL | KeyModifiers::ALT | KeyModifiers::SUPER | KeyModifiers::SHIFT,
|
||||
)
|
||||
}
|
||||
|
||||
/// Shift+Tab cycles the permission posture. Terminals encode the same chord
|
||||
/// Shift+Tab steps the permission dial. Terminals encode the same chord
|
||||
/// either as `BackTab` or as `Tab` + SHIFT; accept both.
|
||||
#[must_use]
|
||||
pub fn is_permission_cycle_shortcut(key: &KeyEvent) -> bool {
|
||||
@@ -798,13 +769,13 @@ mod tests {
|
||||
route(Focus::Composer, &ctrl_shift_tab),
|
||||
Some(ShellBindingId::ViewCycleBack)
|
||||
);
|
||||
// Plain Tab / Shift+Tab stay the mode and permission cycles.
|
||||
// Plain Tab is composer completion only; Shift+Tab is the dial.
|
||||
assert_eq!(
|
||||
route(
|
||||
Focus::Composer,
|
||||
&KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)
|
||||
),
|
||||
Some(ShellBindingId::ModeCycle)
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
route(
|
||||
@@ -1095,16 +1066,12 @@ mod tests {
|
||||
fn tab_and_shift_tab_are_distinct_bindings() {
|
||||
let tab = KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE);
|
||||
let shift_tab = KeyEvent::new(KeyCode::Tab, KeyModifiers::SHIFT);
|
||||
assert_eq!(
|
||||
route(Focus::Composer, &tab),
|
||||
Some(ShellBindingId::ModeCycle)
|
||||
);
|
||||
assert_eq!(route(Focus::Composer, &tab), None);
|
||||
assert_eq!(
|
||||
route(Focus::Composer, &shift_tab),
|
||||
Some(ShellBindingId::PermissionCycle)
|
||||
);
|
||||
// Both shell controls are live on the launch stage as well.
|
||||
assert_eq!(route(Focus::Launch, &tab), Some(ShellBindingId::ModeCycle));
|
||||
assert_eq!(route(Focus::Launch, &tab), None);
|
||||
assert_eq!(
|
||||
route(Focus::Launch, &shift_tab),
|
||||
Some(ShellBindingId::PermissionCycle)
|
||||
@@ -1218,7 +1185,6 @@ mod tests {
|
||||
ShellBindingId::ProviderRoute,
|
||||
ShellBindingId::Help,
|
||||
ShellBindingId::Settings,
|
||||
ShellBindingId::ModeCycle,
|
||||
ShellBindingId::PermissionCycle,
|
||||
ShellBindingId::ViewCycle,
|
||||
ShellBindingId::ViewCycleBack,
|
||||
|
||||
@@ -753,7 +753,7 @@ pub(crate) fn apply_goal_snapshot_to_app(app: &mut App, snapshot: &GoalSnapshot)
|
||||
/// Apply an explicit mode selection from a user shortcut (Alt+A/P/Y).
|
||||
///
|
||||
/// Uses `select_mode`, not `set_mode`, so an explicitly chosen mode is also the
|
||||
/// startup default next launch – matching the Tab cycle and hotbar paths.
|
||||
/// startup default next launch – matching the permission dial and hotbar paths.
|
||||
pub(crate) async fn apply_mode_update(
|
||||
app: &mut App,
|
||||
engine_handle: &EngineHandle,
|
||||
|
||||
@@ -727,25 +727,16 @@ pub(crate) enum TabDispatch {
|
||||
Completion,
|
||||
/// Nobody owns Tab in this focus state.
|
||||
Ignored,
|
||||
/// The session mode cycled. The caller syncs the engine.
|
||||
ModeCycled {
|
||||
prior_mode: AppMode,
|
||||
prior_model: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// Tab dispatch, lifted out of the event loop body so a test can press Tab.
|
||||
///
|
||||
/// The composer's completions get the key first: a mention menu, a slash
|
||||
/// menu, an in-progress command or file mention, a waiting prompt
|
||||
/// suggestion. Those are genuine composer *editing* questions about the
|
||||
/// text. Once none of them claims the key, Tab is the shell's mode cycle,
|
||||
/// admitted by [`App::focus`] alone — whether the composer holds text is not
|
||||
/// part of that decision. It used to be: `if !app.input.is_empty()
|
||||
/// { continue; }` killed Tab the moment the user typed anything.
|
||||
/// suggestion. Tab never changes the mode: the permission dial on Shift+Tab
|
||||
/// owns that.
|
||||
pub(crate) fn dispatch_tab_key(
|
||||
app: &mut App,
|
||||
key: &KeyEvent,
|
||||
mention_menu_entries: &[String],
|
||||
slash_menu_entries: &[crate::tui::widgets::SlashMenuEntry],
|
||||
) -> TabDispatch {
|
||||
@@ -771,19 +762,7 @@ pub(crate) fn dispatch_tab_key(
|
||||
app.needs_redraw = true;
|
||||
return TabDispatch::Completion;
|
||||
}
|
||||
if shell_binding_for_key(app, key) != Some(ShellBindingId::ModeCycle) {
|
||||
return TabDispatch::Ignored;
|
||||
}
|
||||
// Sending or queueing input is reserved for Enter, so Tab never changes
|
||||
// roles based on whether a turn happens to be running.
|
||||
let prior_model = app.model.clone();
|
||||
let prior_mode = app.mode;
|
||||
app.cycle_mode();
|
||||
app.note_footer_hint_used(crate::tui::footer_hints::MODE_CYCLE);
|
||||
TabDispatch::ModeCycled {
|
||||
prior_mode,
|
||||
prior_model,
|
||||
}
|
||||
TabDispatch::Ignored
|
||||
}
|
||||
|
||||
/// Whether a mouse event is a wheel/trackpad scroll in any direction.
|
||||
@@ -1154,7 +1133,7 @@ pub async fn run_tui(
|
||||
&& !app.onboarding_missing_key_recovery
|
||||
{
|
||||
// The one-time Fleet intro is no longer a launch push: it appears the
|
||||
// first time the user opens `/fleet` or enters Operate (apply.rs).
|
||||
// first time the user opens `/fleet` (apply.rs).
|
||||
let _ = open_setup_checkpoint_if_due(&mut app, config, options.skip_onboarding);
|
||||
}
|
||||
|
||||
@@ -1464,7 +1443,7 @@ pub async fn run_tui(
|
||||
// Join the startup-default writer before anything else tears down.
|
||||
//
|
||||
// The last thing a user does before quitting is very often the selection
|
||||
// they most want to survive — Tab into Operate, then Ctrl+C. Those writes
|
||||
// they most want to survive — Shift+Tab into Plan, then Ctrl+C. Those writes
|
||||
// are queued off the event loop on purpose, so at this point one may still
|
||||
// be in flight or not yet started. Draining here is what makes "the last
|
||||
// immediate selection lands" true rather than a race against process exit.
|
||||
@@ -7455,26 +7434,8 @@ pub(crate) async fn run_event_loop(
|
||||
app.scroll_down(page);
|
||||
}
|
||||
KeyCode::Tab => {
|
||||
match dispatch_tab_key(app, &key, &mention_menu_entries, &slash_menu_entries) {
|
||||
TabDispatch::Completion | TabDispatch::Ignored => continue,
|
||||
TabDispatch::ModeCycled {
|
||||
prior_mode,
|
||||
prior_model,
|
||||
} => {
|
||||
if app.mode != prior_mode {
|
||||
sync_mode_update(app, &engine_handle).await;
|
||||
}
|
||||
if app.model != prior_model {
|
||||
let _ = engine_handle
|
||||
.send(Op::SetModel {
|
||||
model: app.model.clone(),
|
||||
mode: app.mode,
|
||||
route_limits: app.active_route_limits,
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
dispatch_tab_key(app, &mention_menu_entries, &slash_menu_entries);
|
||||
continue;
|
||||
}
|
||||
// Transcript-nav shortcuts now require Alt, leaving most bare
|
||||
// letters free to insert as text. Requiring Alt is also why
|
||||
|
||||
@@ -147,7 +147,7 @@ fn count_rows_containing(rows: &[String], needle: &str) -> usize {
|
||||
}
|
||||
|
||||
/// Every chrome fact paints in exactly one row of the composed default
|
||||
/// frame: the context reading, the mode and permission chips, the model,
|
||||
/// frame: the context reading, the permission dial chip, the model,
|
||||
/// the cost, the agent count, and the help hint.
|
||||
///
|
||||
/// 160 columns joins the blocker sizes so both working-clock halves can
|
||||
@@ -163,13 +163,10 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() {
|
||||
let rows = draw(&mut app, width, height);
|
||||
let pct = super::info_context_percent(&app);
|
||||
let (_, model) = app.effective_route_identity_display();
|
||||
let (mode, permission) = crate::tui::underwater::posture_chips(&app);
|
||||
let mode = mode.expect("mode chip").0.into_owned();
|
||||
let permission = permission.expect("permission chip").0.into_owned();
|
||||
let permission = crate::tui::underwater::dial_chip(&app).0.into_owned();
|
||||
// The context reading paints exactly once, at every fullness
|
||||
// (#5950 — it used to go silent below 50%).
|
||||
let mut facts = vec![
|
||||
("mode chip", format!(" {mode} (")),
|
||||
// Mark 8: the permission in force is marked `●` and followed by
|
||||
// what its key does, not a parenthesised chord.
|
||||
("permission chip", format!("● {permission}")),
|
||||
@@ -442,9 +439,8 @@ fn row_presets_reclaim_rows_and_quiet_them_in_the_composed_frame() {
|
||||
let posture = posture_row(&rows).expect("compact paints the posture bar");
|
||||
let metrics = metrics_row(&rows).expect("compact paints the metrics line");
|
||||
assert_eq!(metrics, posture + 1);
|
||||
let (mode, permission) = crate::tui::underwater::posture_chips(&app);
|
||||
assert!(rows[posture].contains(permission.expect("permission chip").0.as_ref()));
|
||||
assert!(rows[posture].contains(mode.expect("mode chip").0.as_ref()));
|
||||
let permission = crate::tui::underwater::dial_chip(&app);
|
||||
assert!(rows[posture].contains(permission.0.as_ref()));
|
||||
for gone in ["working", "2 agents", "Esc to interrupt"] {
|
||||
assert!(
|
||||
!rows[posture].contains(gone),
|
||||
|
||||
@@ -208,15 +208,12 @@ pub(crate) async fn cycle_permission_posture(
|
||||
app.config_profile.as_deref(),
|
||||
&app.workspace,
|
||||
);
|
||||
let changed = if control == codewhale_runtime::config::ApprovalPolicyControl::RootConfig {
|
||||
app.cycle_root_approval_posture()
|
||||
} else {
|
||||
app.cycle_approval_posture()
|
||||
};
|
||||
if changed {
|
||||
if control == codewhale_runtime::config::ApprovalPolicyControl::RootConfig {
|
||||
config.approval_policy = None;
|
||||
}
|
||||
let root_control = control == codewhale_runtime::config::ApprovalPolicyControl::RootConfig;
|
||||
let step = app.step_permission_dial(root_control);
|
||||
if step.posture_changed && root_control {
|
||||
config.approval_policy = None;
|
||||
}
|
||||
if step.changed() {
|
||||
sync_mode_update(app, engine_handle).await;
|
||||
refresh_config_view_if_open(app, "permission_posture");
|
||||
}
|
||||
|
||||
@@ -1133,7 +1133,6 @@ fn shell_binding_probe(id: ShellBindingId) -> KeyEvent {
|
||||
ShellBindingId::ExternalEditor => KeyEvent::new(KeyCode::F(4), KeyModifiers::NONE),
|
||||
ShellBindingId::ProviderOffer => KeyEvent::new(KeyCode::F(7), KeyModifiers::NONE),
|
||||
ShellBindingId::Settings => KeyEvent::new(KeyCode::F(2), KeyModifiers::NONE),
|
||||
ShellBindingId::ModeCycle => KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
|
||||
ShellBindingId::PermissionCycle => KeyEvent::new(KeyCode::BackTab, KeyModifiers::NONE),
|
||||
ShellBindingId::ViewCycle => KeyEvent::new(KeyCode::Char(']'), KeyModifiers::CONTROL),
|
||||
ShellBindingId::ViewCycleBack => KeyEvent::new(
|
||||
@@ -1183,8 +1182,8 @@ fn no_shell_binding_changes_meaning_once_the_composer_has_text() {
|
||||
.focus
|
||||
.admits(crate::tui::shell_key_routing::Focus::Composer)
|
||||
{
|
||||
// A chord may serve a different owner: Tab cycles the composer's
|
||||
// mode and switches panes inside pet mode. The modal binding
|
||||
// A chord may serve a different owner: Tab switches panes inside
|
||||
// pet mode. The modal binding
|
||||
// itself must never act on the composer draft.
|
||||
assert_ne!(
|
||||
on_typed,
|
||||
@@ -1199,23 +1198,6 @@ fn no_shell_binding_changes_meaning_once_the_composer_has_text() {
|
||||
"{:?} must still be live mid-draft",
|
||||
binding.id
|
||||
);
|
||||
|
||||
// Admission is only half the family: the handler behind a binding
|
||||
// must not re-guess focus from the text either. Tab is the one with
|
||||
// a dispatcher of its own, so press it in both states.
|
||||
if binding.id == ShellBindingId::ModeCycle {
|
||||
let mut empty = focus_test_app();
|
||||
empty.prompt_suggestion = None;
|
||||
let mut typed = focus_test_app();
|
||||
typed.prompt_suggestion = None;
|
||||
typed.input = "draft in progress".to_string();
|
||||
typed.cursor_position = typed.input.chars().count();
|
||||
assert_eq!(
|
||||
dispatch_tab_key(&mut empty, &key, &[], &[]),
|
||||
dispatch_tab_key(&mut typed, &key, &[], &[]),
|
||||
"Tab dispatched differently because the composer has text"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// The exception the rule is for: the composer's own editing keys are
|
||||
@@ -1235,57 +1217,25 @@ fn no_shell_binding_changes_meaning_once_the_composer_has_text() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_cycles_the_mode_on_the_launch_screen() {
|
||||
// Founder live-test: "from here tab needs to work". The launch screen's
|
||||
// composer is focused from first paint and the card's rows are arrowed,
|
||||
// so Tab has no focus left to move — it is the shell's mode cycle there,
|
||||
// exactly as in a live session.
|
||||
let mut app = focus_test_app();
|
||||
app.prompt_suggestion = None;
|
||||
app.launch.visible = true;
|
||||
assert_eq!(app.focus(), crate::tui::shell_key_routing::Focus::Launch);
|
||||
let before = app.mode;
|
||||
fn tab_never_changes_the_mode() {
|
||||
for (launch, draft) in [(true, ""), (false, ""), (false, "explain this repo")] {
|
||||
let mut app = focus_test_app();
|
||||
app.prompt_suggestion = None;
|
||||
app.launch.visible = launch;
|
||||
app.input = draft.to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
let before = app.mode;
|
||||
|
||||
let dispatch = dispatch_tab_key(
|
||||
&mut app,
|
||||
&KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
let dispatch = dispatch_tab_key(&mut app, &[], &[]);
|
||||
|
||||
assert!(
|
||||
matches!(dispatch, TabDispatch::ModeCycled { .. }),
|
||||
"Tab on the launch screen dispatched {dispatch:?}"
|
||||
);
|
||||
assert_ne!(app.mode, before, "the mode must actually change");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_still_cycles_the_mode_after_the_user_has_typed() {
|
||||
// Regression: Tab used to stop working the moment the composer held any
|
||||
// text, which is most of the time you are actually using the product.
|
||||
let mut app = focus_test_app();
|
||||
app.prompt_suggestion = None;
|
||||
app.input = "explain this repo".to_string();
|
||||
app.cursor_position = app.input.chars().count();
|
||||
let before = app.mode;
|
||||
|
||||
let dispatch = dispatch_tab_key(
|
||||
&mut app,
|
||||
&KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(dispatch, TabDispatch::ModeCycled { .. }),
|
||||
"Tab with a typed composer dispatched {dispatch:?}"
|
||||
);
|
||||
assert_ne!(app.mode, before, "the session mode must actually change");
|
||||
assert_eq!(
|
||||
app.input, "explain this repo",
|
||||
"cycling the mode must not disturb the draft"
|
||||
);
|
||||
assert_eq!(
|
||||
dispatch,
|
||||
TabDispatch::Ignored,
|
||||
"launch={launch} draft={draft:?}"
|
||||
);
|
||||
assert_eq!(app.mode, before, "launch={launch} draft={draft:?}");
|
||||
assert_eq!(app.input, draft);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -713,26 +713,26 @@ pub(crate) fn header_permission_ink(mode: ApprovalMode) -> ChromeInk {
|
||||
/// made of, now carried as merged-footer chips.
|
||||
pub(crate) type PostureChip = (Cow<'static, str>, ChromeInk);
|
||||
|
||||
/// The posture lockup as two standalone chips for the Tideline merged
|
||||
/// footer (spec §3: the old header's mode/permission chips move into the
|
||||
/// footer activity segment). Same words, same inks, and the same mapping
|
||||
/// the classic header used — [`header_mode_ink`] for the mode word,
|
||||
/// [`header_permission_ink`] for the permission phrase. The filesystem
|
||||
/// scope notice, when it deviates, folds into the permission chip's text
|
||||
/// (the header already painted it in the permission ink).
|
||||
pub(crate) fn posture_chips(app: &App) -> (Option<PostureChip>, Option<PostureChip>) {
|
||||
let mode = (
|
||||
mode_label(app.ui_locale, app.mode),
|
||||
header_mode_ink(app.mode),
|
||||
);
|
||||
let mut permission = (
|
||||
permission_label(app),
|
||||
header_permission_ink(app.approval_mode),
|
||||
);
|
||||
/// The permission dial stop as the Tideline footer's one posture chip: the
|
||||
/// Plan word in [`header_mode_ink`] while planning, otherwise the posture
|
||||
/// phrase in [`header_permission_ink`]. The filesystem scope notice, when it
|
||||
/// deviates, folds into the chip's text.
|
||||
pub(crate) fn dial_chip(app: &App) -> PostureChip {
|
||||
let mut chip = dial_chip_for(app.ui_locale, app.mode, app.approval_mode);
|
||||
if let Some(scope) = filesystem_scope_notice(app) {
|
||||
permission.0 = format!("{} · {scope}", permission.0).into();
|
||||
chip.0 = format!("{} · {scope}", chip.0).into();
|
||||
}
|
||||
(Some(mode), Some(permission))
|
||||
chip
|
||||
}
|
||||
|
||||
pub(crate) fn dial_chip_for(locale: Locale, mode: AppMode, approval: ApprovalMode) -> PostureChip {
|
||||
if mode == AppMode::Plan {
|
||||
return (mode_label(locale, mode), header_mode_ink(mode));
|
||||
}
|
||||
(
|
||||
codewhale_engine::core::authority::permission_label_for(locale, mode, approval),
|
||||
header_permission_ink(approval),
|
||||
)
|
||||
}
|
||||
|
||||
/// Summarize only tools whose lifecycle is actually `Running`. A read label
|
||||
|
||||
@@ -5455,22 +5455,11 @@ impl ConfigView {
|
||||
let approval = value_of("approval_mode")
|
||||
.or_else(|| value_of("permission_posture"))
|
||||
.and_then(|value| codewhale_execpolicy::ApprovalMode::from_config_value(&value))
|
||||
.unwrap_or(codewhale_execpolicy::ApprovalMode::Suggest);
|
||||
let mode_label = crate::tui::underwater::mode_label(self.locale, mode);
|
||||
let permission =
|
||||
codewhale_engine::core::authority::permission_label_for(self.locale, mode, approval);
|
||||
let footer = crate::tui::phase_strip::TidelineFooter::new(
|
||||
&theme,
|
||||
(
|
||||
permission.as_ref(),
|
||||
crate::tui::underwater::header_permission_ink(approval),
|
||||
),
|
||||
)
|
||||
.mode_chip(Some((
|
||||
mode_label.as_ref(),
|
||||
crate::tui::underwater::header_mode_ink(mode),
|
||||
)))
|
||||
.context_percent(context_percent);
|
||||
.unwrap_or(codewhale_execpolicy::ApprovalMode::AcceptEdits);
|
||||
let (dial, dial_ink) = crate::tui::underwater::dial_chip_for(self.locale, mode, approval);
|
||||
let footer =
|
||||
crate::tui::phase_strip::TidelineFooter::new(&theme, (dial.as_ref(), dial_ink))
|
||||
.context_percent(context_percent);
|
||||
crate::tui::phase_strip::render_tideline_footer(
|
||||
Rect {
|
||||
x: area.x.saturating_add(label_width),
|
||||
|
||||
Reference in new issue
Block a user