From fa42ac094dca23c6ae6d05e1487f3e0c7daa29ad Mon Sep 17 00:00:00 2001 From: brooklyn! Date: Sun, 7 Jun 2026 20:57:08 -0500 Subject: [PATCH] feat(desktop): Shift+click the status-bar zap to toggle YOLO globally (#41666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status-bar zap currently toggles per-session approval bypass (the same scope as the TUI's Shift+Tab). This adds a global escape hatch: Shift+clicking the zap flips the persistent approvals.mode in config.yaml between "off" (bypass on) and "manual" (bypass off), affecting every session, the CLI, the TUI, and cron — and it survives restarts. - statusbar-controls: thread the click's shiftKey through onSelect via a new StatusbarSelectModifiers arg. - yolo-session: add setGlobalYolo() that calls config.set with scope="global". - use-statusbar-items: branch toggleYolo on modifiers.shiftKey; plain click stays per-session, Shift+click goes global. - tui_gateway config.set "yolo" key: add scope="global" that reads/writes approvals.mode through the gateway's own (mtime-cached) config view, honors an explicit value, and re-emits session.info to every live session so each window's zap reflects the flip immediately. - i18n: tooltip copy in en/ja/zh/zh-hant notes Shift+click toggles globally. Tests: two new tui_gateway tests cover the global toggle and explicit-value paths; existing session/process-scope yolo tests still pass. --- .../app/shell/hooks/use-statusbar-items.tsx | 51 ++++++++---- .../src/app/shell/statusbar-controls.tsx | 10 ++- apps/desktop/src/i18n/en.ts | 4 +- apps/desktop/src/i18n/ja.ts | 4 +- apps/desktop/src/i18n/zh-hant.ts | 4 +- apps/desktop/src/i18n/zh.ts | 4 +- apps/desktop/src/lib/yolo-session.ts | 24 ++++++ tests/test_tui_gateway_server.py | 60 ++++++++++++++ tui_gateway/server.py | 79 +++++++++++++------ 9 files changed, 188 insertions(+), 52 deletions(-) diff --git a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx index c700cb51019..80843a00f09 100644 --- a/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx +++ b/apps/desktop/src/app/shell/hooks/use-statusbar-items.tsx @@ -4,6 +4,7 @@ import { useCallback, useMemo } from 'react' import type { CommandCenterSection } from '@/app/command-center' import { GatewayMenuPanel } from '@/app/shell/gateway-menu-panel' +import { useI18n } from '@/i18n' import { Activity, AlertCircle, @@ -16,12 +17,11 @@ import { Zap, ZapFilled } from '@/lib/icons' -import { useI18n } from '@/i18n' import { formatModelStatusLabel } from '@/lib/model-status-label' import type { RuntimeReadinessResult } from '@/lib/runtime-readiness' import { contextBarLabel, LiveDuration, usageContextLabel } from '@/lib/statusbar' import { cn } from '@/lib/utils' -import { setSessionYolo } from '@/lib/yolo-session' +import { setGlobalYolo, setSessionYolo } from '@/lib/yolo-session' import { $desktopActionTasks } from '@/store/activity' import { $previewServerRestartStatus } from '@/store/preview' import { @@ -44,7 +44,7 @@ import { $desktopVersion, $updateApply, $updateStatus, setUpdateOverlayOpen } fr import type { StatusResponse } from '@/types/hermes' import { CRON_ROUTE } from '../../routes' -import type { StatusbarItem } from '../statusbar-controls' +import type { StatusbarItem, StatusbarSelectModifiers } from '../statusbar-controls' interface StatusbarItemsOptions { agentsOpen: boolean @@ -105,22 +105,39 @@ export function useStatusbarItems({ // Per-session approval bypass (same scope as the TUI's Shift+Tab). On a // new-chat draft (no runtime session yet) we arm locally; the session-create // path applies it once the backend session exists. - const toggleYolo = useCallback(async () => { - const next = !$yoloActive.get() - const sid = $activeSessionId.get() + // + // Shift+click flips the GLOBAL approvals.mode instead — a persistent, + // all-sessions/CLI/TUI/cron bypass that survives restarts. + const toggleYolo = useCallback( + async (modifiers?: StatusbarSelectModifiers) => { + const next = !$yoloActive.get() - setYoloActive(next) + setYoloActive(next) - if (!sid) { - return - } + if (modifiers?.shiftKey) { + try { + await setGlobalYolo(requestGateway, next) + } catch { + setYoloActive(!next) + } - try { - await setSessionYolo(requestGateway, sid, next) - } catch { - setYoloActive(!next) - } - }, [requestGateway]) + return + } + + const sid = $activeSessionId.get() + + if (!sid) { + return + } + + try { + await setSessionYolo(requestGateway, sid, next) + } catch { + setYoloActive(!next) + } + }, + [requestGateway] + ) const showYoloToggle = gatewayState === 'open' && (!!activeSessionId || freshDraftReady) @@ -333,7 +350,7 @@ export function useStatusbarItems({ ), id: 'yolo', - onSelect: () => void toggleYolo(), + onSelect: modifiers => void toggleYolo(modifiers), title: yoloActive ? copy.yoloOn : copy.yoloOff, variant: 'action' }, diff --git a/apps/desktop/src/app/shell/statusbar-controls.tsx b/apps/desktop/src/app/shell/statusbar-controls.tsx index 6a103160e65..dc3a4d77382 100644 --- a/apps/desktop/src/app/shell/statusbar-controls.tsx +++ b/apps/desktop/src/app/shell/statusbar-controls.tsx @@ -35,12 +35,16 @@ export interface StatusbarItem { menuClassName?: string menuContent?: ReactNode menuItems?: readonly StatusbarMenuItem[] - onSelect?: () => void + onSelect?: (modifiers: StatusbarSelectModifiers) => void title?: string to?: string variant?: 'action' | 'link' | 'menu' | 'text' } +export interface StatusbarSelectModifiers { + shiftKey: boolean +} + export type StatusbarItemSide = 'left' | 'right' export type SetStatusbarItemGroup = (id: string, items: readonly StatusbarItem[], side?: StatusbarItemSide) => void @@ -170,12 +174,12 @@ function StatusbarItemView({ item, navigate }: { item: StatusbarItem; navigate: