From c6163506f5a18a2554053c70f60765c5d09b1d0d Mon Sep 17 00:00:00 2001 From: Hunter B Date: Wed, 8 Jul 2026 11:32:30 -0700 Subject: [PATCH 1/2] tui: fold Multitask into Operate and remember /provider picker Make the visible mode roster Act/Plan/Operate with no Multitask or numeric gaps, keep yolo/4 as invisible Act+Bypass shorthand, mirror /model picker memory onto /provider, and credit the #4098 harvest. --- CHANGELOG.md | 22 +++-- crates/tui/CHANGELOG.md | 22 +++-- crates/tui/locales/en.json | 6 +- .../tui/src/commands/groups/config/config.rs | 21 ++-- .../tui/src/commands/groups/config/status.rs | 4 +- crates/tui/src/commands/groups/core/core.rs | 10 +- crates/tui/src/config_ui.rs | 2 +- crates/tui/src/core/authority.rs | 34 +++---- crates/tui/src/core/engine.rs | 4 +- crates/tui/src/localization.rs | 4 - crates/tui/src/palette/themes.rs | 38 +++----- crates/tui/src/palette/tokens.rs | 6 -- crates/tui/src/prompts.rs | 1 - crates/tui/src/prompts/modes/multitask.md | 14 --- crates/tui/src/prompts/modes/operate.md | 2 +- crates/tui/src/tools/subagent/mod.rs | 12 --- crates/tui/src/tui/app.rs | 80 +++++++-------- crates/tui/src/tui/app/tests.rs | 43 +++----- crates/tui/src/tui/footer_ui.rs | 6 +- crates/tui/src/tui/hotbar/actions.rs | 22 +---- crates/tui/src/tui/provider_picker.rs | 97 ++++++++++++++++++- crates/tui/src/tui/ui.rs | 12 ++- crates/tui/src/tui/views/mod.rs | 6 ++ crates/tui/src/tui/views/mode_picker.rs | 16 ++- crates/tui/src/tui/widgets/footer.rs | 18 ++-- crates/tui/src/tui/widgets/header.rs | 8 +- crates/tui/src/tui/widgets/mod.rs | 4 +- crates/tui/tests/qa_pty.rs | 1 - docs/CONTRIBUTORS.md | 4 + 29 files changed, 271 insertions(+), 248 deletions(-) delete mode 100644 crates/tui/src/prompts/modes/multitask.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 9570c30d8..ebe536974 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,12 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hotbar sources for MCP tools and skills: MCP tool slots prefill the composer (execution stays behind the normal tool-approval flow) and skill slots activate through the existing `$skill` alias (#2068, #2069). -- Mode & permission surface: Tab cycles Plan → Act → Multitask → Operate; - Shift+Tab cycles the Agent permission posture (Ask / Auto-Review / Full - Access) with a footer permission chip; Ctrl+T cycles reasoning effort and - Ctrl+Shift+T opens the live transcript overlay. Multitask raises the - default sub-agent fan-out and focuses the Agents sidebar; Operate is a - thin Fleet-operator mode wired to `/setup` readiness. +- Mode & permission surface: Tab cycles Plan → Act → Operate; Shift+Tab + cycles the Agent permission posture (Ask / Auto-Review / Full Access) with + a footer permission chip; Ctrl+T cycles reasoning effort and Ctrl+Shift+T + opens the live transcript overlay. Operate is the orchestration mode + (delegate, wait, inspect, dispatch) and raises sub-agent fan-out while + focusing the Agents sidebar. - Provider lake facade: the provider/model pickers, hotbar, and model inventory now enumerate configured providers' models from the bundled catalog (with an `A` toggle to browse the full catalog), replacing the @@ -39,6 +39,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Sub-agent waiting no longer peek→sleep polls: `agent(action="wait")` joins + children, unchanged peeks are throttled (~30s) with an anti-polling nudge, + and mode prompts teach the join primitive (#4097). Harvested from PR #4098 + by [@Mr-Moon121](https://github.com/Mr-Moon121) (Jeffrey Luna). +- `/provider` picker remembers catalog/configured view and highlighted row + across reopen, matching `/model` picker memory. +- Mode picker roster is exactly Act / Plan / Operate (no Multitask, no + numeric `4`/`5` gaps). Legacy `yolo`/`4` remain invisible one-way + permission shorthand for Act + Bypass. + - Fleet setup is a role/profile roster editor, not a provider-scoped model picker: the Model step lists routes from every configured provider (not only the active one), a picked route's provider is persisted explicitly in diff --git a/crates/tui/CHANGELOG.md b/crates/tui/CHANGELOG.md index 7170f4b54..8853fe9f7 100644 --- a/crates/tui/CHANGELOG.md +++ b/crates/tui/CHANGELOG.md @@ -20,12 +20,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Hotbar sources for MCP tools and skills: MCP tool slots prefill the composer (execution stays behind the normal tool-approval flow) and skill slots activate through the existing `$skill` alias (#2068, #2069). -- Mode & permission surface: Tab cycles Plan → Act → Multitask → Operate; - Shift+Tab cycles the Agent permission posture (Ask / Auto-Review / Full - Access) with a footer permission chip; Ctrl+T cycles reasoning effort and - Ctrl+Shift+T opens the live transcript overlay. Multitask raises the - default sub-agent fan-out and focuses the Agents sidebar; Operate is a - thin Fleet-operator mode wired to `/setup` readiness. +- Mode & permission surface: Tab cycles Plan → Act → Operate; Shift+Tab + cycles the Agent permission posture (Ask / Auto-Review / Full Access) with + a footer permission chip; Ctrl+T cycles reasoning effort and Ctrl+Shift+T + opens the live transcript overlay. Operate is the orchestration mode + (delegate, wait, inspect, dispatch) and raises sub-agent fan-out while + focusing the Agents sidebar. - Provider lake facade: the provider/model pickers, hotbar, and model inventory now enumerate configured providers' models from the bundled catalog (with an `A` toggle to browse the full catalog), replacing the @@ -39,6 +39,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Sub-agent waiting no longer peek→sleep polls: `agent(action="wait")` joins + children, unchanged peeks are throttled (~30s) with an anti-polling nudge, + and mode prompts teach the join primitive (#4097). Harvested from PR #4098 + by [@Mr-Moon121](https://github.com/Mr-Moon121) (Jeffrey Luna). +- `/provider` picker remembers catalog/configured view and highlighted row + across reopen, matching `/model` picker memory. +- Mode picker roster is exactly Act / Plan / Operate (no Multitask, no + numeric `4`/`5` gaps). Legacy `yolo`/`4` remain invisible one-way + permission shorthand for Act + Bypass. + - Fleet setup is a role/profile roster editor, not a provider-scoped model picker: the Model step lists routes from every configured provider (not only the active one), a picked route's provider is persisted explicitly in diff --git a/crates/tui/locales/en.json b/crates/tui/locales/en.json index 8da57442d..09d8b806c 100644 --- a/crates/tui/locales/en.json +++ b/crates/tui/locales/en.json @@ -258,7 +258,7 @@ "KbThinkingPager": "Open Turn Inspector", "KbLiveTranscript": "Open live transcript overlay (Ctrl+Shift+T; sticky-tail auto-scroll)", "KbBacktrackMessage": "Backtrack to a previous user message (Left/Right step, Enter to rewind)", - "KbCompleteCycleModes": "Complete /command, queue running-turn follow-up, cycle modes (Plan → Act → Multitask → Operate)", + "KbCompleteCycleModes": "Complete /command, queue running-turn follow-up, cycle modes (Plan → Act → Operate)", "KbCycleThinking": "Cycle reasoning effort for the active provider (Ctrl+T)", "KbCyclePermissions": "Cycle permission posture: Ask → Auto-Review → Full Access (Shift+Tab)", "KbJumpPlanAgentYolo": "Trigger hotbar slots", @@ -531,14 +531,12 @@ "AppModeAuto": "Auto", "AppModeYolo": "YOLO (deprecated)", "AppModePlan": "Plan", - "AppModeMultitask": "Multitask", "AppModeOperate": "Operate", "AppModeAgentHint": "Execute directly — edits and shell ask for approval", "AppModeAutoHint": "Shell enabled with automatic risk review", "AppModePlanHint": "Read-only research first — present a plan before acting", "AppModeYoloHint": "Deprecated — maps to Act + Full Access", - "AppModeMultitaskHint": "Stay responsive — fan quick work out to background agents", - "AppModeOperateHint": "Conduct an agent fleet — decompose, delegate, verify, synthesize", + "AppModeOperateHint": "Coordinate workflows, spawn workers, wait, and dispatch more work", "VimModeNormal": "-- NORMAL --", "VimModeInsert": "-- INSERT --", "VimModeVisual": "-- VISUAL --", diff --git a/crates/tui/src/commands/groups/config/config.rs b/crates/tui/src/commands/groups/config/config.rs index 29f09b3d2..e9ed74caa 100644 --- a/crates/tui/src/commands/groups/config/config.rs +++ b/crates/tui/src/commands/groups/config/config.rs @@ -1464,7 +1464,7 @@ pub fn set_config_value(app: &mut App, key: &str, value: &str, persist: bool) -> " (session only, add --save to persist)".to_string() }; let mode_hint = if enabled { - " Agent mode will expose shell on the next turn with approval gating. YOLO also enables shell and auto-approves." + " Agent mode will expose shell on the next turn with approval gating. Bypass permissions (Shift+Tab) also enables shell and auto-approves." } else { " Shell tools will be hidden on the next turn. Re-enable with `/config allow_shell true`." }; @@ -1853,9 +1853,7 @@ pub fn mode(app: &mut App, arg: Option<&str>) -> CommandResult { CommandResult::message(message) } } - None => { - CommandResult::error("Usage: /mode [act|agent|plan|multitask|operate|yolo|1|2|3|5|4]") - } + None => CommandResult::error("Usage: /mode [act|agent|plan|operate|1|2|3]"), } } @@ -2545,17 +2543,14 @@ Parse error: permissions.toml at permissions.toml could not be parsed: expected let _ = mode(&mut app, Some("plan")); assert_eq!(app.mode, AppMode::Plan); let result = mode(&mut app, Some("3")); - assert_eq!( - result.action, - Some(AppAction::ModeChanged(AppMode::Multitask)) - ); - assert_eq!(app.mode, AppMode::Multitask); - let result = mode(&mut app, Some("5")); assert_eq!( result.action, Some(AppAction::ModeChanged(AppMode::Operate)) ); assert_eq!(app.mode, AppMode::Operate); + let result = mode(&mut app, Some("5")); + assert!(result.is_error); + assert_eq!(app.mode, AppMode::Operate); let result = mode(&mut app, Some("9")); assert!(result.is_error); assert_eq!(app.mode, AppMode::Operate); @@ -2853,7 +2848,9 @@ Parse error: permissions.toml at permissions.toml could not be parsed: expected assert!(msg.contains("Agent mode")); assert!(msg.contains("approval gating")); assert!(msg.contains("next turn")); - assert!(msg.contains("YOLO also enables shell and auto-approves")); + assert!( + msg.contains("Bypass permissions (Shift+Tab) also enables shell and auto-approves") + ); } #[test] @@ -2876,7 +2873,7 @@ Parse error: permissions.toml at permissions.toml could not be parsed: expected assert_eq!( msg, format!( - "allow_shell = true (saved to {}). Agent mode will expose shell on the next turn with approval gating. YOLO also enables shell and auto-approves.", + "allow_shell = true (saved to {}). Agent mode will expose shell on the next turn with approval gating. Bypass permissions (Shift+Tab) also enables shell and auto-approves.", config_path.display() ) ); diff --git a/crates/tui/src/commands/groups/config/status.rs b/crates/tui/src/commands/groups/config/status.rs index a4e99b95e..31c3c4f2d 100644 --- a/crates/tui/src/commands/groups/config/status.rs +++ b/crates/tui/src/commands/groups/config/status.rs @@ -145,9 +145,7 @@ fn permission_summary(app: &App) -> String { fn safety_summary(app: &App) -> &'static str { match app.mode { AppMode::Plan => "sandbox read-only, network off", - AppMode::Agent | AppMode::Auto | AppMode::Multitask | AppMode::Operate => { - "sandbox workspace-write, network on" - } + AppMode::Agent | AppMode::Auto | AppMode::Operate => "sandbox workspace-write, network on", AppMode::Yolo => "sandbox disabled, network unrestricted", } } diff --git a/crates/tui/src/commands/groups/core/core.rs b/crates/tui/src/commands/groups/core/core.rs index 22e8f554f..06655606b 100644 --- a/crates/tui/src/commands/groups/core/core.rs +++ b/crates/tui/src/commands/groups/core/core.rs @@ -610,17 +610,10 @@ pub fn home_dashboard(app: &mut App) -> CommandResult { let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeYoloModeTip)); let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeYoloModeCaution)); } - AppMode::Multitask => { - let _ = writeln!(stats, "{}", tr(locale, MessageId::HomeAgentModeTip)); - let _ = writeln!( - stats, - " Multitask: light delegation — session model is operator; background workers" - ); - } AppMode::Operate => { let _ = writeln!( stats, - " Operate: Fleet operator — /model route; decompose into workflow/Fleet; monitor" + " Operate: coordinate workflows, spawn workers, wait on results, dispatch more work" ); } AppMode::Plan => { @@ -1383,7 +1376,6 @@ mod tests { AppMode::Auto, AppMode::Yolo, AppMode::Plan, - AppMode::Multitask, AppMode::Operate, ]; for mode in modes { diff --git a/crates/tui/src/config_ui.rs b/crates/tui/src/config_ui.rs index 3c1c0950b..d25592b4a 100644 --- a/crates/tui/src/config_ui.rs +++ b/crates/tui/src/config_ui.rs @@ -951,7 +951,7 @@ impl From<&str> for TranscriptSpacingValue { impl From<&str> for DefaultModeValue { fn from(value: &str) -> Self { match AppMode::from_setting(value) { - AppMode::Agent | AppMode::Multitask | AppMode::Operate => Self::Agent, + AppMode::Agent | AppMode::Operate => Self::Agent, AppMode::Plan => Self::Plan, AppMode::Auto => Self::Agent, AppMode::Yolo => Self::Yolo, diff --git a/crates/tui/src/core/authority.rs b/crates/tui/src/core/authority.rs index 65caaa09e..6a88d355c 100644 --- a/crates/tui/src/core/authority.rs +++ b/crates/tui/src/core/authority.rs @@ -42,7 +42,7 @@ pub(crate) struct EffectiveModePolicy { /// - `Plan` -> read-only: no shell, no trust, `Suggest` approvals. /// - `Agent` -> the user's durable baseline (`prefs`). /// - `Auto` -> compatibility alias for Agent; not a separate behavior. -/// - `Multitask` / `Operate` -> Agent baseline plus orchestration posture in prompts. +/// - `Operate` -> Agent baseline plus orchestration posture in prompts. /// - `Yolo` -> legacy compat; full authority: shell + trust + `Bypass` approvals. #[must_use] pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> EffectiveModePolicy { @@ -53,14 +53,12 @@ pub(crate) fn base_policy_for_mode(mode: AppMode, prefs: &ModeSessionPrefs) -> E trust_mode: false, approval_mode: ApprovalMode::Suggest, }, - AppMode::Agent | AppMode::Auto | AppMode::Multitask | AppMode::Operate => { - EffectiveModePolicy { - mode, - allow_shell: prefs.agent_allow_shell, - trust_mode: prefs.agent_trust_mode, - approval_mode: prefs.agent_approval_mode, - } - } + AppMode::Agent | AppMode::Auto | AppMode::Operate => EffectiveModePolicy { + mode, + allow_shell: prefs.agent_allow_shell, + trust_mode: prefs.agent_trust_mode, + approval_mode: prefs.agent_approval_mode, + }, AppMode::Yolo => EffectiveModePolicy { mode, allow_shell: true, @@ -195,14 +193,12 @@ pub(crate) fn agent_approval_mode_for_turn( pub(crate) fn sandbox_policy_for_mode(mode: AppMode, workspace: &Path) -> SandboxPolicy { match mode { AppMode::Plan => SandboxPolicy::ReadOnly, - AppMode::Agent | AppMode::Auto | AppMode::Multitask | AppMode::Operate => { - SandboxPolicy::WorkspaceWrite { - writable_roots: vec![workspace.to_path_buf()], - network_access: true, - exclude_tmpdir: false, - exclude_slash_tmp: false, - } - } + AppMode::Agent | AppMode::Auto | AppMode::Operate => SandboxPolicy::WorkspaceWrite { + writable_roots: vec![workspace.to_path_buf()], + network_access: true, + exclude_tmpdir: false, + exclude_slash_tmp: false, + }, AppMode::Yolo => SandboxPolicy::DangerFullAccess, } } @@ -215,8 +211,6 @@ pub(crate) fn shell_policy_for_mode(mode: AppMode, allow_shell: bool) -> ShellPo } match mode { AppMode::Plan => ShellPolicy::None, - AppMode::Agent | AppMode::Auto | AppMode::Multitask | AppMode::Operate | AppMode::Yolo => { - ShellPolicy::Full - } + AppMode::Agent | AppMode::Auto | AppMode::Operate | AppMode::Yolo => ShellPolicy::Full, } } diff --git a/crates/tui/src/core/engine.rs b/crates/tui/src/core/engine.rs index d54f6b7cf..b09f23217 100644 --- a/crates/tui/src/core/engine.rs +++ b/crates/tui/src/core/engine.rs @@ -674,11 +674,9 @@ fn subagent_mailbox_best_effort_send_permitted( impl Engine { fn mode_runtime_instructions(mode: AppMode) -> &'static str { match mode { - AppMode::Agent | AppMode::Auto => prompts::AGENT_MODE, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => prompts::AGENT_MODE, AppMode::Plan => prompts::PLAN_MODE, - AppMode::Multitask => prompts::MULTITASK_MODE, AppMode::Operate => prompts::OPERATE_MODE, - AppMode::Yolo => prompts::YOLO_MODE, } .trim() } diff --git a/crates/tui/src/localization.rs b/crates/tui/src/localization.rs index c25e7d582..9fd879f48 100644 --- a/crates/tui/src/localization.rs +++ b/crates/tui/src/localization.rs @@ -621,13 +621,11 @@ pub enum MessageId { AppModeAuto, AppModeYolo, AppModePlan, - AppModeMultitask, AppModeOperate, AppModeAgentHint, AppModeAutoHint, AppModePlanHint, AppModeYoloHint, - AppModeMultitaskHint, AppModeOperateHint, VimModeNormal, VimModeInsert, @@ -1375,13 +1373,11 @@ pub const ALL_MESSAGE_IDS: &[MessageId] = &[ MessageId::AppModeAuto, MessageId::AppModeYolo, MessageId::AppModePlan, - MessageId::AppModeMultitask, MessageId::AppModeOperate, MessageId::AppModeAgentHint, MessageId::AppModeAutoHint, MessageId::AppModePlanHint, MessageId::AppModeYoloHint, - MessageId::AppModeMultitaskHint, MessageId::AppModeOperateHint, MessageId::VimModeNormal, MessageId::VimModeInsert, diff --git a/crates/tui/src/palette/themes.rs b/crates/tui/src/palette/themes.rs index 5e1c9d122..edd472b5e 100644 --- a/crates/tui/src/palette/themes.rs +++ b/crates/tui/src/palette/themes.rs @@ -38,11 +38,10 @@ pub struct UiTheme { pub warning: Color, pub success: Color, pub info: Color, - // Mode badge colors (act/yolo/plan/multitask/operate) + // Mode badge colors (act/plan/operate; mode_yolo kept for legacy theme data) pub mode_agent: Color, pub mode_yolo: Color, pub mode_plan: Color, - pub mode_multitask: Color, pub mode_operate: Color, // Footer statusline colors pub status_ready: Color, @@ -125,7 +124,6 @@ pub const UI_THEME: UiTheme = UiTheme { mode_agent: MODE_AGENT, mode_yolo: MODE_YOLO, mode_plan: MODE_PLAN, - mode_multitask: MODE_MULTITASK, mode_operate: MODE_OPERATE, status_ready: TEXT_MUTED, status_working: WHALE_INFO, @@ -163,14 +161,13 @@ pub const LIGHT_UI_THEME: UiTheme = UiTheme { error_surface: Color::Rgb(254, 229, 229), error_border: Color::Rgb(240, 120, 130), error_text: Color::Rgb(120, 20, 30), - warning: Color::Rgb(180, 83, 9), // amber - success: Color::Rgb(21, 128, 61), // green - info: Color::Rgb(53, 120, 229), // blue - mode_agent: Color::Rgb(53, 120, 229), // blue - mode_yolo: Color::Rgb(200, 40, 60), // red - mode_plan: Color::Rgb(180, 83, 9), // amber - mode_multitask: Color::Rgb(80, 180, 130), // mint green - mode_operate: Color::Rgb(124, 58, 237), // violet + warning: Color::Rgb(180, 83, 9), // amber + success: Color::Rgb(21, 128, 61), // green + info: Color::Rgb(53, 120, 229), // blue + mode_agent: Color::Rgb(53, 120, 229), // blue + mode_yolo: Color::Rgb(200, 40, 60), // red + mode_plan: Color::Rgb(180, 83, 9), // amber + mode_operate: Color::Rgb(124, 58, 237), // violet status_ready: LIGHT_TEXT_MUTED, status_working: Color::Rgb(53, 120, 229), // blue status_warning: Color::Rgb(180, 83, 9), // amber @@ -213,7 +210,6 @@ pub const SOLARIZED_LIGHT_UI_THEME: UiTheme = UiTheme { mode_agent: SOLARIZED_BLUE, mode_yolo: SOLARIZED_RED, mode_plan: SOLARIZED_ORANGE, - mode_multitask: SOLARIZED_GREEN, mode_operate: Color::Rgb(0x6C, 0x71, 0xC4), // solarized violet status_ready: SOLARIZED_CYAN, status_working: SOLARIZED_BLUE, @@ -257,7 +253,6 @@ pub const GRAYSCALE_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(200, 200, 200), mode_yolo: GRAYSCALE_TEXT_BODY, mode_plan: GRAYSCALE_TEXT_MUTED, - mode_multitask: GRAYSCALE_TEXT_SOFT, // Monochrome theme: pure white is the one step left above the YOLO // body tone (236) that stays unmistakably distinct. mode_operate: Color::Rgb(255, 255, 255), @@ -303,7 +298,6 @@ pub const CATPPUCCIN_MOCHA_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(0x89, 0xb4, 0xfa), // blue mode_yolo: Color::Rgb(0xf3, 0x8b, 0xa8), // red mode_plan: Color::Rgb(0xfa, 0xb3, 0x87), // peach - mode_multitask: Color::Rgb(0xa6, 0xe3, 0xa1), // green mode_operate: Color::Rgb(0xcb, 0xa6, 0xf7), // mauve status_ready: Color::Rgb(0x7f, 0x84, 0x9c), // overlay1 status_working: Color::Rgb(0x74, 0xc7, 0xec), // sapphire @@ -347,7 +341,6 @@ pub const TOKYO_NIGHT_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(0x7a, 0xa2, 0xf7), // blue mode_yolo: Color::Rgb(0xf7, 0x76, 0x8e), // red mode_plan: Color::Rgb(0xff, 0x9e, 0x64), // orange - mode_multitask: Color::Rgb(0x9e, 0xce, 0x6a), // green mode_operate: Color::Rgb(0xbb, 0x9a, 0xf7), // purple status_ready: Color::Rgb(0x56, 0x5f, 0x89), // comment status_working: Color::Rgb(0x7d, 0xcf, 0xff), // cyan @@ -391,7 +384,6 @@ pub const DRACULA_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(0xbd, 0x93, 0xf9), // purple mode_yolo: Color::Rgb(0xff, 0x55, 0x55), // red mode_plan: Color::Rgb(0xff, 0xb8, 0x6c), // orange - mode_multitask: Color::Rgb(0x50, 0xfa, 0x7b), // green mode_operate: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan status_ready: Color::Rgb(0x62, 0x72, 0xa4), // comment status_working: Color::Rgb(0x8b, 0xe9, 0xfd), // cyan @@ -447,7 +439,6 @@ pub const TERMINAL_UI_THEME: UiTheme = UiTheme { // so the mode indicator and warning chip don't collide on themes that // render both in the status row. mode_plan: Color::Magenta, - mode_multitask: Color::Green, mode_operate: Color::Cyan, // DarkGray gives "Ready" a low-contrast but still distinguishable hue // versus default body text (which is `Color::Reset` on this theme). @@ -493,7 +484,6 @@ pub const GRUVBOX_DARK_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(0x83, 0xa5, 0x98), // blue mode_yolo: Color::Rgb(0xfb, 0x49, 0x34), // red mode_plan: Color::Rgb(0xfe, 0x80, 0x19), // orange - mode_multitask: Color::Rgb(0x8e, 0xc0, 0x7c), // green mode_operate: Color::Rgb(0xd3, 0x86, 0x9b), // purple status_ready: Color::Rgb(0x92, 0x83, 0x74), // gray status_working: Color::Rgb(0x8e, 0xc0, 0x7c), // aqua @@ -540,11 +530,10 @@ pub const CLAUDE_UI_THEME: UiTheme = UiTheme { success: Color::Rgb(0x5d, 0xb8, 0x72), // green info: Color::Rgb(0x5d, 0xb8, 0xa6), // teal // Mode badges - mode_agent: Color::Rgb(0xcc, 0x78, 0x5c), // coral - mode_yolo: Color::Rgb(0xc6, 0x45, 0x45), // red - mode_plan: Color::Rgb(0xe8, 0xa5, 0x5a), // amber - mode_multitask: Color::Rgb(0x5d, 0xb8, 0x72), // green - mode_operate: Color::Rgb(0x8a, 0x63, 0xd2), // violet + mode_agent: Color::Rgb(0xcc, 0x78, 0x5c), // coral + mode_yolo: Color::Rgb(0xc6, 0x45, 0x45), // red + mode_plan: Color::Rgb(0xe8, 0xa5, 0x5a), // amber + mode_operate: Color::Rgb(0x8a, 0x63, 0xd2), // violet // Footer statusline status_ready: Color::Rgb(0xa0, 0x9d, 0x96), status_working: Color::Rgb(0x5d, 0xb8, 0xa6), @@ -646,7 +635,6 @@ pub const MATRIX_UI_THEME: UiTheme = UiTheme { mode_agent: Color::Rgb(0, 153, 0), mode_yolo: Color::Rgb(255, 100, 100), mode_plan: Color::Rgb(255, 170, 60), - mode_multitask: Color::Rgb(170, 255, 170), mode_operate: Color::Rgb(100, 255, 220), status_ready: Color::Rgb(0, 85, 0), status_working: Color::Rgb( @@ -912,9 +900,7 @@ mod tests { let ui = theme_id.ui_theme(); let badges = [ ("act", ui.mode_agent), - ("yolo", ui.mode_yolo), ("plan", ui.mode_plan), - ("multitask", ui.mode_multitask), ("operate", ui.mode_operate), ]; for (i, (name_a, color_a)) in badges.iter().enumerate() { diff --git a/crates/tui/src/palette/tokens.rs b/crates/tui/src/palette/tokens.rs index ede9167e2..976982dba 100644 --- a/crates/tui/src/palette/tokens.rs +++ b/crates/tui/src/palette/tokens.rs @@ -57,7 +57,6 @@ pub const WHALE_DIFF_DELETED_BG_RGB: (u8, u8, u8) = (42, 18, 26); // #2A121A pub const WHALE_MODE_AGENT_RGB: (u8, u8, u8) = (80, 150, 255); // #5096FF pub const WHALE_MODE_YOLO_RGB: (u8, u8, u8) = (255, 100, 100); // #FF6464 pub const WHALE_MODE_PLAN_RGB: (u8, u8, u8) = (246, 196, 83); // #F6C453 Signal Gold -pub const WHALE_MODE_MULTITASK_RGB: (u8, u8, u8) = (100, 220, 160); // #64DCA0 pub const WHALE_MODE_OPERATE_RGB: (u8, u8, u8) = (178, 132, 255); // #B284FF pub const WHALE_TOOL_LIVE_RGB: (u8, u8, u8) = (140, 190, 238); // #8CBEEE pub const WHALE_TOOL_ISSUE_RGB: (u8, u8, u8) = (198, 150, 160); // #C696A0 @@ -487,11 +486,6 @@ pub const MODE_PLAN: Color = Color::Rgb( WHALE_MODE_PLAN_RGB.1, WHALE_MODE_PLAN_RGB.2, ); -pub const MODE_MULTITASK: Color = Color::Rgb( - WHALE_MODE_MULTITASK_RGB.0, - WHALE_MODE_MULTITASK_RGB.1, - WHALE_MODE_MULTITASK_RGB.2, -); pub const MODE_OPERATE: Color = Color::Rgb( WHALE_MODE_OPERATE_RGB.0, WHALE_MODE_OPERATE_RGB.1, diff --git a/crates/tui/src/prompts.rs b/crates/tui/src/prompts.rs index 9531f8ad5..c1f4ebb9f 100644 --- a/crates/tui/src/prompts.rs +++ b/crates/tui/src/prompts.rs @@ -877,7 +877,6 @@ pub const PLAYFUL_PERSONALITY: &str = include_str!("prompts/personalities/playfu pub const AGENT_MODE: &str = include_str!("prompts/modes/agent.md"); pub const PLAN_MODE: &str = include_str!("prompts/modes/plan.md"); pub const YOLO_MODE: &str = include_str!("prompts/modes/yolo.md"); -pub const MULTITASK_MODE: &str = include_str!("prompts/modes/multitask.md"); pub const OPERATE_MODE: &str = include_str!("prompts/modes/operate.md"); /// Approval-policy overlays — whether tool calls are auto-approved, diff --git a/crates/tui/src/prompts/modes/multitask.md b/crates/tui/src/prompts/modes/multitask.md deleted file mode 100644 index dbcd72416..000000000 --- a/crates/tui/src/prompts/modes/multitask.md +++ /dev/null @@ -1,14 +0,0 @@ -##### Mode: Multitask - -You are the Fleet operator in **light delegation** posture — the same session route as `/model` and the pinned operator row in `/fleet roster`, but you stay highly responsive while workers run elsewhere. - -**Lighter than Operate:** spawn background sub-agents (`agent` with `run_in_background: true`) and non-blocking `/workflow` starts; keep your turn short. Operate is the full conductor posture for durable value-stream orchestration; Multitask is parallel fan-out without building a full workflow plan. - -Keep your turn lightweight: -- Spawn 2–4 explore/review children in parallel when work decomposes cleanly. -- Start workflows non-blocking; monitor receipts and integrate results as they arrive. -- Do only trivial one-liners inline (single read, quick grep, status check). - -After spawning background workers, keep doing independent parent work in the same turn. Treat `` and workflow run cards as internal signals — verify load-bearing claims before integrating. Never poll workers with peek/status loops or `sleep`: completions arrive on their own; use one `agent(action="wait")` call only when you must block for fan-in. - -Do NOT monopolize the turn with long sequential tool chains when delegation would finish faster. Do NOT announce that you are in Multitask mode. diff --git a/crates/tui/src/prompts/modes/operate.md b/crates/tui/src/prompts/modes/operate.md index a804813b5..5abcb4774 100644 --- a/crates/tui/src/prompts/modes/operate.md +++ b/crates/tui/src/prompts/modes/operate.md @@ -16,7 +16,7 @@ You are the **Fleet operator** — the session's `/model` route, pinned as the f - Do **not** sequentially grind through independent slices; fan out and monitor. - Prefer `workflow`, `agent`, and fleet-related tools over solo `exec_shell` / patch spam. -Multitask is lighter delegation (background fan-out, stay responsive). **Operate is the full operator conductor** — plan the value stream, launch durable work, throttle on worker capacity, close with an orchestration summary. +**Operate** coordinates the value stream: fan out workers, wait on results, launch durable workflows, throttle on capacity, and close with an orchestration summary. Before large fan-out, check Operate/Fleet readiness (`/setup report`). If roster or concurrency is not ready, say so briefly and route to `/setup fleet` rather than pretending Fleet is configured. diff --git a/crates/tui/src/tools/subagent/mod.rs b/crates/tui/src/tools/subagent/mod.rs index efc82fb93..23e19e5bb 100644 --- a/crates/tui/src/tools/subagent/mod.rs +++ b/crates/tui/src/tools/subagent/mod.rs @@ -4341,18 +4341,6 @@ fn apply_session_spawn_policy( return None; } match runtime.parent_mode { - AppMode::Multitask => { - // Background-friendly fan-out: when the operator did not specify - // strength or an exact model, prefer the faster sibling for parallel - // lookup/review children. - if !request.model_strength_explicit - && request.model.is_none() - && request.agent_type != SubAgentType::Explore - { - request.model_strength = SubAgentModelStrength::Faster; - } - None - } AppMode::Operate => { if request.profile.is_some() || request.agent_type_explicit { return None; diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index d60e4567e..25878673b 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -169,7 +169,6 @@ pub enum AppMode { /// Legacy compatibility alias; resolves to [`Self::Agent`] + bypass approvals. Yolo, Plan, - Multitask, Operate, } @@ -375,6 +374,17 @@ pub struct ModelPickerMemory { pub selected_row_id: Option, } +/// Browsing context captured when the `/provider` picker is dismissed. +/// Mirrors [`ModelPickerMemory`] so reopen restores view + highlight. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ProviderPickerMemory { + /// True when the user left the picker in the full-catalog view + /// (`A` toggle), false for the configured-only default view. + pub catalog_view: bool, + /// Provider id highlighted at dismissal, if it was a real row. + pub selected_provider_id: Option, +} + #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct AgentProgressMeta { pub parent_run_id: Option, @@ -928,23 +938,23 @@ const MAX_COMPOSER_DISPLAY_CHARS: usize = 4_000; const MAX_DRAFT_HISTORY: usize = 50; impl AppMode { - /// Keyboard cycle order: Plan -> Act -> Multitask -> Operate -> Plan. + /// Keyboard cycle order: Plan -> Act -> Operate -> Plan. /// /// `Auto` remains an internal variant while the real implementation is /// redesigned; do not expose it through user-facing mode selection (#3733). /// `Yolo` is kept for parse/back-compat only and is not in the Tab cycle. - pub const CYCLE: [Self; 4] = [Self::Plan, Self::Agent, Self::Multitask, Self::Operate]; + pub const CYCLE: [Self; 3] = [Self::Plan, Self::Agent, Self::Operate]; - /// User-facing picker / numeric command order. - pub const CHOICES: [Self; 4] = [Self::Agent, Self::Plan, Self::Multitask, Self::Operate]; + /// User-facing picker / numeric command order: 1 Act / 2 Plan / 3 Operate. + pub const CHOICES: [Self; 3] = [Self::Agent, Self::Plan, Self::Operate]; #[must_use] pub fn parse(value: &str) -> Option { match value.trim().to_ascii_lowercase().as_str() { "agent" | "act" | "auto" | "1" => Some(Self::Agent), "plan" | "2" => Some(Self::Plan), - "multitask" | "multi" | "3" => Some(Self::Multitask), - "operate" | "operation" | "ops" | "5" => Some(Self::Operate), + "operate" | "operation" | "ops" | "3" => Some(Self::Operate), + // Invisible one-way permission shorthand only — never a visible mode. "yolo" | "4" | "bypass" | "bypass-permissions" | "bypasspermissions" => { Some(Self::Yolo) } @@ -954,7 +964,11 @@ impl AppMode { #[must_use] pub fn from_setting(value: &str) -> Self { - Self::parse(value).unwrap_or(Self::Agent) + // Unreleased Multitask never shipped; normalize leftover settings to Operate. + match value.trim().to_ascii_lowercase().as_str() { + "multitask" | "multi" | "5" => Self::Operate, + other => Self::parse(other).unwrap_or(Self::Agent), + } } #[must_use] @@ -962,9 +976,9 @@ impl AppMode { match self { Self::Agent => "agent", Self::Auto => "agent", - Self::Yolo => "yolo", + // Write current permission vocabulary, not the legacy YOLO label. + Self::Yolo => "agent", Self::Plan => "plan", - Self::Multitask => "multitask", Self::Operate => "operate", } } @@ -974,9 +988,8 @@ impl AppMode { match self { AppMode::Agent => "ACT", AppMode::Auto => "ACT", - AppMode::Yolo => "YOLO", + AppMode::Yolo => "ACT", AppMode::Plan => "PLAN", - AppMode::Multitask => "MULTITASK", AppMode::Operate => "OPERATE", } } @@ -986,9 +999,8 @@ impl AppMode { match self { AppMode::Agent => "Act", AppMode::Auto => "Act", - AppMode::Yolo => "YOLO", + AppMode::Yolo => "Act", AppMode::Plan => "Plan", - AppMode::Multitask => "Multitask", AppMode::Operate => "Operate", } } @@ -996,29 +1008,23 @@ impl AppMode { #[must_use] pub fn number(self) -> char { match self { - AppMode::Agent => '1', + AppMode::Agent | AppMode::Auto | AppMode::Yolo => '1', AppMode::Plan => '2', - AppMode::Multitask => '3', - AppMode::Auto => '1', - AppMode::Yolo => '4', - AppMode::Operate => '5', + AppMode::Operate => '3', } } #[must_use] pub fn uses_agent_baseline(self) -> bool { - matches!( - self, - Self::Agent | Self::Auto | Self::Multitask | Self::Operate - ) + matches!(self, Self::Agent | Self::Auto | Self::Operate) } - /// Wave 7 M4/M5: delegation modes get a higher parallel launch floor so - /// background fan-out is not throttled to a single slot when config is low. + /// Operate gets a higher parallel launch floor so background fan-out is + /// not throttled to a single slot when config is low. #[must_use] pub fn mode_delegation_launch_floor(self) -> usize { match self { - Self::Multitask | Self::Operate => 4, + Self::Operate => 4, _ => 1, } } @@ -1026,7 +1032,7 @@ impl AppMode { /// Whether entering this mode should emphasize the Agents sidebar panel. #[must_use] pub fn prefers_agents_sidebar(self) -> bool { - matches!(self, Self::Multitask | Self::Operate) + matches!(self, Self::Operate) } /// Localized short name for the mode picker (user-facing surface only). @@ -1035,10 +1041,8 @@ impl AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Auto => MessageId::AppModeAgent, - AppMode::Yolo => MessageId::AppModeYolo, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgent, AppMode::Plan => MessageId::AppModePlan, - AppMode::Multitask => MessageId::AppModeMultitask, AppMode::Operate => MessageId::AppModeOperate, }, ) @@ -1050,10 +1054,8 @@ impl AppMode { tr( locale, match self { - AppMode::Agent | AppMode::Auto => MessageId::AppModeAgentHint, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => MessageId::AppModeAgentHint, AppMode::Plan => MessageId::AppModePlanHint, - AppMode::Yolo => MessageId::AppModeYoloHint, - AppMode::Multitask => MessageId::AppModeMultitaskHint, AppMode::Operate => MessageId::AppModeOperateHint, }, ) @@ -1064,11 +1066,10 @@ impl AppMode { pub fn description(self) -> &'static str { match self { AppMode::Agent | AppMode::Auto => "Act mode - autonomous task execution with tools", - AppMode::Yolo => "YOLO mode - full tool access without approvals (deprecated)", + AppMode::Yolo => "Act mode with bypass permissions (legacy YOLO shorthand)", AppMode::Plan => "Plan mode - design before implementing", - AppMode::Multitask => "Multitask mode - light delegation; operator stays responsive", AppMode::Operate => { - "Operate mode - Fleet operator conductor; workers execute, you monitor" + "Operate mode - coordinate workflows, spawn workers, wait, and dispatch more work" } } } @@ -1793,6 +1794,8 @@ pub struct App { /// restores the view mode and highlighted row instead of resetting to the /// top (#4109 picker memory). Session-scoped, never persisted. pub model_picker_memory: Option, + /// Browsing context from the last dismissed `/provider` picker. + pub provider_picker_memory: Option, /// Last known mouse position for tooltip placement. pub last_mouse_pos: Option<(u16, u16)>, /// Whether the user is currently dragging the sidebar resize handle. @@ -2785,6 +2788,7 @@ impl App { sidebar_hover_tooltip: None, cached_work_summary: None, model_picker_memory: None, + provider_picker_memory: None, last_mouse_pos: None, sidebar_resizing: false, sidebar_resize_anchor_x: 0, @@ -3207,7 +3211,7 @@ impl App { } } - /// Cycle through modes: Plan → Act → Multitask → Operate → Plan. + /// Cycle through modes: Plan → Act → Operate → Plan. pub fn cycle_mode(&mut self) { if self.reject_setting_change_while_busy("Mode") { return; @@ -6168,7 +6172,7 @@ pub enum AppAction { OpenProviderSetup { provider: Option, }, - /// Open the `/mode` picker modal for Agent / Plan / YOLO. + /// Open the `/mode` picker modal for Act / Plan / Operate. OpenModePicker, /// Refresh the engine prompt after the UI operating mode changes. ModeChanged(AppMode), diff --git a/crates/tui/src/tui/app/tests.rs b/crates/tui/src/tui/app/tests.rs index 15ce74a40..a76dc2379 100644 --- a/crates/tui/src/tui/app/tests.rs +++ b/crates/tui/src/tui/app/tests.rs @@ -1476,50 +1476,45 @@ fn app_mode_helpers_centralize_parse_labels_and_cycle_order() { assert_eq!(AppMode::parse("act"), Some(AppMode::Agent)); assert_eq!(AppMode::parse("2"), Some(AppMode::Plan)); assert_eq!(AppMode::parse("auto"), Some(AppMode::Agent)); - assert_eq!(AppMode::parse("3"), Some(AppMode::Multitask)); - assert_eq!(AppMode::parse("5"), Some(AppMode::Operate)); + assert_eq!(AppMode::parse("3"), Some(AppMode::Operate)); + assert_eq!(AppMode::parse("operate"), Some(AppMode::Operate)); assert_eq!(AppMode::parse("YOLO"), Some(AppMode::Yolo)); assert_eq!(AppMode::parse("4"), Some(AppMode::Yolo)); + assert_eq!(AppMode::parse("multitask"), None); + assert_eq!(AppMode::parse("5"), None); assert_eq!(AppMode::parse("fast"), None); + assert_eq!(AppMode::from_setting("multitask"), AppMode::Operate); + assert_eq!(AppMode::from_setting("5"), AppMode::Operate); assert_eq!(AppMode::Agent.as_setting(), "agent"); assert_eq!(AppMode::Auto.as_setting(), "agent"); + assert_eq!(AppMode::Yolo.as_setting(), "agent"); assert_eq!(AppMode::Plan.display_name(), "Plan"); assert_eq!(AppMode::Auto.display_name(), "Act"); assert_eq!(AppMode::Auto.label(), "ACT"); - assert_eq!(AppMode::Yolo.label(), "YOLO"); + assert_eq!(AppMode::Yolo.label(), "ACT"); + assert_eq!(AppMode::Yolo.display_name(), "Act"); assert_eq!(AppMode::Agent.number(), '1'); assert_eq!(AppMode::Auto.number(), '1'); - assert_eq!(AppMode::Yolo.number(), '4'); + assert_eq!(AppMode::Yolo.number(), '1'); + assert_eq!(AppMode::Operate.number(), '3'); assert_eq!( AppMode::CHOICES, - [ - AppMode::Agent, - AppMode::Plan, - AppMode::Multitask, - AppMode::Operate - ] + [AppMode::Agent, AppMode::Plan, AppMode::Operate] ); assert_eq!( AppMode::CYCLE, - [ - AppMode::Plan, - AppMode::Agent, - AppMode::Multitask, - AppMode::Operate - ] + [AppMode::Plan, AppMode::Agent, AppMode::Operate] ); assert_eq!(AppMode::Plan.next(), AppMode::Agent); - assert_eq!(AppMode::Agent.next(), AppMode::Multitask); - assert_eq!(AppMode::Multitask.next(), AppMode::Operate); + assert_eq!(AppMode::Agent.next(), AppMode::Operate); assert_eq!(AppMode::Operate.next(), AppMode::Plan); assert_eq!(AppMode::Auto.next(), AppMode::Agent); assert_eq!(AppMode::Yolo.next(), AppMode::Agent); assert_eq!(AppMode::Plan.previous(), AppMode::Operate); assert_eq!(AppMode::Agent.previous(), AppMode::Plan); - assert_eq!(AppMode::Multitask.previous(), AppMode::Agent); - assert_eq!(AppMode::Operate.previous(), AppMode::Multitask); + assert_eq!(AppMode::Operate.previous(), AppMode::Agent); assert_eq!(AppMode::Auto.previous(), AppMode::Agent); assert_eq!(AppMode::Yolo.previous(), AppMode::Agent); } @@ -1543,10 +1538,6 @@ fn test_cycle_mode_reverse_transitions() { app.mode = AppMode::Operate; app.cycle_mode_reverse(); - assert_eq!(app.mode, AppMode::Multitask); - - app.mode = AppMode::Multitask; - app.cycle_mode_reverse(); assert_eq!(app.mode, AppMode::Agent); app.mode = AppMode::Agent; @@ -1760,9 +1751,7 @@ fn base_policy_for_mode_projects_the_mode_permission_table() { assert!(auto.trust_mode); assert_eq!(auto.approval_mode, ApprovalMode::Never); - // Multitask / Operate use the Agent baseline. - let multitask = base_policy_for_mode(AppMode::Multitask, &prefs); - assert_eq!(multitask.approval_mode, ApprovalMode::Never); + // Operate uses the Agent baseline. let operate = base_policy_for_mode(AppMode::Operate, &prefs); assert_eq!(operate.approval_mode, ApprovalMode::Never); diff --git a/crates/tui/src/tui/footer_ui.rs b/crates/tui/src/tui/footer_ui.rs index f0d13d8f9..de56913c1 100644 --- a/crates/tui/src/tui/footer_ui.rs +++ b/crates/tui/src/tui/footer_ui.rs @@ -353,7 +353,11 @@ mod tests { resume_session_id: None, initial_input: None, }; - App::new(options, &Config::default()) + // Pin sidebar so dogfood machines with Agents-visible settings.toml + // do not hide the footer agents chip this test asserts. + let mut app = App::new(options, &Config::default()); + app.sidebar_focus = crate::tui::app::SidebarFocus::Hidden; + app } #[test] diff --git a/crates/tui/src/tui/hotbar/actions.rs b/crates/tui/src/tui/hotbar/actions.rs index 476e0ee0c..f76c68c8b 100644 --- a/crates/tui/src/tui/hotbar/actions.rs +++ b/crates/tui/src/tui/hotbar/actions.rs @@ -581,27 +581,13 @@ impl HotbarActionSource for BuiltinHotbarActionSource { "Switch the conversation into Act (Agent) mode.", AppHotbarKind::Mode(AppMode::Agent), )); - registry.register(AppHotbarAction::new( - "mode.multitask", - "multi", - "Multitask mode", - "Switch into Multitask delegation mode.", - AppHotbarKind::Mode(AppMode::Multitask), - )); registry.register(AppHotbarAction::new( "mode.operate", "operate", "Operate mode", - "Switch into Operate fleet/workflow conductor mode.", + "Coordinate workflows, spawn workers, wait, and dispatch more work.", AppHotbarKind::Mode(AppMode::Operate), )); - registry.register(AppHotbarAction::new( - "mode.yolo", - "yolo", - "YOLO (deprecated)", - "Legacy shim: Act mode with Full Access permissions.", - AppHotbarKind::Mode(AppMode::Yolo), - )); registry.register(AppHotbarAction::new( "reasoning.cycle", "reason", @@ -922,7 +908,7 @@ impl AppHotbarAction { AppHotbarKind::Mode(AppMode::Plan) => MessageId::HotbarActionModePlanName, AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentName, AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloName, - AppHotbarKind::Mode(AppMode::Auto | AppMode::Multitask | AppMode::Operate) => { + AppHotbarKind::Mode(AppMode::Auto | AppMode::Operate) => { return None; } AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleName, @@ -940,7 +926,7 @@ impl AppHotbarAction { AppHotbarKind::Mode(AppMode::Plan) => MessageId::HotbarActionModePlanDescription, AppHotbarKind::Mode(AppMode::Agent) => MessageId::HotbarActionModeAgentDescription, AppHotbarKind::Mode(AppMode::Yolo) => MessageId::HotbarActionModeYoloDescription, - AppHotbarKind::Mode(AppMode::Auto | AppMode::Multitask | AppMode::Operate) => { + AppHotbarKind::Mode(AppMode::Auto | AppMode::Operate) => { return None; } AppHotbarKind::ReasoningCycle => MessageId::HotbarActionReasoningCycleDescription, @@ -2020,10 +2006,8 @@ mod tests { vec![ "filetree.toggle", "mode.agent", - "mode.multitask", "mode.operate", "mode.plan", - "mode.yolo", "palette.open", "reasoning.cycle", "session.compact", diff --git a/crates/tui/src/tui/provider_picker.rs b/crates/tui/src/tui/provider_picker.rs index b9c192ff1..0775a1d96 100644 --- a/crates/tui/src/tui/provider_picker.rs +++ b/crates/tui/src/tui/provider_picker.rs @@ -1128,6 +1128,16 @@ impl ProviderPickerView { active: ApiProvider, config: &Config, runtime_status: Option, + ) -> Self { + Self::new_with_runtime_status_and_memory(active, config, runtime_status, None) + } + + #[must_use] + pub fn new_with_runtime_status_and_memory( + active: ApiProvider, + config: &Config, + runtime_status: Option, + memory: Option<&crate::tui::app::ProviderPickerMemory>, ) -> Self { // Present providers in the shared metadata display order (#3076). The // active provider is highlighted via `selected_idx` below, so it is @@ -1166,7 +1176,7 @@ impl ProviderPickerView { } else { ProviderListView::Catalog }; - Self { + let mut picker = Self { rows, selected_idx, stage: Stage::List, @@ -1179,6 +1189,37 @@ impl ProviderPickerView { custom_provider_base_url: String::new(), custom_provider_model: String::new(), custom_provider_api_key_env: String::new(), + }; + picker.restore_memory(memory); + picker + } + + /// Restore browsing context from the last dismissed `/provider` picker. + fn restore_memory(&mut self, memory: Option<&crate::tui::app::ProviderPickerMemory>) { + let Some(memory) = memory else { + return; + }; + if memory.catalog_view { + self.view = ProviderListView::Catalog; + } + if let Some(remembered_id) = memory.selected_provider_id.as_deref() { + if let Some(idx) = self + .rows + .iter() + .position(|row| row.provider_id == remembered_id) + { + if self.row_visible(idx) || memory.catalog_view { + if memory.catalog_view { + self.view = ProviderListView::Catalog; + } + self.selected_idx = idx; + } + } + } + if !self.rows.is_empty() && !self.row_visible(self.selected_idx) { + self.selected_idx = (0..self.rows.len()) + .find(|idx| self.row_visible(*idx)) + .unwrap_or(0); } } @@ -1935,7 +1976,13 @@ impl ModalView for ProviderPickerView { self.update_query(String::new()); ViewAction::None } - KeyCode::Esc => ViewAction::Close, + KeyCode::Esc => ViewAction::EmitAndClose(ViewEvent::ProviderPickerDismissed { + catalog_view: self.view == ProviderListView::Catalog, + selected_provider_id: self + .rows + .get(self.selected_idx) + .map(|row| row.provider_id.clone()), + }), KeyCode::Up => { self.move_up(); ViewAction::None @@ -3650,11 +3697,14 @@ mod tests { } #[test] - fn list_esc_closes_without_emitting() { + fn list_esc_emits_dismiss_memory() { let config = Config::default(); let mut picker = ProviderPickerView::new(ApiProvider::Deepseek, &config); let action = picker.handle_key(key(KeyCode::Esc)); - assert!(matches!(action, ViewAction::Close)); + assert!(matches!( + action, + ViewAction::EmitAndClose(ViewEvent::ProviderPickerDismissed { .. }) + )); } #[test] @@ -3797,4 +3847,43 @@ mod tests { "selected provider row should use a visible continuous highlight" ); } + + #[test] + fn esc_reports_browsing_context_and_reopen_restores_it() { + let config = Config::default(); + let mut picker = ProviderPickerView::new(ApiProvider::Deepseek, &config); + // Browse full catalog and move highlight. + picker.handle_key(key(KeyCode::Char('a'))); + picker.handle_key(key(KeyCode::Down)); + let remembered_id = picker.rows[picker.selected_idx].provider_id.clone(); + let action = picker.handle_key(key(KeyCode::Esc)); + let ViewAction::EmitAndClose(ViewEvent::ProviderPickerDismissed { + catalog_view, + selected_provider_id, + }) = action + else { + panic!("expected ProviderPickerDismissed"); + }; + assert!(catalog_view); + assert_eq!( + selected_provider_id.as_deref(), + Some(remembered_id.as_str()) + ); + + let memory = crate::tui::app::ProviderPickerMemory { + catalog_view, + selected_provider_id, + }; + let reopened = ProviderPickerView::new_with_runtime_status_and_memory( + ApiProvider::Deepseek, + &config, + None, + Some(&memory), + ); + assert_eq!(reopened.view, ProviderListView::Catalog); + assert_eq!( + reopened.rows[reopened.selected_idx].provider_id, + remembered_id + ); + } } diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 09ca08bf2..c913802fb 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -8293,10 +8293,11 @@ async fn apply_command_result( if app.view_stack.top_kind() != Some(ModalKind::ProviderPicker) { let runtime_status = query_provider_runtime_status(engine_handle).await; app.view_stack.push( - crate::tui::provider_picker::ProviderPickerView::new_with_runtime_status( + crate::tui::provider_picker::ProviderPickerView::new_with_runtime_status_and_memory( app.api_provider, config, runtime_status, + app.provider_picker_memory.as_ref(), ), ); } @@ -10654,6 +10655,15 @@ async fn handle_view_events( selected_row_id, }); } + ViewEvent::ProviderPickerDismissed { + catalog_view, + selected_provider_id, + } => { + app.provider_picker_memory = Some(crate::tui::app::ProviderPickerMemory { + catalog_view, + selected_provider_id, + }); + } ViewEvent::ProviderPickerApplied { provider, provider_id, diff --git a/crates/tui/src/tui/views/mod.rs b/crates/tui/src/tui/views/mod.rs index 7b80d8a0a..c9e51a372 100644 --- a/crates/tui/src/tui/views/mod.rs +++ b/crates/tui/src/tui/views/mod.rs @@ -579,6 +579,12 @@ pub enum ViewEvent { catalog_view: bool, selected_row_id: Option, }, + /// Emitted by the `/provider` picker on Esc so the next open can restore + /// the browsing context — view mode and highlighted row. + ProviderPickerDismissed { + catalog_view: bool, + selected_provider_id: Option, + }, /// Emitted by the `/provider` picker when the user selects a provider /// that already has credentials — the handler should perform the same /// switch as `AppAction::SwitchProvider`. diff --git a/crates/tui/src/tui/views/mode_picker.rs b/crates/tui/src/tui/views/mode_picker.rs index 23c57decd..0beffea85 100644 --- a/crates/tui/src/tui/views/mode_picker.rs +++ b/crates/tui/src/tui/views/mode_picker.rs @@ -1,4 +1,4 @@ -//! `/mode` picker for Agent / Plan / YOLO. +//! `/mode` picker for Act / Plan / Operate. use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ @@ -254,28 +254,24 @@ mod tests { #[test] fn number_keys_select_modes() { - // Wave 7 picker roster: 1 Act, 2 Plan, 3 Multitask, 5 Operate. + // Visible roster: 1 Act, 2 Plan, 3 Operate. No Multitask / YOLO / gap. let mut view = ModePickerView::new(AppMode::Agent, Locale::En); let action = view.handle_key(KeyEvent::new(KeyCode::Char('3'), KeyModifiers::NONE)); match action { ViewAction::EmitAndClose(ViewEvent::ModeSelected { mode }) => { - assert_eq!(mode, AppMode::Multitask); + assert_eq!(mode, AppMode::Operate); } other => panic!("expected ModeSelected, got {other:?}"), } - // The deprecated YOLO slot (4) is not offered by the picker. + // Legacy YOLO shorthand (4) is not offered by the picker. let mut view = ModePickerView::new(AppMode::Agent, Locale::En); let action = view.handle_key(KeyEvent::new(KeyCode::Char('4'), KeyModifiers::NONE)); assert!(matches!(action, ViewAction::None)); + // Old Operate number (5) is gone — no numeric gaps. let mut view = ModePickerView::new(AppMode::Agent, Locale::En); let action = view.handle_key(KeyEvent::new(KeyCode::Char('5'), KeyModifiers::NONE)); - match action { - ViewAction::EmitAndClose(ViewEvent::ModeSelected { mode }) => { - assert_eq!(mode, AppMode::Operate); - } - other => panic!("expected ModeSelected, got {other:?}"), - } + assert!(matches!(action, ViewAction::None)); } } diff --git a/crates/tui/src/tui/widgets/footer.rs b/crates/tui/src/tui/widgets/footer.rs index c192a914e..119a946fb 100644 --- a/crates/tui/src/tui/widgets/footer.rs +++ b/crates/tui/src/tui/widgets/footer.rs @@ -302,22 +302,15 @@ impl FooterProps { fn mode_style(app: &App) -> (&'static str, Color) { let label = match app.mode { - AppMode::Agent => "act", - AppMode::Auto => "act", - AppMode::Yolo => "yolo", + AppMode::Agent | AppMode::Auto | AppMode::Yolo => "act", AppMode::Plan => "plan", - AppMode::Multitask => "multitask", AppMode::Operate => "operate", }; - // Every mode gets its own badge color (dogfood A7): Operate previously - // borrowed the YOLO red (reads as "dangerous") and Multitask the Act - // blue (mode change was invisible in the footer). + // Visible modes get distinct badge colors (dogfood A7). YOLO is no longer + // a visible mode — it remaps to Act + bypass permissions. let color = match app.mode { - AppMode::Agent => app.ui_theme.mode_agent, - AppMode::Auto => app.ui_theme.mode_agent, - AppMode::Yolo => app.ui_theme.mode_yolo, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => app.ui_theme.mode_agent, AppMode::Plan => app.ui_theme.mode_plan, - AppMode::Multitask => app.ui_theme.mode_multitask, AppMode::Operate => app.ui_theme.mode_operate, }; (label, color) @@ -980,8 +973,9 @@ mod tests { let mut app = make_app(); let cases = [ (AppMode::Agent, "act", palette::MODE_AGENT), - (AppMode::Yolo, "yolo", palette::MODE_YOLO), + (AppMode::Yolo, "act", palette::MODE_AGENT), (AppMode::Plan, "plan", palette::MODE_PLAN), + (AppMode::Operate, "operate", palette::MODE_OPERATE), ]; for (mode, expected_label, expected_color) in cases { app.mode = mode; diff --git a/crates/tui/src/tui/widgets/header.rs b/crates/tui/src/tui/widgets/header.rs index d28e0a5ef..e8cbb3524 100644 --- a/crates/tui/src/tui/widgets/header.rs +++ b/crates/tui/src/tui/widgets/header.rs @@ -178,18 +178,16 @@ impl<'a> HeaderWidget<'a> { fn mode_color(mode: AppMode) -> Color { match mode { - AppMode::Agent | AppMode::Auto | AppMode::Multitask => palette::MODE_AGENT, - AppMode::Yolo | AppMode::Operate => palette::MODE_YOLO, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => palette::MODE_AGENT, AppMode::Plan => palette::MODE_PLAN, + AppMode::Operate => palette::MODE_OPERATE, } } fn mode_name(mode: AppMode) -> &'static str { match mode { - AppMode::Agent | AppMode::Auto => "Act", - AppMode::Yolo => "Yolo", + AppMode::Agent | AppMode::Auto | AppMode::Yolo => "Act", AppMode::Plan => "Plan", - AppMode::Multitask => "Multitask", AppMode::Operate => "Operate", } } diff --git a/crates/tui/src/tui/widgets/mod.rs b/crates/tui/src/tui/widgets/mod.rs index c3b6ba1b3..af2b2dbaa 100644 --- a/crates/tui/src/tui/widgets/mod.rs +++ b/crates/tui/src/tui/widgets/mod.rs @@ -619,9 +619,9 @@ impl<'a> ComposerWidget<'a> { fn mode_color(&self) -> Color { match self.app.mode { - AppMode::Agent | AppMode::Auto | AppMode::Multitask => palette::MODE_AGENT, - AppMode::Yolo | AppMode::Operate => palette::MODE_YOLO, + AppMode::Agent | AppMode::Auto | AppMode::Yolo => palette::MODE_AGENT, AppMode::Plan => palette::MODE_PLAN, + AppMode::Operate => palette::MODE_OPERATE, } } diff --git a/crates/tui/tests/qa_pty.rs b/crates/tui/tests/qa_pty.rs index d026f578b..d795c2678 100644 --- a/crates/tui/tests/qa_pty.rs +++ b/crates/tui/tests/qa_pty.rs @@ -95,7 +95,6 @@ fn assert_viewport_starts_at_top(frame: &qa_harness::Frame) { frame.row(0).contains("Plan") || frame.row(0).contains("Act") || frame.row(0).contains("Agent") - || frame.row(0).contains("Multitask") || frame.row(0).contains("Operate") || frame.row(0).contains("Yolo") || frame.row(0).contains("DeepSeek"), diff --git a/docs/CONTRIBUTORS.md b/docs/CONTRIBUTORS.md index 2c786a721..1e63fd2e2 100644 --- a/docs/CONTRIBUTORS.md +++ b/docs/CONTRIBUTORS.md @@ -34,6 +34,10 @@ The v0.8.68 maintenance lane started with post-release cleanup on `main`: runtim thread persistence, terminal selection, UTF-8 edit handling, README discovery, and dead-code removal landed alongside Cursor dogfood evidence for v0.8.67. +- **[Jeffrey Luna / Mr-Moon121](https://github.com/Mr-Moon121)** — anti-polling + constitution for sub-agent waiting (harvested into #4097 / PR #4229 from + PR #4098) + - **[MXAntian](https://github.com/MXAntian)** — persist compaction summaries into thread records so `/v1` engine reloads keep compacted context (#4091) - **[nightt5879](https://github.com/nightt5879)** — keep native terminal From 3730ce5564574ea91404d7c66f9f3856d92a35f5 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Wed, 8 Jul 2026 11:59:33 -0700 Subject: [PATCH 2/2] =?UTF-8?q?fix(tui):=20align=20Multitask=E2=86=92Opera?= =?UTF-8?q?te=20tests=20with=20invisible=20YOLO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DEFAULT_HOTBAR now binds mode.operate (not mode.yolo), and assertions expect Act-labeled YOLO shorthand plus Agent prompt markers so macOS CI matches the folded mode roster. Signed-off-by: Hunter B --- config.example.toml | 2 +- crates/config/src/lib.rs | 2 +- .../tui/src/commands/groups/config/config.rs | 3 ++- crates/tui/src/core/engine/tests.rs | 6 ++++-- crates/tui/src/tui/hotbar/actions.rs | 19 ++++++++----------- crates/tui/src/tui/widgets/header.rs | 9 ++++++--- docs/KEYBINDINGS.md | 2 +- 7 files changed, 23 insertions(+), 20 deletions(-) diff --git a/config.example.toml b/config.example.toml index b549e8f76..215120f94 100644 --- a/config.example.toml +++ b/config.example.toml @@ -102,7 +102,7 @@ check_for_updates = true # Optional 1-8 sidebar hotbar bindings. When no [[hotbar]] tables are present, # the TUI uses built-in defaults: # 1 voice.toggle 2 session.compact 3 mode.plan 4 mode.agent -# 5 mode.yolo 6 palette.open 7 sidebar.toggle 8 trust.toggle +# 5 mode.operate 6 palette.open 7 sidebar.toggle 8 trust.toggle # # Invalid slots are skipped with a warning, duplicate slots use the last entry, # and unknown actions are preserved so the UI can show a disabled entry. diff --git a/crates/config/src/lib.rs b/crates/config/src/lib.rs index a45dbcb63..42781e3b7 100644 --- a/crates/config/src/lib.rs +++ b/crates/config/src/lib.rs @@ -714,7 +714,7 @@ pub const DEFAULT_HOTBAR_ACTIONS: [&str; HOTBAR_SLOT_COUNT as usize] = [ "session.compact", "mode.plan", "mode.agent", - "mode.yolo", + "mode.operate", "palette.open", "sidebar.toggle", "trust.toggle", diff --git a/crates/tui/src/commands/groups/config/config.rs b/crates/tui/src/commands/groups/config/config.rs index e9ed74caa..bf3195f52 100644 --- a/crates/tui/src/commands/groups/config/config.rs +++ b/crates/tui/src/commands/groups/config/config.rs @@ -2519,7 +2519,8 @@ Parse error: permissions.toml at permissions.toml could not be parsed: expected // user settings on the host machine. let _ = mode(&mut app, Some("agent")); let result = mode(&mut app, Some("yolo")); - assert!(result.message.unwrap().contains("Switched to YOLO mode")); + // YOLO is invisible Act+Bypass shorthand — user-facing copy says Act. + assert!(result.message.unwrap().contains("Switched to Act mode")); assert_eq!(result.action, Some(AppAction::ModeChanged(AppMode::Yolo))); assert!(app.allow_shell); assert!(app.trust_mode); diff --git a/crates/tui/src/core/engine/tests.rs b/crates/tui/src/core/engine/tests.rs index dbf27194b..009af8640 100644 --- a/crates/tui/src/core/engine/tests.rs +++ b/crates/tui/src/core/engine/tests.rs @@ -3696,10 +3696,12 @@ fn mode_invariant_matrix_covers_context_catalog_subagents_and_prompt_metadata() plan_hint: false, }, ModeCase { + // YOLO remains an elevated-permission alias, but prompt/setting + // surfaces now speak Act (invisible one-way permission shorthand). name: "yolo", mode: AppMode::Yolo, - setting: "yolo", - prompt_marker: "##### Mode: YOLO", + setting: "agent", + prompt_marker: "##### Mode: Agent", shell_policy: ShellPolicy::Full, sandbox: ExpectedSandbox::DangerFullAccess, trust_mode: true, diff --git a/crates/tui/src/tui/hotbar/actions.rs b/crates/tui/src/tui/hotbar/actions.rs index f76c68c8b..ec442be9e 100644 --- a/crates/tui/src/tui/hotbar/actions.rs +++ b/crates/tui/src/tui/hotbar/actions.rs @@ -1978,7 +1978,7 @@ mod tests { (2, Some("compact")), (3, Some("plan")), (4, Some("agent")), - (5, Some("yolo")), + (5, Some("operate")), (6, Some("palette")), (7, Some("side")), (8, Some("trust")), @@ -2314,11 +2314,12 @@ mod tests { let registry = HotbarActionRegistry::with_builtins(); let plan = registry.get("mode.plan").expect("plan action"); let agent = registry.get("mode.agent").expect("agent action"); - let yolo = registry.get("mode.yolo").expect("yolo action"); + let operate = registry.get("mode.operate").expect("operate action"); let mut app = test_app(); assert!(agent.is_active(&app)); assert!(!plan.is_active(&app)); + assert!(registry.get("mode.yolo").is_none()); assert_eq!( plan.dispatch(&mut app).expect("dispatch plan"), @@ -2329,16 +2330,12 @@ mod tests { assert!(!agent.is_active(&app)); assert_eq!( - yolo.dispatch(&mut app).expect("dispatch yolo"), - HotbarDispatch::AppAction(AppAction::ModeChanged(AppMode::Yolo)) + operate.dispatch(&mut app).expect("dispatch operate"), + HotbarDispatch::AppAction(AppAction::ModeChanged(AppMode::Operate)) ); - assert!(app.allow_shell); - assert!(app.trust_mode); - // YOLO is a legacy compatibility alias: entering it resolves to Agent - // mode with the bypass mirrors set, so the Agent action lights up. - assert_eq!(app.mode, AppMode::Agent); - assert!(app.yolo); - assert!(agent.is_active(&app)); + assert_eq!(app.mode, AppMode::Operate); + assert!(operate.is_active(&app)); + assert!(!agent.is_active(&app)); } #[test] diff --git a/crates/tui/src/tui/widgets/header.rs b/crates/tui/src/tui/widgets/header.rs index e8cbb3524..8fbc182e8 100644 --- a/crates/tui/src/tui/widgets/header.rs +++ b/crates/tui/src/tui/widgets/header.rs @@ -660,6 +660,7 @@ mod tests { fn narrow_header_drops_version_chip_before_dropping_mode() { // Very tight width budget — the version is among the first // chips to disappear; the mode label must still render. + // YOLO is invisible Act+Bypass shorthand, so the chip reads "Act". let rendered = render_header( HeaderData::new( AppMode::Yolo, @@ -677,7 +678,7 @@ mod tests { "version chip should drop under width pressure: {rendered:?}", ); assert!( - rendered.contains("Yolo") || rendered.contains('Y'), + rendered.contains("Act") || rendered.contains('A'), "mode label must survive: {rendered:?}", ); } @@ -730,9 +731,11 @@ mod tests { 8, ); - assert!(rendered.trim_start().starts_with('Y')); + // YOLO renders as Act; under extreme width pressure only the first + // glyph of the mode chip remains. + assert!(rendered.trim_start().starts_with('A')); assert!(!rendered.contains("Plan")); - assert!(!rendered.contains("Agent")); + assert!(!rendered.contains("Operate")); } #[test] diff --git a/docs/KEYBINDINGS.md b/docs/KEYBINDINGS.md index 4c468fd34..3ed38893d 100644 --- a/docs/KEYBINDINGS.md +++ b/docs/KEYBINDINGS.md @@ -67,7 +67,7 @@ Fresh configs resolve to this default bar unless `[[hotbar]]` overrides it or `h | 2 | `Alt-2` | `session.compact` | `compact` | | 3 | `Alt-3` | `mode.plan` | `plan` | | 4 | `Alt-4` | `mode.agent` | `agent` | -| 5 | `Alt-5` | `mode.yolo` | `yolo` | +| 5 | `Alt-5` | `mode.operate` | `operate` | | 6 | `Alt-6` | `palette.open` | `palette` | | 7 | `Alt-7` | `sidebar.toggle` | `side` | | 8 | `Alt-8` | `trust.toggle` | `trust` |