fix(desktop): add a HUD layout reset control

A persisted tall/narrow size has no way out on Linux. Put a reset next
to Exit HUD so the default size (and position, where the compositor
allows it) is one click away.

Co-authored-by: Shawn Wang <32839114+enwaiax@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson
2026-08-23 23:34:57 -05:00
committed by brooklyn!
parent b595fcd5e1
commit d467da9100
10 changed files with 56 additions and 19 deletions
@@ -72,6 +72,7 @@ describe('HUD mode', () => {
expect(screen.getByLabelText('Voice dictation')).toBeTruthy()
expect(screen.getByLabelText('Read replies aloud')).toBeTruthy()
expect(screen.queryByLabelText('Exit HUD mode')).toBeNull()
expect(screen.queryByLabelText('Reset HUD size and position')).toBeNull()
expect(screen.queryByLabelText('Voice')).toBeNull()
})
@@ -80,6 +81,7 @@ describe('HUD mode', () => {
renderControls()
expect(screen.getByLabelText('Voice')).toBeTruthy()
expect(screen.getByLabelText('Reset HUD size and position')).toBeTruthy()
expect(screen.getByLabelText('Exit HUD mode')).toBeTruthy()
// Folded away, not duplicated — the whole point is the row's width back.
+29 -15
View File
@@ -7,7 +7,7 @@ import { useI18n } from '@/i18n'
import { triggerHaptic } from '@/lib/haptics'
import { AudioLines, Ear, EarOff, iconSize, Layers3, Loader2, Square, Volume2, VolumeX } from '@/lib/icons'
import { cn } from '@/lib/utils'
import { $hudMode, closeHud } from '@/store/hud'
import { $hudMode, closeHud, resetHudLayout } from '@/store/hud'
import { $wakeWord, toggleWakeWord } from '@/store/wake-word'
import { ACTIVE_ICON_BTN, GHOST_ICON_BTN, PRIMARY_ICON_BTN } from './control-classes'
@@ -173,27 +173,41 @@ export function ComposerControls({
the surface, paid for in every state, for a control that is invisible
until hovered. Here it costs no reserved space and sits with the other
things you can press. */}
{hudMode ? <ExitHudButton /> : null}
{hudMode ? <HudWindowButtons /> : null}
</div>
)
}
function ExitHudButton() {
function HudWindowButtons() {
const { t } = useI18n()
return (
<Tip label={t.titlebar.exitHud}>
<Button
aria-label={t.titlebar.exitHud}
className={cn(GHOST_ICON_BTN, 'p-0')}
onClick={closeHud}
size="icon"
type="button"
variant="ghost"
>
<Codicon name="screen-normal" size="0.875rem" />
</Button>
</Tip>
<>
<Tip label={t.titlebar.resetHudLayout}>
<Button
aria-label={t.titlebar.resetHudLayout}
className={cn(GHOST_ICON_BTN, 'p-0')}
onClick={resetHudLayout}
size="icon"
type="button"
variant="ghost"
>
<Codicon name="discard" size="0.875rem" />
</Button>
</Tip>
<Tip label={t.titlebar.exitHud}>
<Button
aria-label={t.titlebar.exitHud}
className={cn(GHOST_ICON_BTN, 'p-0')}
onClick={closeHud}
size="icon"
type="button"
variant="ghost"
>
<Codicon name="screen-normal" size="0.875rem" />
</Button>
</Tip>
</>
)
}
+1
View File
@@ -218,6 +218,7 @@ export const ar = defineLocale({
openStarmap: 'فتح خريطة الذاكرة',
enterHud: 'وضع HUD',
exitHud: 'إنهاء وضع HUD',
resetHudLayout: 'إعادة تعيين حجم HUD وموضعه',
layoutEditor: 'محرر التخطيط',
layoutEditorTitle: modifier => `محرر التخطيط — انقر مع ${modifier} لإعادة ضبط التخطيط`
},
+1
View File
@@ -251,6 +251,7 @@ export const en: Translations = {
openStarmap: 'Open memory graph',
enterHud: 'HUD mode',
exitHud: 'Exit HUD mode',
resetHudLayout: 'Reset HUD size and position',
layoutEditor: 'Layout editor',
layoutEditorTitle: mod => `Layout editor — ${mod}-click resets the layout`
},
+2 -1
View File
@@ -249,7 +249,8 @@ export const ja = defineLocale({
muteHaptics: '触覚フィードバックをオフ',
unmuteHaptics: '触覚フィードバックをオン',
openSettings: '設定を開く',
openStarmap: 'メモリグラフを開く'
openStarmap: 'メモリグラフを開く',
resetHudLayout: 'HUD のサイズと位置をリセット'
},
language: {
+1
View File
@@ -287,6 +287,7 @@ export interface Translations {
openStarmap: string
enterHud: string
exitHud: string
resetHudLayout: string
layoutEditor: string
layoutEditorTitle: (modifier: string) => string
}
+2 -1
View File
@@ -241,7 +241,8 @@ export const zhHant = defineLocale({
muteHaptics: '靜音觸感回饋',
unmuteHaptics: '開啟觸感回饋',
openSettings: '開啟設定',
openStarmap: '開啟記憶圖譜'
openStarmap: '開啟記憶圖譜',
resetHudLayout: '重設 HUD 大小和位置'
},
language: {
+1
View File
@@ -244,6 +244,7 @@ export const zh: Translations = {
openStarmap: '打开记忆图谱',
enterHud: 'HUD 模式',
exitHud: '退出 HUD 模式',
resetHudLayout: '重置 HUD 大小和位置',
layoutEditor: '布局编辑器',
layoutEditorTitle: mod => `布局编辑器 — ${mod} 点击重置布局`
},
+12 -2
View File
@@ -4,16 +4,17 @@ import { $activeGatewayProfile } from '@/store/profile'
import { $sessions } from '@/store/session'
import type { SessionInfo } from '@/types/hermes'
import { $hudActive, $hudSession, openHud } from './hud'
import { $hudActive, $hudSession, openHud, resetHudLayout } from './hud'
const desktopWindow = window as unknown as { hermesDesktop?: Window['hermesDesktop'] }
const initialHermesDesktop = desktopWindow.hermesDesktop
const open = vi.fn().mockResolvedValue({ ok: true })
const resetLayout = vi.fn().mockResolvedValue({ ok: true })
function installBridge() {
desktopWindow.hermesDesktop = {
hud: { open }
hud: { open, resetLayout }
} as unknown as Window['hermesDesktop']
}
@@ -23,6 +24,7 @@ function session(overrides: Partial<SessionInfo>): SessionInfo {
beforeEach(() => {
open.mockClear()
resetLayout.mockClear()
installBridge()
$hudActive.set(false)
$hudSession.set(null)
@@ -38,6 +40,14 @@ afterEach(() => {
}
})
describe('resetHudLayout', () => {
it('uses the native HUD recovery capability', () => {
resetHudLayout()
expect(resetLayout).toHaveBeenCalledOnce()
})
})
describe('openHud profile targeting (#82285)', () => {
it('carries the session-stamped profile when the target belongs to another profile', () => {
$sessions.set([session({ id: 'abc', profile: 'work' })])
+5
View File
@@ -87,6 +87,11 @@ export function closeHud(): void {
export const toggleHud = (sessionId?: null | string) => ($hudActive.get() ? closeHud() : openHud(sessionId))
/** Restore the HUD's persisted geometry to its display-aware default. */
export function resetHudLayout(): void {
void window.hermesDesktop?.hud?.resetLayout?.()
}
/** Tell main which session this HUD is on. Main holds it (the HUD's renderer
* doesn't outlive the window) and hands it back in the close broadcast so the
* app window knows what to re-home onto. */