diff --git a/apps/desktop/src/app/chat/composer/controls.test.tsx b/apps/desktop/src/app/chat/composer/controls.test.tsx index bbee021649..d5694b6144 100644 --- a/apps/desktop/src/app/chat/composer/controls.test.tsx +++ b/apps/desktop/src/app/chat/composer/controls.test.tsx @@ -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. diff --git a/apps/desktop/src/app/chat/composer/controls.tsx b/apps/desktop/src/app/chat/composer/controls.tsx index c48e3eb21b..388df3cdc0 100644 --- a/apps/desktop/src/app/chat/composer/controls.tsx +++ b/apps/desktop/src/app/chat/composer/controls.tsx @@ -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 ? : null} + {hudMode ? : null} ) } -function ExitHudButton() { +function HudWindowButtons() { const { t } = useI18n() return ( - - - + <> + + + + + + + ) } diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts index 2bc76807ab..0f18ac2a7b 100644 --- a/apps/desktop/src/i18n/ar.ts +++ b/apps/desktop/src/i18n/ar.ts @@ -218,6 +218,7 @@ export const ar = defineLocale({ openStarmap: 'فتح خريطة الذاكرة', enterHud: 'وضع HUD', exitHud: 'إنهاء وضع HUD', + resetHudLayout: 'إعادة تعيين حجم HUD وموضعه', layoutEditor: 'محرر التخطيط', layoutEditorTitle: modifier => `محرر التخطيط — انقر مع ${modifier} لإعادة ضبط التخطيط` }, diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 65c9078a88..9b55af9d6e 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -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` }, diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index 4c339562a8..b4d1047d00 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -249,7 +249,8 @@ export const ja = defineLocale({ muteHaptics: '触覚フィードバックをオフ', unmuteHaptics: '触覚フィードバックをオン', openSettings: '設定を開く', - openStarmap: 'メモリグラフを開く' + openStarmap: 'メモリグラフを開く', + resetHudLayout: 'HUD のサイズと位置をリセット' }, language: { diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 4479acf298..534732c320 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -287,6 +287,7 @@ export interface Translations { openStarmap: string enterHud: string exitHud: string + resetHudLayout: string layoutEditor: string layoutEditorTitle: (modifier: string) => string } diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index e1988b62b4..c3ab703aea 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -241,7 +241,8 @@ export const zhHant = defineLocale({ muteHaptics: '靜音觸感回饋', unmuteHaptics: '開啟觸感回饋', openSettings: '開啟設定', - openStarmap: '開啟記憶圖譜' + openStarmap: '開啟記憶圖譜', + resetHudLayout: '重設 HUD 大小和位置' }, language: { diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index 3041c5d8c5..316a80d145 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -244,6 +244,7 @@ export const zh: Translations = { openStarmap: '打开记忆图谱', enterHud: 'HUD 模式', exitHud: '退出 HUD 模式', + resetHudLayout: '重置 HUD 大小和位置', layoutEditor: '布局编辑器', layoutEditorTitle: mod => `布局编辑器 — ${mod} 点击重置布局` }, diff --git a/apps/desktop/src/store/hud.test.ts b/apps/desktop/src/store/hud.test.ts index 8cd85c0157..a2ed7a6663 100644 --- a/apps/desktop/src/store/hud.test.ts +++ b/apps/desktop/src/store/hud.test.ts @@ -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 { 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' })]) diff --git a/apps/desktop/src/store/hud.ts b/apps/desktop/src/store/hud.ts index 2f04dafdb7..9d52b86067 100644 --- a/apps/desktop/src/store/hud.ts +++ b/apps/desktop/src/store/hud.ts @@ -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. */