mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
feat(desktop): make the ⌘K row note a shared primitive with a state variant
A toggle's on/off isn't a passive fact like a version string — the row is about to change it. It keeps the label's color so the two read as one phrase, and earns its separation from a faint underline instead of going muted. Both variants live in floating-hud.ts beside the other shared HUD chrome. That underline needs room to exist: flex items are blockified, so the note's truncate/overflow-hidden bites, and the app's global 0.25rem offset sits at the bottom edge of a text-xs line box. The state variant drops truncate (one word, nothing to clip) and pulls the offset to 2px. Flipping a setting isn't navigation either, so toggles keep the palette open the way the theme and color-mode rows already do, and selecting a keepOpen row bumps a counter that rebuilds the groups so the note can't report the state it left. Also: yolo is lowercase, and logs gets its own icon instead of borrowing the YOLO bolt.
This commit is contained in:
parent
09fa5c063c
commit
129b4e7fef
4 changed files with 74 additions and 21 deletions
|
|
@ -19,11 +19,15 @@ export interface PaletteContribution {
|
|||
keywords?: string[]
|
||||
run: () => void
|
||||
/**
|
||||
* Muted text after the label — the live state the row acts on. A function
|
||||
* Short note 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
|
||||
/** `state` when running the row CHANGES what `detail` says. */
|
||||
detailVariant?: 'muted' | 'state'
|
||||
/** Leave the palette open after running — for rows you may run repeatedly. */
|
||||
keepOpen?: boolean
|
||||
}
|
||||
|
||||
/** Contributed palette rows, with stable render keys. */
|
||||
|
|
@ -34,12 +38,15 @@ export function usePaletteContributions(): Array<PaletteContribution & { key: st
|
|||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* A binary setting as one palette row: `Toggle status bar` trailed by the live
|
||||
* state. The verb says what the row does, the note says where it stands —
|
||||
* neither alone is enough to act on.
|
||||
*
|
||||
* Rows keep the palette open: flipping a setting is the kind of thing you do
|
||||
* two or three of in a row, and the note updating in place is the receipt.
|
||||
*/
|
||||
export function paletteToggle(
|
||||
spec: Omit<PaletteContribution, 'detail' | 'run'> & {
|
||||
spec: Omit<PaletteContribution, 'detail' | 'detailVariant' | 'keepOpen' | 'run'> & {
|
||||
get: () => boolean
|
||||
set: (enabled: boolean) => void
|
||||
}
|
||||
|
|
@ -49,6 +56,8 @@ export function paletteToggle(
|
|||
const data: PaletteContribution = {
|
||||
...rest,
|
||||
detail: () => (get() ? 'on' : 'off'),
|
||||
detailVariant: 'state',
|
||||
keepOpen: true,
|
||||
keywords: [...keywords, 'on', 'off', 'enable', 'disable'],
|
||||
run: () => set(!get())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { Dialog as DialogPrimitive } from 'radix-ui'
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
import { HUD_HEADING, HUD_ITEM, HUD_POSITION, HUD_SURFACE, HUD_TEXT } from '@/app/floating-hud'
|
||||
import {
|
||||
HUD_HEADING,
|
||||
HUD_ITEM,
|
||||
HUD_NOTE,
|
||||
HUD_NOTE_VARIANT,
|
||||
HUD_POSITION,
|
||||
HUD_SURFACE,
|
||||
HUD_TEXT
|
||||
} from '@/app/floating-hud'
|
||||
import { setTerminalTakeover } from '@/app/right-sidebar/store'
|
||||
import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { HighlightMatches } from '@/components/ui/highlight-matches'
|
||||
|
|
@ -61,7 +69,7 @@ import {
|
|||
import { $bindings } from '@/store/keybinds'
|
||||
import { openPetGenerate } from '@/store/pet-generate'
|
||||
import { requestStartWorkSession } from '@/store/projects'
|
||||
import { $connection, $yoloActive } from '@/store/session'
|
||||
import { $connection } from '@/store/session'
|
||||
import { runGatewayRestart } from '@/store/system-actions'
|
||||
import {
|
||||
$backendUpdateApply,
|
||||
|
|
@ -103,8 +111,10 @@ interface PaletteItem {
|
|||
action?: string
|
||||
/** Renders a trailing check: this row IS the current setting (theme, mode). */
|
||||
active?: boolean
|
||||
/** Muted text beside the label — state the row acts on (a version, a count). */
|
||||
/** Short note beside the label — state the row acts on (a version, a count). */
|
||||
detail?: string
|
||||
/** `state` when the row will change what `detail` says (a toggle's on/off). */
|
||||
detailVariant?: keyof typeof HUD_NOTE_VARIANT
|
||||
icon: IconComponent
|
||||
id: string
|
||||
/** Keep the palette open after running (live-preview pickers like theme/mode). */
|
||||
|
|
@ -260,9 +270,9 @@ const PaletteRow = memo(function PaletteRow({
|
|||
shows exactly which words earned the row its rank. */}
|
||||
<HighlightMatches query={search.split(/\s+/)} text={item.label} />
|
||||
</span>
|
||||
{/* Reads as a suffix of the label, so it sits closer than the row's
|
||||
icon-to-label gap-2 — negative margin trims that back to ~4px. */}
|
||||
{item.detail && <span className="-ml-1 truncate text-muted-foreground/80">{item.detail}</span>}
|
||||
{item.detail && (
|
||||
<span className={cn(HUD_NOTE, HUD_NOTE_VARIANT[item.detailVariant ?? 'muted'])}>{item.detail}</span>
|
||||
)}
|
||||
{combo && <KbdCombo className="ml-auto opacity-55" combo={combo} size="sm" />}
|
||||
{item.to && <ChevronRight className={cn('size-3.5 shrink-0 text-muted-foreground/70', !combo && 'ml-auto')} />}
|
||||
{item.active && <Check className={cn('size-3.5 shrink-0 text-primary', !combo && !item.to && 'ml-auto')} />}
|
||||
|
|
@ -376,9 +386,10 @@ 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)
|
||||
// Running a keepOpen row (a toggle) changes state the rows themselves report,
|
||||
// so the groups have to rebuild without the palette closing. A counter keeps
|
||||
// that generic — the palette never learns which stores its contributions read.
|
||||
const [selectTick, setSelectTick] = useState(0)
|
||||
|
||||
const updateVersionLabel = useMemo(() => {
|
||||
const backend = connection?.mode === 'remote'
|
||||
|
|
@ -536,11 +547,13 @@ 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.
|
||||
// Read on open and after every select (the deps below), so a
|
||||
// row that reports state can't show the state it just left.
|
||||
detail: item.detail?.(),
|
||||
detailVariant: item.detailVariant,
|
||||
icon: item.icon ?? Zap,
|
||||
id: item.key,
|
||||
keepOpen: item.keepOpen,
|
||||
keywords: item.keywords,
|
||||
label: item.label,
|
||||
run: item.run
|
||||
|
|
@ -731,7 +744,11 @@ export function CommandPalette() {
|
|||
]
|
||||
}
|
||||
]
|
||||
}, [contributedItems, go, open, settingsSectionLabel, t, updateVersionLabel, yoloActive])
|
||||
// `open` and `selectTick` are deliberate re-read triggers, not values: rows
|
||||
// report live state through `detail()`, so the groups must rebuild when the
|
||||
// palette opens and after each select — eslint only sees unused deps.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [contributedItems, go, open, selectTick, settingsSectionLabel, t, updateVersionLabel])
|
||||
|
||||
// The long, granular lists (settings fields, API keys, MCP servers, archived
|
||||
// chats) only surface once the user types — otherwise they'd bury the
|
||||
|
|
@ -1021,7 +1038,13 @@ export function CommandPalette() {
|
|||
|
||||
if (!item.keepOpen) {
|
||||
closeCommandPalette()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Staying open means the rows are still on screen — re-read anything they
|
||||
// report (a toggle's on/off) so the note isn't showing the previous state.
|
||||
setSelectTick(tick => tick + 1)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ 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, Zap } from '@/lib/icons'
|
||||
import { FileText, 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'
|
||||
|
|
@ -232,12 +232,12 @@ registry.registerMany([
|
|||
},
|
||||
{
|
||||
// Optional chrome — in NO default layout. Adoption stacks it with the
|
||||
// terminal; $logsOpen (default off, ⌘K "Logs on") reveals it.
|
||||
// terminal; $logsOpen (default off, ⌘K "Toggle logs") 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 "Logs on") instead of leaving it collapsed.
|
||||
// logs pane on (like a ⌘K "Toggle logs") instead of leaving it collapsed.
|
||||
data: { placement: 'bottom', height: '20vh', minHeight: '7.5rem', maxHeight: '80vh', revealOnPreset: true },
|
||||
render: () => idle(<LogsPane />)
|
||||
}
|
||||
|
|
@ -598,6 +598,7 @@ registry.register(
|
|||
paletteToggle({
|
||||
id: 'logs.toggle',
|
||||
label: 'Toggle logs',
|
||||
icon: FileText,
|
||||
keywords: ['logs', 'agent log', 'tail', 'debug'],
|
||||
get: () => $logsOpen.get(),
|
||||
set: enabled => $logsOpen.set(enabled)
|
||||
|
|
@ -610,7 +611,7 @@ registry.register(
|
|||
registry.register(
|
||||
paletteToggle({
|
||||
id: 'session.yolo',
|
||||
label: 'Toggle YOLO',
|
||||
label: 'Toggle yolo',
|
||||
icon: Zap,
|
||||
keywords: ['yolo', 'approvals', 'auto-approve', 'bypass', 'dangerous', 'commands'],
|
||||
get: () => $yoloActive.get(),
|
||||
|
|
|
|||
|
|
@ -31,3 +31,23 @@ export const HUD_ITEM = 'gap-2 px-2 py-1'
|
|||
// heading via the universal-descendant variant.
|
||||
export const HUD_HEADING =
|
||||
'**:[[cmdk-group-heading]]:static **:[[cmdk-group-heading]]:bg-transparent **:[[cmdk-group-heading]]:px-2.5 **:[[cmdk-group-heading]]:pb-1 **:[[cmdk-group-heading]]:pt-2.5 **:[[cmdk-group-heading]]:text-[0.64rem] **:[[cmdk-group-heading]]:font-semibold **:[[cmdk-group-heading]]:uppercase **:[[cmdk-group-heading]]:tracking-[0.16em] **:[[cmdk-group-heading]]:text-(--theme-primary)'
|
||||
|
||||
// A short note trailing a row's label — a version, a count, a live state. Sits
|
||||
// closer than the row's icon-to-label `gap-2` because it reads as a suffix of
|
||||
// the label, not as another item in the row.
|
||||
export const HUD_NOTE = '-ml-1'
|
||||
|
||||
// `state` marks a note the row will CHANGE (a toggle's on/off) rather than a
|
||||
// passive fact. It keeps the label's own color so the pair reads as one phrase,
|
||||
// and earns its separation from a faint underline instead of going muted.
|
||||
//
|
||||
// That underline needs room, so `state` skips `truncate`: flex items are
|
||||
// blockified, which makes its `overflow: hidden` bite, and the app's global
|
||||
// 0.25rem underline offset (styles.css) already sits at the bottom edge of a
|
||||
// `text-xs` line box — together they clip the rule away entirely. Pulling the
|
||||
// offset to 2px keeps it inside the box; dropping truncate costs nothing
|
||||
// because these notes are one word.
|
||||
export const HUD_NOTE_VARIANT = {
|
||||
muted: 'truncate text-muted-foreground/80',
|
||||
state: 'shrink-0 underline decoration-current/50 underline-offset-2'
|
||||
} as const
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue