feat(desktop): Shift+click the status-bar zap to toggle YOLO globally (#41666)

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.
This commit is contained in:
brooklyn!
2026-06-07 20:57:08 -05:00
committed by GitHub
parent 30c7913617
commit fa42ac094d
9 changed files with 188 additions and 52 deletions
@@ -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({
<Zap className="size-3.5 shrink-0 opacity-70" />
),
id: 'yolo',
onSelect: () => void toggleYolo(),
onSelect: modifiers => void toggleYolo(modifiers),
title: yoloActive ? copy.yoloOn : copy.yoloOff,
variant: 'action'
},
@@ -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:
<button
className={cn(STATUSBAR_ACTION_CLASS, item.className)}
disabled={item.disabled}
onClick={() => {
onClick={event => {
if (item.to) {
navigate(item.to)
}
item.onSelect?.()
item.onSelect?.({ shiftKey: event.shiftKey })
}}
type="button"
>
+2 -2
View File
@@ -1463,8 +1463,8 @@ export const en: Translations = {
contextUsage: 'Context usage',
session: 'Session',
runtimeSessionElapsed: 'Runtime session elapsed',
yoloOn: 'YOLO on — auto-approving dangerous commands. Click to turn off.',
yoloOff: 'YOLO off — click to auto-approve dangerous commands.',
yoloOn: 'YOLO on — auto-approving dangerous commands. Click to turn off. Shift+click toggles it globally.',
yoloOff: 'YOLO off — click to auto-approve dangerous commands. Shift+click toggles it globally.',
modelNone: 'none',
noModel: 'no model',
switchModel: 'Switch model',
+2 -2
View File
@@ -1606,8 +1606,8 @@ export const ja = defineLocale({
contextUsage: 'コンテキスト使用状況',
session: 'セッション',
runtimeSessionElapsed: 'ランタイムセッション経過時間',
yoloOn: 'YOLO オン — 危険なコマンドを自動承認中。クリックでオフに。',
yoloOff: 'YOLO オフ — クリックで危険なコマンドを自動承認。',
yoloOn: 'YOLO オン — 危険なコマンドを自動承認中。クリックでオフに。Shift+クリックで全体に切り替え。',
yoloOff: 'YOLO オフ — クリックで危険なコマンドを自動承認。Shift+クリックで全体に切り替え。',
modelNone: 'なし',
noModel: 'モデルなし',
switchModel: 'モデルを切り替え',
+2 -2
View File
@@ -1567,8 +1567,8 @@ export const zhHant = defineLocale({
contextUsage: '上下文使用量',
session: '工作階段',
runtimeSessionElapsed: '執行時工作階段已用時間',
yoloOn: 'YOLO 已開啟 — 自動核准危險指令。點擊關閉。',
yoloOff: 'YOLO 已關閉 — 點擊自動核准危險指令。',
yoloOn: 'YOLO 已開啟 — 自動核准危險指令。點擊關閉。Shift+點擊可全域切換。',
yoloOff: 'YOLO 已關閉 — 點擊自動核准危險指令。Shift+點擊可全域切換。',
modelNone: '無',
noModel: '無模型',
switchModel: '切換模型',
+2 -2
View File
@@ -1644,8 +1644,8 @@ export const zh: Translations = {
contextUsage: '上下文用量',
session: '会话',
runtimeSessionElapsed: '运行时会话已用时间',
yoloOn: 'YOLO 已开启 - 自动批准危险命令。点击关闭。',
yoloOff: 'YOLO 已关闭 - 点击自动批准危险命令。',
yoloOn: 'YOLO 已开启 - 自动批准危险命令。点击关闭。Shift+点击可全局切换。',
yoloOff: 'YOLO 已关闭 - 点击自动批准危险命令。Shift+点击可全局切换。',
modelNone: '无',
noModel: '无模型',
switchModel: '切换模型',
+24
View File
@@ -24,3 +24,27 @@ export async function setSessionYolo(
return active
}
/**
* Toggle GLOBAL YOLO (approval bypass) via gateway `config.set` with
* `scope: 'global'`. This 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. Triggered by
* Shift+clicking the status-bar zap.
*/
export async function setGlobalYolo(
requestGateway: GatewayRequester,
enabled: boolean
): Promise<boolean> {
const result = await requestGateway<{ value?: string }>('config.set', {
key: 'yolo',
scope: 'global',
value: enabled ? '1' : '0'
})
const active = result?.value === '1'
setYoloActive(active)
return active
}