Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
// IPC surface for HUD mode (the chrome-free floating chat band). Extracted
|
||||
// from main.ts; the HUD window handle and session-id latch stay injected
|
||||
// because main.ts owns the window lifecycle and the close broadcast reads the
|
||||
// latch when handing the session back to the app window.
|
||||
import { type BrowserWindow, ipcMain, screen } from 'electron'
|
||||
|
||||
import { createHudDragSession } from './hud-drag'
|
||||
import { normalizeHudResizeBounds } from './hud-geometry'
|
||||
import { hudWindowingView, resolveHudWindowing } from './hud-windowing'
|
||||
import { hudFrostFor, type TranslucencyState } from './translucency'
|
||||
|
||||
function hudWindowing() {
|
||||
return resolveHudWindowing(process.platform, process.env, process.argv)
|
||||
}
|
||||
|
||||
export interface HudIpcDeps {
|
||||
isMac: boolean
|
||||
/** Main's authoritative translucency state (Settings → Appearance). */
|
||||
getTranslucencyState: () => TranslucencyState
|
||||
getHudWindow: () => BrowserWindow | null
|
||||
openHudWindow: (sessionId: null | string, profile: null | string) => void
|
||||
closeHudWindow: () => void
|
||||
resetHudLayout: () => boolean
|
||||
setHudSessionId: (sessionId: null | string) => void
|
||||
}
|
||||
|
||||
export function registerHudIpc({
|
||||
isMac,
|
||||
getTranslucencyState,
|
||||
getHudWindow,
|
||||
openHudWindow,
|
||||
closeHudWindow,
|
||||
resetHudLayout,
|
||||
setHudSessionId
|
||||
}: HudIpcDeps) {
|
||||
const hudDrag = createHudDragSession()
|
||||
|
||||
// The renderer needs this before first paint so X11 never installs the
|
||||
// Chromium drag region that steals modifier-drag gestures from the WM.
|
||||
// Main answers because it owns the actual Ozone backend selection.
|
||||
ipcMain.on('hermes:hud:native-drag', event => {
|
||||
event.returnValue = hudWindowing().move === 'native-drag'
|
||||
})
|
||||
|
||||
ipcMain.on('hermes:hud:windowing', event => {
|
||||
event.returnValue = hudWindowingView(hudWindowing())
|
||||
})
|
||||
|
||||
// X11/KWin window transfer: a renderer-driven grab is temporarily sticky so
|
||||
// the user can keep Ctrl+primary-button held while invoking KDE's desktop
|
||||
// switch shortcut. Clearing sticky on release makes Chromium assign the
|
||||
// window to `_NET_CURRENT_DESKTOP`, exactly like releasing a native titlebar
|
||||
// drag on the destination desktop. Native Wayland owns its move loop and
|
||||
// Windows/macOS stay out of this Linux-specific bridge.
|
||||
ipcMain.on('hermes:hud:workspace-transfer', (event, transferring) => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (
|
||||
!hudWindow ||
|
||||
hudWindow.isDestroyed() ||
|
||||
event.sender !== hudWindow.webContents ||
|
||||
!hudWindowing().workspaceTransfer
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
hudWindow.setVisibleOnAllWorkspaces(Boolean(transferring))
|
||||
} catch {
|
||||
// Workspace APIs are window-manager capabilities — best effort.
|
||||
}
|
||||
})
|
||||
|
||||
// Whether the band currently covers the window below the bar. The renderer
|
||||
// is the only party that can know this (it measures the transcript), and it
|
||||
// is half of the frost decision — the other half is the user's setting,
|
||||
// which main owns. Latched so a Settings change can re-decide without
|
||||
// waiting for the HUD to report again.
|
||||
let bandShowing = false
|
||||
let applied: null | string = null
|
||||
let appliedTo: BrowserWindow | null = null
|
||||
|
||||
// Real frosted glass behind the band — the thing CSS backdrop-filter cannot do,
|
||||
// because Chromium composites a transparent window's page against nothing and
|
||||
// the desktop is not in its backdrop root. The material IS the window's content
|
||||
// view, so it frosts the whole rectangle; the HUD's layout leaves no dead
|
||||
// margins for that reason, and it only turns on while the band is showing
|
||||
// (idle HUD mode must be the bar and nothing else).
|
||||
//
|
||||
// macOS ONLY. Windows' equivalent (setBackgroundMaterial → the DWM backdrop)
|
||||
// is mutually exclusive with window transparency, so it is not called at all
|
||||
// here — see the note at the bottom of this function.
|
||||
//
|
||||
// Diffed before issuing: `setVibrancy` carries a 150ms animation that restarts
|
||||
// if re-issued, so a repeated call would keep the material from ever settling
|
||||
// (the same churn the chat windows' native-diff contract exists to prevent).
|
||||
//
|
||||
// The diff is keyed to the WINDOW as well as the value. A HUD respawn (the
|
||||
// profile switch in openHudWindow destroys and rebuilds it) hands back a
|
||||
// fresh window carrying no material, and a latch that only remembered the
|
||||
// value would recognise its own last answer and skip — leaving the new HUD
|
||||
// unfrosted until something else happened to change the signature.
|
||||
const applyHudFrost = () => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (!hudWindow || hudWindow.isDestroyed()) {
|
||||
applied = null
|
||||
appliedTo = null
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
const frost = hudFrostFor(getTranslucencyState(), bandShowing)
|
||||
const signature = `${frost.vibrancy ?? 'off'}:${frost.backgroundMaterial}`
|
||||
|
||||
if (applied === signature && appliedTo === hudWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
applied = signature
|
||||
appliedTo = hudWindow
|
||||
|
||||
if (isMac && typeof hudWindow.setVibrancy === 'function') {
|
||||
hudWindow.setVibrancy(frost.vibrancy)
|
||||
}
|
||||
|
||||
// Windows: never touch setBackgroundMaterial on the HUD. Live-verified on
|
||||
// Win11 (Electron 40.10.2, RTX 4090): ANY setBackgroundMaterial call on a
|
||||
// transparent window — including 'none', which is what the idle HUD asks
|
||||
// for — permanently kills per-pixel alpha, and every transparent pixel
|
||||
// composites as opaque white. Neither 'auto' nor a follow-up
|
||||
// setBackgroundColor('#00000000') restores it. The DWM backdrop and window
|
||||
// transparency are mutually exclusive, so the Windows HUD keeps the CSS
|
||||
// tint the sheet already paints and skips the native frost entirely.
|
||||
}
|
||||
|
||||
ipcMain.handle('hermes:hud:open', async (_event, request) => {
|
||||
openHudWindow(
|
||||
typeof request?.sessionId === 'string' ? request.sessionId : null,
|
||||
typeof request?.profile === 'string' ? request.profile : null
|
||||
)
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('hermes:hud:frost', (_event, showing) => {
|
||||
bandShowing = Boolean(showing)
|
||||
applyHudFrost()
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
// Let clicks fall through the HUD wherever it isn't really there. An
|
||||
// always-on-top window eats every click inside its rectangle, and most of that
|
||||
// rectangle is a faded-out band over whatever the user is actually working in.
|
||||
// `forward` keeps mousemove flowing so the renderer can re-arm when the cursor
|
||||
// reaches the bar.
|
||||
ipcMain.on('hermes:hud:ignore-mouse', (_event, ignore) => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (!hudWindow || hudWindow.isDestroyed()) {
|
||||
return
|
||||
}
|
||||
|
||||
// On X11 ignore-mouse is a one-way door: setIgnoreMouseEvents(false)
|
||||
// cannot restore the input region afterwards. Veto the request there so
|
||||
// the HUD stays a normal solid window. Native Wayland and macOS/Windows
|
||||
// keep the per-element path.
|
||||
if (Boolean(ignore) && !hudWindowing().ignoreMouse) {
|
||||
return
|
||||
}
|
||||
|
||||
hudWindow.setIgnoreMouseEvents(Boolean(ignore), { forward: true })
|
||||
})
|
||||
|
||||
ipcMain.on('hermes:hud:begin-move', event => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (
|
||||
!hudWindow ||
|
||||
hudWindow.isDestroyed() ||
|
||||
event.sender !== hudWindow.webContents ||
|
||||
!hudWindowing().clientPlacement
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [x, y] = hudWindow.getPosition()
|
||||
hudDrag.begin(screen.getCursorScreenPoint(), { x, y })
|
||||
})
|
||||
|
||||
ipcMain.on('hermes:hud:end-move', event => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (hudWindow && !hudWindow.isDestroyed() && event.sender !== hudWindow.webContents) {
|
||||
return
|
||||
}
|
||||
|
||||
hudDrag.end()
|
||||
})
|
||||
|
||||
ipcMain.on('hermes:hud:move-by', (event, delta) => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (!hudWindow || hudWindow.isDestroyed() || event.sender !== hudWindow.webContents) {
|
||||
return
|
||||
}
|
||||
|
||||
const width = Number(delta?.width)
|
||||
const height = Number(delta?.height)
|
||||
|
||||
if (!Number.isFinite(width) || !Number.isFinite(height) || !hudWindowing().clientPlacement) {
|
||||
return
|
||||
}
|
||||
|
||||
const origin = hudDrag.origin(screen.getCursorScreenPoint())
|
||||
|
||||
if (!origin) {
|
||||
return
|
||||
}
|
||||
|
||||
// Cursor − grab offset in Electron DIP (see hud-drag.ts). setBounds —
|
||||
// NOT setPosition: on Windows, a transparent frameless window silently
|
||||
// grows ~1px per setPosition call (worse at >100% DPI). The renderer
|
||||
// snapshots outerWidth/outerHeight when the composer drag arms and
|
||||
// re-pins to that size on every move (same pattern as the pet overlay).
|
||||
hudWindow.setBounds({
|
||||
x: origin.x,
|
||||
y: origin.y,
|
||||
width: Math.round(width),
|
||||
height: Math.round(height)
|
||||
})
|
||||
})
|
||||
|
||||
// Resize from the HUD's edge/corner handles. The window is created non-resizable
|
||||
// (see spawnHudWindow — a transparent frameless window must not expose a
|
||||
// system resize hot-zone, or dragging grows it), which on Windows/Linux also
|
||||
// blocks programmatic setBounds sizing — so briefly flip resizable on while
|
||||
// the size actually changes, exactly like the pet overlay's wheel-scale does.
|
||||
ipcMain.on('hermes:hud:set-bounds', (event, bounds) => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (!hudWindow || hudWindow.isDestroyed() || event.sender !== hudWindow.webContents || !bounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextBounds = normalizeHudResizeBounds(bounds)
|
||||
|
||||
if (!nextBounds) {
|
||||
return
|
||||
}
|
||||
|
||||
const win = hudWindow
|
||||
const { width, height } = nextBounds
|
||||
const [curW, curH] = win.getSize()
|
||||
const resizing = width !== curW || height !== curH
|
||||
const restoreResizeLock = resizing && !win.isResizable()
|
||||
|
||||
try {
|
||||
if (restoreResizeLock) {
|
||||
win.setResizable(true)
|
||||
}
|
||||
|
||||
win.setBounds(nextBounds)
|
||||
} catch {
|
||||
// The window may disappear between validation and the native call.
|
||||
} finally {
|
||||
if (restoreResizeLock && !win.isDestroyed()) {
|
||||
win.setResizable(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hermes:hud:reset-layout', event => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (!hudWindow || hudWindow.isDestroyed() || event.sender !== hudWindow.webContents) {
|
||||
return { ok: false }
|
||||
}
|
||||
|
||||
return { ok: resetHudLayout() }
|
||||
})
|
||||
|
||||
// The HUD renderer reporting which session it is on, so the close broadcast
|
||||
// can hand it back to the app window (see hudSessionId).
|
||||
ipcMain.on('hermes:hud:session', (event, sessionId) => {
|
||||
const hudWindow = getHudWindow()
|
||||
|
||||
if (hudWindow && !hudWindow.isDestroyed() && event.sender === hudWindow.webContents) {
|
||||
setHudSessionId(typeof sessionId === 'string' && sessionId ? sessionId : null)
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('hermes:hud:close', async () => {
|
||||
closeHudWindow()
|
||||
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
// Main re-applies the frost when the translucency SETTING changes, since the
|
||||
// band's own report only fires when the band itself moves.
|
||||
return { applyHudFrost }
|
||||
}
|
||||
Reference in New Issue
Block a user