mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): put YOLO in ⌘K and show each toggle's live state
YOLO had a status-bar zap and a slash command but was never registered as a palette contribution, so ⌘K couldn't reach it. Adding it exposed the wider gap: "Toggle status bar" doesn't say which way it will go. Rows already carry a muted `detail` slot, so paletteToggle fills it with on/off — no new chrome, and the verb stays. Status bar, logs, and layout edit mode go through the same helper.
This commit is contained in:
parent
8e1debd5ed
commit
b86ae0d108
4 changed files with 111 additions and 39 deletions
|
|
@ -18,6 +18,12 @@ export interface PaletteContribution {
|
|||
icon?: IconComponent
|
||||
keywords?: string[]
|
||||
run: () => void
|
||||
/**
|
||||
* Muted text after the label — the live state the row acts on. A function
|
||||
* because contributions register once at boot while that state keeps moving;
|
||||
* the palette re-reads it on open.
|
||||
*/
|
||||
detail?: () => string
|
||||
}
|
||||
|
||||
/** Contributed palette rows, with stable render keys. */
|
||||
|
|
@ -26,3 +32,26 @@ export function usePaletteContributions(): Array<PaletteContribution & { key: st
|
|||
.map(c => ({ key: `${c.source ?? 'core'}:${c.id}`, ...(c.data as PaletteContribution) }))
|
||||
.filter(item => Boolean(item.label && item.run))
|
||||
}
|
||||
|
||||
/**
|
||||
* A binary setting as one palette row: `Toggle status bar` with the live state
|
||||
* trailing in muted text. The verb says what the row does, the detail says
|
||||
* which way it will go — neither alone is enough.
|
||||
*/
|
||||
export function paletteToggle(
|
||||
spec: Omit<PaletteContribution, 'detail' | 'run'> & {
|
||||
get: () => boolean
|
||||
set: (enabled: boolean) => void
|
||||
}
|
||||
) {
|
||||
const { get, keywords = [], set, ...rest } = spec
|
||||
|
||||
const data: PaletteContribution = {
|
||||
...rest,
|
||||
detail: () => (get() ? 'on' : 'off'),
|
||||
keywords: [...keywords, 'on', 'off', 'enable', 'disable'],
|
||||
run: () => set(!get())
|
||||
}
|
||||
|
||||
return { id: data.id, area: PALETTE_AREA, data }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ import {
|
|||
import { $bindings } from '@/store/keybinds'
|
||||
import { openPetGenerate } from '@/store/pet-generate'
|
||||
import { requestStartWorkSession } from '@/store/projects'
|
||||
import { $connection } from '@/store/session'
|
||||
import { $connection, $yoloActive } from '@/store/session'
|
||||
import { runGatewayRestart } from '@/store/system-actions'
|
||||
import {
|
||||
$backendUpdateApply,
|
||||
|
|
@ -374,6 +374,9 @@ export function CommandPalette() {
|
|||
const clientApply = useStore($updateApply)
|
||||
const backendStatus = useStore($backendUpdateStatus)
|
||||
const backendApply = useStore($backendUpdateApply)
|
||||
// Contributed on/off rows check the live half (YOLO on / YOLO off), so the
|
||||
// groups have to rebuild when that state moves under an open palette.
|
||||
const yoloActive = useStore($yoloActive)
|
||||
|
||||
const updateVersionLabel = useMemo(() => {
|
||||
const backend = connection?.mode === 'remote'
|
||||
|
|
@ -704,6 +707,9 @@ export function CommandPalette() {
|
|||
heading: cc.commands,
|
||||
items: contributedItems.map(item => ({
|
||||
action: item.action,
|
||||
// Resolved per palette open (see the `open` dep below) so a
|
||||
// state-describing row can't show a state it left.
|
||||
detail: item.detail?.(),
|
||||
icon: item.icon ?? Zap,
|
||||
id: item.key,
|
||||
keywords: item.keywords,
|
||||
|
|
@ -714,7 +720,7 @@ export function CommandPalette() {
|
|||
]
|
||||
: [])
|
||||
]
|
||||
}, [contributedItems, go, settingsSectionLabel, t, updateVersionLabel, worktrees])
|
||||
}, [contributedItems, go, open, settingsSectionLabel, t, updateVersionLabel, worktrees, yoloActive])
|
||||
|
||||
// The long, granular lists (settings fields, API keys, MCP servers, archived
|
||||
// chats) only surface once the user types — otherwise they'd bury the
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ import type { CSSProperties, ReactElement, PointerEvent as ReactPointerEvent } f
|
|||
|
||||
import { PREVIEW_RAIL_MAX_WIDTH, PREVIEW_RAIL_MIN_WIDTH } from '@/app/chat/right-rail'
|
||||
import { SessionStatusDot } from '@/app/chat/session-status-dot'
|
||||
import { PALETTE_AREA, type PaletteContribution } from '@/app/command-palette/contrib'
|
||||
import { PALETTE_AREA, type PaletteContribution, paletteToggle } from '@/app/command-palette/contrib'
|
||||
import { type StatusbarItem } from '@/app/shell/statusbar-controls'
|
||||
import { IdleMount } from '@/components/idle-mount'
|
||||
import { toggleLayoutEditMode } from '@/components/pane-shell/edit-mode'
|
||||
import { $layoutEditMode, toggleLayoutEditMode } from '@/components/pane-shell/edit-mode'
|
||||
import { allPaneIds, group, groupLeafIds, split } from '@/components/pane-shell/tree/model'
|
||||
import { LayoutTreeRoot } from '@/components/pane-shell/tree/renderer'
|
||||
import type { DoubleTapContext } from '@/components/pane-shell/tree/renderer/drag-session'
|
||||
|
|
@ -38,9 +38,10 @@ import { useContributions } from '@/contrib/react/use-contributions'
|
|||
import { registry } from '@/contrib/registry'
|
||||
import { discoverRuntimePlugins } from '@/contrib/runtime-loader'
|
||||
import { sessionTitle as storedSessionTitle } from '@/lib/chat-runtime'
|
||||
import { LayoutDashboard, PanelBottom } from '@/lib/icons'
|
||||
import { LayoutDashboard, PanelBottom, Zap } from '@/lib/icons'
|
||||
import { type KeybindContribution, KEYBINDS_AREA } from '@/lib/keybinds/actions'
|
||||
import { Codecs, persistentAtom } from '@/lib/persisted'
|
||||
import { setYoloEnabled } from '@/lib/yolo-session'
|
||||
import { pruneComposerPopoutZones } from '@/store/composer-popout'
|
||||
import {
|
||||
$fileBrowserOpen,
|
||||
|
|
@ -56,9 +57,9 @@ import {
|
|||
} from '@/store/layout'
|
||||
import { $previewOpenRequest, $previewTabs, closeRightRail } from '@/store/preview'
|
||||
import { $reviewOpen, closeReview, REVIEW_PANE_ID } from '@/store/review'
|
||||
import { $currentCwd, $selectedStoredSessionId, $sessions, sessionMatchesStoredId } from '@/store/session'
|
||||
import { $currentCwd, $selectedStoredSessionId, $sessions, $yoloActive, sessionMatchesStoredId } from '@/store/session'
|
||||
import { watchSessionPins } from '@/store/session-pin-sync'
|
||||
import { $statusbarVisible, toggleStatusbarVisible } from '@/store/statusbar-prefs'
|
||||
import { $statusbarVisible } from '@/store/statusbar-prefs'
|
||||
|
||||
import type { SessionDragPayload } from '../chat/composer/inline-refs'
|
||||
import { watchRouteTiles } from '../chat/route-tile'
|
||||
|
|
@ -231,12 +232,12 @@ registry.registerMany([
|
|||
},
|
||||
{
|
||||
// Optional chrome — in NO default layout. Adoption stacks it with the
|
||||
// terminal; $logsOpen (default off, ⌘K "Toggle logs") reveals it.
|
||||
// terminal; $logsOpen (default off, ⌘K "Logs on") reveals it.
|
||||
id: 'logs',
|
||||
area: 'panes',
|
||||
title: 'logs',
|
||||
// revealOnPreset: the Quad layout places logs, so applying it turns the
|
||||
// logs pane on (like a ⌘K "Toggle logs") instead of leaving it collapsed.
|
||||
// logs pane on (like a ⌘K "Logs on") instead of leaving it collapsed.
|
||||
data: { placement: 'bottom', height: '20vh', minHeight: '7.5rem', maxHeight: '80vh', revealOnPreset: true },
|
||||
render: () => idle(<LogsPane />)
|
||||
}
|
||||
|
|
@ -267,18 +268,15 @@ registry.registerMany([
|
|||
run: toggleLayoutEditMode
|
||||
} satisfies KeybindContribution
|
||||
},
|
||||
{
|
||||
paletteToggle({
|
||||
id: 'layout.editMode',
|
||||
area: PALETTE_AREA,
|
||||
data: {
|
||||
id: 'layout.editMode',
|
||||
label: 'Toggle layout edit mode',
|
||||
action: 'layout.editMode',
|
||||
icon: LayoutDashboard,
|
||||
keywords: ['layout', 'zones', 'panes', 'edit', 'rearrange'],
|
||||
run: toggleLayoutEditMode
|
||||
} satisfies PaletteContribution
|
||||
},
|
||||
label: 'Toggle layout edit mode',
|
||||
action: 'layout.editMode',
|
||||
icon: LayoutDashboard,
|
||||
keywords: ['layout', 'zones', 'panes', 'edit', 'rearrange'],
|
||||
get: () => $layoutEditMode.get(),
|
||||
set: enabled => $layoutEditMode.set(enabled)
|
||||
}),
|
||||
// The agent's write -> see loop: rescan <hermes home>/desktop-plugins
|
||||
// without relaunching (same-id reloads dispose the previous incarnation).
|
||||
{
|
||||
|
|
@ -304,18 +302,15 @@ registry.registerMany([
|
|||
},
|
||||
// Hiding the bar removes the surface that would otherwise offer it back, so
|
||||
// ⌘K is the guaranteed door in (alongside the rebindable ⌘⇧S).
|
||||
{
|
||||
paletteToggle({
|
||||
id: 'view.toggleStatusbar',
|
||||
area: PALETTE_AREA,
|
||||
data: {
|
||||
id: 'view.toggleStatusbar',
|
||||
label: 'Toggle status bar',
|
||||
action: 'view.toggleStatusbar',
|
||||
icon: PanelBottom,
|
||||
keywords: ['status bar', 'statusbar', 'bottom bar', 'hide', 'show', 'chrome'],
|
||||
run: toggleStatusbarVisible
|
||||
} satisfies PaletteContribution
|
||||
},
|
||||
label: 'Toggle status bar',
|
||||
action: 'view.toggleStatusbar',
|
||||
icon: PanelBottom,
|
||||
keywords: ['status bar', 'statusbar', 'bottom bar', 'hide', 'show', 'chrome'],
|
||||
get: () => $statusbarVisible.get(),
|
||||
set: enabled => $statusbarVisible.set(enabled)
|
||||
}),
|
||||
// The keybind panel's non-titlebar door (the keyboard icon is gone).
|
||||
{
|
||||
id: 'keybinds.panel',
|
||||
|
|
@ -599,16 +594,29 @@ bindPaneCollapse(
|
|||
() => $logsOpen.set(false),
|
||||
() => $logsOpen.set(true)
|
||||
)
|
||||
registry.register({
|
||||
id: 'logs.toggle',
|
||||
area: PALETTE_AREA,
|
||||
data: {
|
||||
registry.register(
|
||||
paletteToggle({
|
||||
id: 'logs.toggle',
|
||||
label: 'Toggle logs',
|
||||
keywords: ['logs', 'agent log', 'tail', 'debug'],
|
||||
run: () => $logsOpen.set(!$logsOpen.get())
|
||||
} satisfies PaletteContribution
|
||||
})
|
||||
get: () => $logsOpen.get(),
|
||||
set: enabled => $logsOpen.set(enabled)
|
||||
})
|
||||
)
|
||||
|
||||
// YOLO (dangerous-command approval bypass) is a status-bar zap and a /yolo
|
||||
// command; ⌘K is the third door onto the SAME store function, so a user who
|
||||
// lives in the palette never has to hunt for the pill.
|
||||
registry.register(
|
||||
paletteToggle({
|
||||
id: 'session.yolo',
|
||||
label: 'Toggle YOLO',
|
||||
icon: Zap,
|
||||
keywords: ['yolo', 'approvals', 'auto-approve', 'bypass', 'dangerous', 'commands'],
|
||||
get: () => $yoloActive.get(),
|
||||
set: enabled => void setYoloEnabled(enabled).catch(() => undefined)
|
||||
})
|
||||
)
|
||||
|
||||
// Sessions/files Close = collapse their SIDE (⌘B/⌘J truthful, titlebar button
|
||||
// flips back) — but only while the pane actually lives in that root side
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { setYoloActive } from '@/store/session'
|
||||
import { $gateway } from '@/store/gateway'
|
||||
import { $activeSessionId, setYoloActive } from '@/store/session'
|
||||
|
||||
export type GatewayRequester = <T = unknown>(method: string, params?: Record<string, unknown>) => Promise<T>
|
||||
|
||||
|
|
@ -45,3 +46,31 @@ export async function setGlobalYolo(requestGateway: GatewayRequester, enabled: b
|
|||
|
||||
return active
|
||||
}
|
||||
|
||||
/**
|
||||
* Set YOLO to an explicit state from a surface that has no React context — the
|
||||
* ⌘K rows. `useSlashCommand` keeps its own `requestGateway` (it already holds
|
||||
* one, with the reconnect handling), so this reaches the active gateway
|
||||
* directly rather than growing a second requester abstraction.
|
||||
*
|
||||
* With no session yet the flag is armed locally; the session-create path
|
||||
* (use-session-actions) applies it on the first message, exactly as a bare
|
||||
* `/yolo` in a fresh draft does.
|
||||
*/
|
||||
export async function setYoloEnabled(enabled: boolean): Promise<boolean> {
|
||||
const sessionId = $activeSessionId.get()
|
||||
|
||||
if (!sessionId) {
|
||||
setYoloActive(enabled)
|
||||
|
||||
return enabled
|
||||
}
|
||||
|
||||
const gateway = $gateway.get()
|
||||
|
||||
if (!gateway) {
|
||||
throw new Error('Hermes gateway unavailable')
|
||||
}
|
||||
|
||||
return setSessionYolo((method, params) => gateway.request(method, params), sessionId, enabled)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue