Files
hermes-agent/apps/desktop/src/store/windows.ts
T
brooklyn!andtim404x b96bd4808d feat(desktop): open any chat in its own window (#43219)
Pops a session into a standalone, focused window for side-by-side work.
A secondary window loads the renderer at the session route with a
?win=secondary flag (ahead of the HashRouter '#'); it drops the global
sidebar plus the install/onboarding overlays and renders a single chat,
sharing the one local gateway over WS (no backend duplication). The main
process keys windows by sessionId so re-opening focuses the existing one
and self-cleans on close.

Open it via:
- ⌘-click (mac) / ⌃-click (win/linux) a sidebar session — the universal
  "open in new window" gesture. Archive moves to the ⋯ / right-click menus
  only, off the easy-to-misfire modifier-click.
- "New window" in the session ⋯ and context menus (link-external icon,
  i18n'd across en/ja/zh/zh-hant).

A standalone window has no left rail, so AppShell treats its edge as
uncovered and applies the titlebar inset — the chat title clears the
macOS traffic lights instead of hiding behind them.

Co-authored-by: tim404x <tim404x@users.noreply.github.com>
2026-06-09 21:09:45 -05:00

53 lines
1.7 KiB
TypeScript

import { notifyError } from './notifications'
// Window flag set by the Electron main process when it opens a standalone
// session window (see electron/main.cjs buildSessionWindowUrl). It rides in the
// query string BEFORE the HashRouter '#', so we read it from location.search,
// never from the router. A "secondary" window renders a single chat without the
// global session sidebar or the install / onboarding overlays.
const SECONDARY_WINDOW_FLAG = 'secondary'
let secondaryWindowCache: boolean | null = null
export function isSecondaryWindow(): boolean {
if (secondaryWindowCache !== null) {
return secondaryWindowCache
}
let result = false
try {
result = new URLSearchParams(window.location.search).get('win') === SECONDARY_WINDOW_FLAG
} catch {
result = false
}
secondaryWindowCache = result
return result
}
// True when running inside the Electron desktop shell (the preload bridge is
// present). The "open in new window" affordance is desktop-only.
export function canOpenSessionWindow(): boolean {
return typeof window !== 'undefined' && typeof window.hermesDesktop?.openSessionWindow === 'function'
}
// Open (or focus) a standalone OS window for a single chat session. No-ops
// gracefully outside Electron so callers can wire it unconditionally.
export async function openSessionInNewWindow(sessionId: string): Promise<void> {
if (!sessionId || !canOpenSessionWindow()) {
return
}
try {
const result = await window.hermesDesktop.openSessionWindow(sessionId)
if (!result?.ok) {
notifyError(new Error(result?.error || 'unknown error'), 'Could not open chat in a new window')
}
} catch (err) {
notifyError(err, 'Could not open chat in a new window')
}
}