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:
@@ -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"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user