diff --git a/apps/desktop/src/app/hud/resize-frame.test.tsx b/apps/desktop/src/app/hud/resize-frame.test.tsx
new file mode 100644
index 0000000000..735a5db73e
--- /dev/null
+++ b/apps/desktop/src/app/hud/resize-frame.test.tsx
@@ -0,0 +1,91 @@
+// @vitest-environment jsdom
+import { act, cleanup, render } from '@testing-library/react'
+import { MemoryRouter } from 'react-router'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+
+vi.mock('../contrib/wiring', () => ({ WiredPane: () => null }))
+
+class ResizeObserverStub {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+Object.assign(globalThis, { ResizeObserver: ResizeObserverStub })
+
+import { HudShell } from './hud-shell'
+
+/** #108793: the resize frame's hit-testability rides the shell's engagement
+ * gates (styles.css keys `pointer-events` off them). The contract under test:
+ * an idle HUD carries NONE of the gates, so the invisible frame falls out of
+ * the hit test entirely; each arm — caret in the composer, a held band, a
+ * solid-input host — re-arms it on its own. */
+describe('HudShell resize-frame engagement gates', () => {
+ afterEach(() => {
+ cleanup()
+ vi.restoreAllMocks()
+ delete (window as { hermesDesktop?: unknown }).hermesDesktop
+ })
+
+ const shellOf = (container: HTMLElement) => {
+ const shell = container.querySelector('[data-hud-shell]')
+
+ expect(shell).not.toBeNull()
+
+ return shell as HTMLElement
+ }
+
+ it('an idle HUD carries none of the engagement gates the frame keys on', () => {
+ const { container } = render(
+
+
+
+ )
+ const shell = shellOf(container)
+
+ expect(shell.hasAttribute('data-hud-typing')).toBe(false)
+ expect(shell.hasAttribute('data-hud-held')).toBe(false)
+ expect(shell.getAttribute('data-hud-input')).toBe('click-through')
+ // The frame itself is mounted (the resize grammar is not conditional) but
+ // no handle is mid-drag.
+ const handles = shell.querySelectorAll('[data-hud-resize]')
+
+ expect(handles.length).toBeGreaterThan(0)
+ handles.forEach(handle => {
+ expect(handle.hasAttribute('data-hud-grabbing')).toBe(false)
+ })
+ })
+
+ it('focus in the composer arms the typing gate the frame re-arms on', () => {
+ const { container } = render(
+
+
+
+ )
+ const shell = shellOf(container)
+
+ // useHudGlass stamps data-hud-typing live from the :focus query — the
+ // same gate the frost and the scrim run on.
+ const composer = document.createElement('input')
+
+ composer.setAttribute('data-slot', 'composer-rich-input')
+ act(() => {
+ shell.appendChild(composer)
+ composer.focus()
+ composer.dispatchEvent(new FocusEvent('focusin', { bubbles: true }))
+ })
+
+ expect(shell.hasAttribute('data-hud-typing')).toBe(true)
+ })
+
+ it('a solid-input host keeps the frame live unconditionally', () => {
+ Object.assign(window, { hermesDesktop: { hud: { windowing: { solid: true } } } })
+
+ const { container } = render(
+
+
+
+ )
+
+ expect(shellOf(container).getAttribute('data-hud-input')).toBe('solid')
+ })
+})
diff --git a/apps/desktop/src/styles.css b/apps/desktop/src/styles.css
index b492e4b93a..2cef8bd884 100644
--- a/apps/desktop/src/styles.css
+++ b/apps/desktop/src/styles.css
@@ -3578,14 +3578,33 @@ button[data-slot='aui_msg-reactions'] svg {
CanvasTTY grammar: four edge strips + four larger corner targets, each with
its native resize cursor and each preserving the opposite edge. Deliberately
- invisible chrome: no glyph or border over whatever app is below the HUD. */
+ invisible chrome: no glyph or border over whatever app is below the HUD.
+
+ Invisible chrome that is ALWAYS hit-testable is a dead zone: an idle HUD is
+ just the bar, the window hands every other point to the app underneath, and
+ the only thing left answering the cursor was this frame — a thin invisible
+ ring that swallowed clicks and drags aimed at the app behind it (#108793).
+ So the frame rides the band's engagement gates: while the HUD is being used
+ (caret in the composer — data-hud-typing, stamped live by useHudGlass — or a
+ held band waiting on an answer) its edges may be grabbed; an in-flight
+ resize keeps its own handle live via data-hud-grabbing, because pointer
+ capture routes the moves to the handle that started the drag. Solid-input
+ hosts (X11) never hand the window away in the first place, so their frame
+ stays live unconditionally. */
[data-hud-shell] [data-hud-resize] {
position: absolute;
z-index: 20;
- pointer-events: auto;
+ pointer-events: none;
touch-action: none;
}
+[data-hud-shell][data-hud-typing] [data-hud-resize],
+[data-hud-shell][data-hud-held] [data-hud-resize],
+[data-hud-shell][data-hud-input='solid'] [data-hud-resize],
+[data-hud-shell] [data-hud-resize][data-hud-grabbing] {
+ pointer-events: auto;
+}
+
[data-hud-shell] [data-hud-resize='n'] {
top: 0;
right: 1.25rem;