Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/src/app/command-palette/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -605,7 +605,7 @@ function CommandPaletteBody({ onExited }: { onExited: () => void }) {
// reopen paints from cache and revalidates in the background.
const configQuery = useQuery({
queryKey: ['command-palette', 'config'],
queryFn: getHermesConfigRecord
queryFn: () => getHermesConfigRecord()
})

const sessionsQuery = useQuery({
Expand Down
4 changes: 2 additions & 2 deletions apps/desktop/src/app/contrib/controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { $previewOpenRequest, $previewTabs, closeRightRail } from '@/store/previ
import { $reviewOpen, closeReview, openReview, REVIEW_PANE_ID } from '@/store/review'
import { $currentCwd, $selectedStoredSessionId, $sessions, $yoloActive, sessionMatchesStoredId } from '@/store/session'
import { watchSessionPins } from '@/store/session-pin-sync'
import { $statusbarVisible } from '@/store/statusbar-prefs'
import { $statusbarVisible, toggleStatusbarVisible } from '@/store/statusbar-prefs'

import type { SessionDragPayload } from '../chat/composer/inline-refs'
import { watchRouteTiles } from '../chat/route-tile'
Expand Down Expand Up @@ -302,7 +302,7 @@ registry.registerMany([
icon: PanelBottom,
keywords: ['status bar', 'statusbar', 'bottom bar', 'hide', 'show', 'chrome'],
get: () => $statusbarVisible.get(),
set: enabled => $statusbarVisible.set(enabled)
set: () => toggleStatusbarVisible()
}),
// The keybind panel's non-titlebar door (the keyboard icon is gone).
{
Expand Down
4 changes: 3 additions & 1 deletion apps/desktop/src/app/contrib/surfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Navigate, Route, Routes, useParams } from 'react-router'

import { ContribBoundary } from '@/contrib/react/boundary'
import { useContributions } from '@/contrib/react/use-contributions'
import { $desktopStatusbarMode } from '@/store/desktop-statusbar'
import { $activeGatewayProfile } from '@/store/profile'
import { $freshDraftReady, $gatewayState } from '@/store/session'

Expand Down Expand Up @@ -76,6 +77,7 @@ export const StatusbarSurface = memo(function StatusbarSurface({
chatOpen: boolean
commandCenterOpen: boolean
}) {
const desktopStatusbarMode = useStore($desktopStatusbarMode)
const gatewayState = useStore($gatewayState)
const freshDraftReady = useStore($freshDraftReady)
const { inferenceStatus, statusSnapshot } = useStatusSnapshot(gatewayState, actions.requestGateway)
Expand All @@ -98,7 +100,7 @@ export const StatusbarSurface = memo(function StatusbarSurface({
toggleCommandCenter: actions.toggleCommandCenter
})

return <StatusbarControls items={statusbarItems} leftItems={leftStatusbarItems} />
return <StatusbarControls items={statusbarItems} leftItems={leftStatusbarItems} mode={desktopStatusbarMode} />
})

/** The workspace pane: the real route table (chat + full-page views + plugin
Expand Down
13 changes: 4 additions & 9 deletions apps/desktop/src/app/hooks/use-config-record.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { useQuery } from '@tanstack/react-query'

import { getHermesConfigRecord } from '@/hermes'
import { queryClient, writeCache } from '@/lib/query-client'
import type { HermesConfigRecord } from '@/types/hermes'
import { HERMES_CONFIG_KEY } from '@/store/hermes-config-record'

export { invalidateHermesConfig, setHermesConfigCache } from '@/store/hermes-config-record'

// One shared cache for the whole profile config record (`GET /api/config`).
// Every settings surface (MCP, model, config) reads and writes through this key
Expand All @@ -11,12 +12,6 @@ import type { HermesConfigRecord } from '@/types/hermes'
//
// Distinct from session/hooks/use-hermes-config.ts, which is side-effecting —
// it pushes personality/cwd/voice/… into the session stores for live chat.
export const HERMES_CONFIG_KEY = ['hermes-config-record'] as const

// staleTime 0 → serve cache instantly, background-revalidate on every mount.
export const useHermesConfigRecord = () =>
useQuery({ queryKey: HERMES_CONFIG_KEY, queryFn: getHermesConfigRecord, staleTime: 0 })

export const setHermesConfigCache = writeCache<HermesConfigRecord>(HERMES_CONFIG_KEY)

export const invalidateHermesConfig = () => queryClient.invalidateQueries({ queryKey: HERMES_CONFIG_KEY })
useQuery({ queryKey: HERMES_CONFIG_KEY, queryFn: () => getHermesConfigRecord(), staleTime: 0 })
46 changes: 43 additions & 3 deletions apps/desktop/src/app/session/hooks/use-hermes-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
import { $terminalFontFamily, setTerminalFontFamilyFromConfig } from '@/app/right-sidebar/terminal/terminal-font'
import { getHermesConfig } from '@/hermes'
import { persistString } from '@/lib/storage'
import { $desktopStatusbarMode, applyDesktopStatusbarFromConfig } from '@/store/desktop-statusbar'
import {
$currentCwd,
$currentFastMode,
Expand All @@ -21,10 +22,14 @@ import {
import { useHermesConfig } from './use-hermes-config'

vi.mock('@/hermes', () => ({
getApiRequestProfile: vi.fn(() => null),
getHermesConfig: vi.fn(),
getHermesConfigDefaults: vi.fn().mockResolvedValue({})
getHermesConfigDefaults: vi.fn().mockResolvedValue({}),
getHermesConfigRecord: vi.fn().mockResolvedValue({}),
saveHermesConfig: vi.fn().mockResolvedValue({ ok: true })
}))

const LEGACY_STATUSBAR_VISIBLE_STORAGE_KEY = 'hermes.desktop.statusbarVisible'
const WORKSPACE_CWD_KEY = 'hermes.desktop.workspace-cwd'

function deferred<T>() {
Expand All @@ -49,6 +54,8 @@ describe('useHermesConfig refreshHermesConfig', () => {
setCurrentReasoningEffort('')
setDefaultReasoningEffort('')
setTerminalFontFamilyFromConfig('')
persistString(LEGACY_STATUSBAR_VISIBLE_STORAGE_KEY, null)
applyDesktopStatusbarFromConfig({ display: { desktop_statusbar: 'off' } })
persistString(WORKSPACE_CWD_KEY, null)
})

Expand Down Expand Up @@ -142,17 +149,50 @@ describe('useHermesConfig refreshHermesConfig', () => {
refreshC = result.current.refreshHermesConfig(true)
})

profileC.resolve({ agent: { reasoning_effort: 'low', service_tier: 'normal' } })
profileC.resolve({
agent: { reasoning_effort: 'low', service_tier: 'normal' },
display: { desktop_statusbar: 'auto-hide' }
})
await act(async () => {
await refreshC
})
profileB.resolve({ agent: { reasoning_effort: 'high', service_tier: 'priority' } })
profileB.resolve({
agent: { reasoning_effort: 'high', service_tier: 'priority' },
display: { desktop_statusbar: 'on' }
})
await act(async () => {
await refreshB
})

expect($currentReasoningEffort.get()).toBe('low')
expect($currentFastMode.get()).toBe(false)
expect($desktopStatusbarMode.get()).toBe('auto-hide')
})

it('does not let an older ordinary refresh overwrite a newer status bar value', async () => {
const older = deferred<Awaited<ReturnType<typeof getHermesConfig>>>()
const newer = deferred<Awaited<ReturnType<typeof getHermesConfig>>>()
vi.mocked(getHermesConfig).mockReturnValueOnce(older.promise).mockReturnValueOnce(newer.promise)

const { result } = renderHook(() => useHermesConfig({ activeSessionIdRef: { current: null } }))

let olderRefresh!: Promise<void>
let newerRefresh!: Promise<void>
act(() => {
olderRefresh = result.current.refreshHermesConfig()
newerRefresh = result.current.refreshHermesConfig()
})

newer.resolve({ display: { desktop_statusbar: 'auto-hide' } })
await act(async () => {
await newerRefresh
})
older.resolve({ display: { desktop_statusbar: 'on' } })
await act(async () => {
await olderRefresh
})

expect($desktopStatusbarMode.get()).toBe('auto-hide')
})

it('loads the profile terminal font for already-mounted terminal surfaces', async () => {
Expand Down
9 changes: 4 additions & 5 deletions apps/desktop/src/app/session/hooks/use-hermes-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { setTerminalFontFamilyFromConfig } from '@/app/right-sidebar/terminal/te
import { getHermesConfig, getHermesConfigDefaults } from '@/hermes'
import { BUILTIN_PERSONALITIES, normalizePersonalityValue, personalityNamesFromConfig } from '@/lib/chat-runtime'
import { normalize } from '@/lib/text'
import { applyDesktopStatusbarFromConfig, migrateLegacyDesktopStatusbarPreference } from '@/store/desktop-statusbar'
import {
getComposerSelectionGeneration,
getCurrentModelSource,
Expand Down Expand Up @@ -56,11 +57,7 @@ export function useHermesConfig({ activeSessionIdRef }: HermesConfigOptions) {

const refreshHermesConfig = useCallback(
async (force = false) => {
if (force) {
profileRefreshEpochRef.current += 1
}

const profileRefreshEpoch = profileRefreshEpochRef.current
const profileRefreshEpoch = ++profileRefreshEpochRef.current
const selectionGeneration = getComposerSelectionGeneration()

try {
Expand Down Expand Up @@ -110,6 +107,8 @@ export function useHermesConfig({ activeSessionIdRef }: HermesConfigOptions) {
setVoiceMaxRecordingSeconds(recordingLimit(config.voice?.max_recording_seconds))
setSttEnabled(config.stt?.enabled !== false)
setTerminalFontFamilyFromConfig(config.terminal?.font_family)
applyDesktopStatusbarFromConfig(config)
void migrateLegacyDesktopStatusbarPreference(config).catch(() => undefined)
applyAutoSpeakFromConfig(config)
applyVoiceStopPhraseFromConfig(config)
applyThinkingSoundFromConfig(config)
Expand Down
37 changes: 36 additions & 1 deletion apps/desktop/src/app/settings/appearance-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@ import { selectableCardClass } from '@/lib/selectable-card'
import { normalize } from '@/lib/text'
import { cn } from '@/lib/utils'
import { $backdrop, setBackdrop } from '@/store/backdrop'
import {
$desktopStatusbarMode,
type DesktopStatusbarMode,
persistDesktopStatusbarMode
} from '@/store/desktop-statusbar'
import { $embedAllowed, $embedMode, clearEmbedAllowed, type EmbedMode, setEmbedMode } from '@/store/embed-consent'
import { notifyError } from '@/store/notifications'
import { $activeGatewayProfile, $profiles, normalizeProfileKey } from '@/store/profile'
import { $reactionsEnabled, setReactionsEnabled } from '@/store/reactions-enabled'
import { $toolViewMode, setToolViewMode } from '@/store/tool-view'
Expand All @@ -24,6 +30,8 @@ import { installVscodeThemeFromMarketplace } from '@/themes/install'
import type { DesktopTheme } from '@/themes/types'
import { $marketplaceInstalls, isUserTheme, removeUserTheme } from '@/themes/user-themes'

import { setHermesConfigCache } from '../hooks/use-config-record'

import { MODE_OPTIONS } from './constants'
import { PetSettings } from './pet-settings'
import { ListRow, SectionHeading, SettingsContent } from './primitives'
Expand Down Expand Up @@ -244,7 +252,7 @@ function MarketplaceThemeResults({
)
}

export function AppearanceSettings() {
export function AppearanceSettings({ onConfigSaved }: { onConfigSaved?: () => void }) {
const { t, isSavingLocale } = useI18n()
const { themeName, mode, resolvedMode, availableThemes, setTheme, setMode } = useTheme()
const toolViewMode = useStore($toolViewMode)
Expand All @@ -254,6 +262,7 @@ export function AppearanceSettings() {
const translucency = useStore($translucency)
const reactionsEnabled = useStore($reactionsEnabled)
const backdrop = useStore($backdrop)
const desktopStatusbarMode = useStore($desktopStatusbarMode)
const installs = useStore($marketplaceInstalls)
const profiles = useStore($profiles)
const activeProfileKey = normalizeProfileKey(useStore($activeGatewayProfile))
Expand Down Expand Up @@ -299,6 +308,12 @@ export function AppearanceSettings() {

const uiScaleOptions = UI_SCALE_PRESETS.map(preset => ({ id: preset, label: `${preset}%` }))

const statusbarOptions = [
{ id: 'on', label: a.statusbarOn },
{ id: 'auto-hide', label: a.statusbarAutoHide },
{ id: 'off', label: a.statusbarOff }
] as const satisfies readonly { id: DesktopStatusbarMode; label: string }[]

const matchedScalePreset = matchUiScalePreset(zoomPercent)

return (
Expand Down Expand Up @@ -433,6 +448,26 @@ export function AppearanceSettings() {

<TerminalFontSetting />

<ListRow
action={
<SegmentedControl
onChange={mode => {
triggerHaptic('selection')
void persistDesktopStatusbarMode(mode)
.then(config => {
setHermesConfigCache(config)
onConfigSaved?.()
})
.catch(error => notifyError(error, a.statusbarSaveFailed))
}}
options={statusbarOptions}
value={desktopStatusbarMode}
/>
}
description={a.statusbarDesc}
title={a.statusbarTitle}
/>

<ListRow
action={
<div className="flex items-center gap-3">
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/app/settings/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ export const ENUM_OPTIONS: Record<string, string[]> = {
'context.engine': ['compressor', 'default', 'custom'],
// '' = inherit the agent's own effort; the rest is the shared scale.
'delegation.reasoning_effort': ['', ...REASONING_EFFORTS],
'display.desktop_statusbar': ['on', 'off', 'auto-hide'],
// NOTE: memory.provider is intentionally NOT listed here. Its options are
// discovery-driven and served by the backend config schema (merged
// per-request in web_server._schema_with_dynamic_provider_options), so
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/app/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ export function SettingsView({ onClose, onConfigSaved, onMainModelChanged }: Set

<OverlayMain className="px-0 pb-0">
{activeView === 'config:appearance' ? (
<AppearanceSettings />
<AppearanceSettings onConfigSaved={onConfigSaved} />
) : activeView === 'about' ? (
<AboutSettings />
) : activeView === 'gateway' ? (
Expand Down
Loading