diff --git a/docs/references/window-manager/README.md b/docs/references/window-manager/README.md index 4326dcfc79..b8e420f7eb 100644 --- a/docs/references/window-manager/README.md +++ b/docs/references/window-manager/README.md @@ -99,7 +99,7 @@ Runtime setters for the declarative behavior layer live on `wm.behavior` (the {@ | Mode | Instances | `open()` behavior | `close()` behavior | Use for | |---|---|---|---|---| -| `default` | many | fresh create every call | destroys permanently | Windows that appear in parallel (e.g. detached tabs) | +| `default` | many | fresh create every call | destroys permanently | Windows that appear in parallel (e.g. sub windows) | | `singleton` | at most one | creates, or shows + focuses the existing one | destroys the sole instance | Unique windows (main, settings) | | `pooled` | many, reusable | pops an idle window, or creates fresh if empty | returns to the idle pool, or destroys if over cap | Frequently opened windows where creation cost matters (selection actions) | diff --git a/docs/references/window-manager/window-manager-api-reference.md b/docs/references/window-manager/window-manager-api-reference.md index 7dd1855c21..ee440bead7 100644 --- a/docs/references/window-manager/window-manager-api-reference.md +++ b/docs/references/window-manager/window-manager-api-reference.md @@ -32,7 +32,7 @@ These operate on the declarative `behavior` layer per instance and are exposed o |--------|-----------|-------------| | `wm.behavior.setHideOnBlur` | `(windowId: string, enabled: boolean) => void` | Override the declared `behavior.hideOnBlur` at runtime. `enabled: true` keeps auto-hide on; `enabled: false` suppresses (effectively "pinned"). No-op when the window type does not declare `behavior.hideOnBlur` (no listener to override). Override is cleared on window destroy and on pool `releaseToPool`. | | `wm.behavior.setAlwaysOnTop` | `(windowId: string, enabled: boolean) => void` | Toggle always-on-top using `level` / `relativeLevel` from `behavior.alwaysOnTop` (single source of truth). When neither is declared, `setAlwaysOnTop(enabled)` is called with no level — matching Electron's default. | -| `wm.behavior.setMacShowInDockByType` | `(type: WindowType, value: boolean) => void` | Override `behavior.macShowInDock` for an entire type at runtime. Use this to express "app is entering / leaving tray mode": `(Main, false)` before `window.hide()` makes the Dock track the transition; `(Main, true)` before `window.show()` lifts the suppression. Keyed by type (not windowId) so it can be set BEFORE the first instance exists (e.g. tray-on-launch path). When multiple window types contribute (e.g. Main + DetachedTab), the Dock stays visible as long as any contributing type is alive — `wm.behavior.setMacShowInDockByType(Main, false)` will not hide the Dock if a DetachedTab window is still present. | +| `wm.behavior.setMacShowInDockByType` | `(type: WindowType, value: boolean) => void` | Override `behavior.macShowInDock` for an entire type at runtime. Use this to express "app is entering / leaving tray mode": `(Main, false)` before `window.hide()` makes the Dock track the transition; `(Main, true)` before `window.show()` lifts the suppression. Keyed by type (not windowId) so it can be set BEFORE the first instance exists (e.g. tray-on-launch path). When multiple window types contribute (e.g. Main + SubWindow), the Dock stays visible as long as any contributing type is alive — `wm.behavior.setMacShowInDockByType(Main, false)` will not hide the Dock if a SubWindow is still present. | > No WM-level `setVisibleOnAllWorkspaces` is provided: its options differ per call in real usage (e.g. SelectionAction's full-screen show sequence), and WM has no state to maintain. Consumers call `window.setVisibleOnAllWorkspaces(enabled, options)` directly on the `BrowserWindow` instance. See [README → When to Provide a Runtime Setter](./README.md#when-to-provide-a-runtime-setter) for the decision rule. diff --git a/docs/references/window-manager/window-manager-overview.md b/docs/references/window-manager/window-manager-overview.md index 2248a1b24a..fcd85e0d12 100644 --- a/docs/references/window-manager/window-manager-overview.md +++ b/docs/references/window-manager/window-manager-overview.md @@ -50,20 +50,20 @@ WindowManager Multi-instance mode. Every `open()` call creates a fresh window. `close()` destroys it permanently. -**Use for**: windows that appear many times simultaneously (e.g., detached tabs). +**Use for**: windows that appear many times simultaneously (e.g., sub windows). ```typescript // windowRegistry.ts -WINDOW_TYPE_REGISTRY[WindowType.DetachedTab] = { - type: WindowType.DetachedTab, +WINDOW_TYPE_REGISTRY[WindowType.SubWindow] = { + type: WindowType.SubWindow, lifecycle: 'default', - htmlPath: 'detached-tab.html', + htmlPath: 'sub-window.html', windowOptions: { ...DEFAULT_WINDOW_CONFIG }, } // Usage — each call creates a new window -const tab1 = wm.open(WindowType.DetachedTab) -const tab2 = wm.open(WindowType.DetachedTab) +const tab1 = wm.open(WindowType.SubWindow) +const tab2 = wm.open(WindowType.SubWindow) wm.close(tab1) // destroyed ``` diff --git a/docs/references/window-manager/window-manager-platform.md b/docs/references/window-manager/window-manager-platform.md index 18baad47c9..e2e1116b83 100644 --- a/docs/references/window-manager/window-manager-platform.md +++ b/docs/references/window-manager/window-manager-platform.md @@ -79,7 +79,7 @@ Runtime setters for the behavior layer live on `wm.behavior` (a `BehaviorControl |---|---| | `wm.behavior.setHideOnBlur(id, enabled)` | Override the declared `behavior.hideOnBlur` per instance. Cleared on destroy and on pool `releaseToPool` — pool consumers that need a non-default value must re-apply after `open()` / reuse. No-op when the window does not declare `behavior.hideOnBlur`. | | `wm.behavior.setAlwaysOnTop(id, enabled)` | Toggle always-on-top using the `level` / `relativeLevel` declared in `behavior.alwaysOnTop`. When neither is declared, calls `setAlwaysOnTop(enabled)` with no level. | -| `wm.behavior.setMacShowInDockByType(type, value)` | Override `behavior.macShowInDock` for an entire window type (not a single instance). Use for app-level tray-mode transitions: `(Main, false)` then `hide()` pulls the Dock icon down; `(Main, true)` then `show()` brings it back. Keyed by type so it can be set BEFORE the first instance exists (tray-on-launch). Multi-window safe: with `Main + DetachedTab` both contributing, a `wm.behavior.setMacShowInDockByType(Main, false)` alone does NOT hide the Dock while any DetachedTab is alive. | +| `wm.behavior.setMacShowInDockByType(type, value)` | Override `behavior.macShowInDock` for an entire window type (not a single instance). Use for app-level tray-mode transitions: `(Main, false)` then `hide()` pulls the Dock icon down; `(Main, true)` then `show()` brings it back. Keyed by type so it can be set BEFORE the first instance exists (tray-on-launch). Multi-window safe: with `Main + SubWindow` both contributing, a `wm.behavior.setMacShowInDockByType(Main, false)` alone does NOT hide the Dock while any SubWindow is alive. | `setVisibleOnAllWorkspaces` intentionally has **no** WM-level setter — consumers call it directly on the `BrowserWindow` when needed. See [README → When to Provide a Runtime Setter](./README.md#when-to-provide-a-runtime-setter). diff --git a/electron.vite.config.ts b/electron.vite.config.ts index 29d051242b..a9a45976d5 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -151,7 +151,7 @@ export default defineConfig({ selectionAction: resolve(__dirname, 'src/renderer/selectionAction.html'), traceWindow: resolve(__dirname, 'src/renderer/traceWindow.html'), migrationV2: resolve(__dirname, 'src/renderer/migrationV2.html'), - detachedWindow: resolve(__dirname, 'src/renderer/detachedWindow.html') + subWindow: resolve(__dirname, 'src/renderer/subWindow.html') }, onwarn(warning, warn) { if (warning.code === 'COMMONJS_VARIABLE_IN_ESM') return diff --git a/src/main/core/application/serviceRegistry.ts b/src/main/core/application/serviceRegistry.ts index af572b0722..a252e2117d 100644 --- a/src/main/core/application/serviceRegistry.ts +++ b/src/main/core/application/serviceRegistry.ts @@ -9,7 +9,6 @@ import { ApiServerService } from '@main/services/ApiServerService' import { AppMenuService } from '@main/services/AppMenuService' import { AppUpdaterService } from '@main/services/AppUpdaterService' import { CodeCliService } from '@main/services/CodeCliService' -import { DetachedWindowManager } from '@main/services/DetachedWindowManager' import { KnowledgeOrchestrationService, KnowledgeRuntimeService } from '@main/services/knowledge' import { KnowledgeVectorStoreService } from '@main/services/knowledge/vectorstore/KnowledgeVectorStoreService' import { LanTransferService } from '@main/services/lanTransfer' @@ -28,6 +27,7 @@ import { SearchService } from '@main/services/SearchService' import { SelectionService } from '@main/services/SelectionService' import { ShortcutService } from '@main/services/ShortcutService' import { SpanCacheService } from '@main/services/SpanCacheService' +import { SubWindowService } from '@main/services/SubWindowService' import { ThemeService } from '@main/services/ThemeService' import { TrayService } from '@main/services/TrayService' import { WebviewService } from '@main/services/WebviewService' @@ -62,7 +62,7 @@ export const services = { DbService, CacheService, DataApiService, - DetachedWindowManager, + SubWindowService, PreferenceService, AnalyticsService, AppMenuService, diff --git a/src/main/core/window/types.ts b/src/main/core/window/types.ts index 68313807d2..05e0e6dc6d 100644 --- a/src/main/core/window/types.ts +++ b/src/main/core/window/types.ts @@ -8,7 +8,7 @@ import type { BrowserWindow, BrowserWindowConstructorOptions, VisibleOnAllWorksp export enum WindowType { Main = 'main', QuickAssistant = 'quickAssistant', - DetachedTab = 'detachedTab', + SubWindow = 'subWindow', SelectionToolbar = 'selectionToolbar', SelectionAction = 'selectionAction' } diff --git a/src/main/services/MainWindowService.ts b/src/main/services/MainWindowService.ts index 2a30a11360..ced6aa31b2 100644 --- a/src/main/services/MainWindowService.ts +++ b/src/main/services/MainWindowService.ts @@ -124,7 +124,7 @@ export class MainWindowService extends BaseService { /** * Resolves the BrowserWindow that originated the IPC call. * Used for window-control channels (minimize/maximize/close) that must operate - * on whichever window sent the IPC — main window or a detached tab window. + * on whichever window sent the IPC — main window or a sub window. * Throws if the sender cannot be mapped to a live window. */ private resolveIpcSenderWindow(sender: Electron.WebContents): BrowserWindow { @@ -568,7 +568,7 @@ export class MainWindowService extends BaseService { // macOS close-to-tray: opt Main windows out of Dock contribution BEFORE hiding. // This tells WindowManager "the app is now in tray mode" so the Dock icon goes // away too. Unlike the previous hard-coded app.dock?.hide(), this cooperates - // with multi-window scenarios: if a DetachedTab (or any other Dock-contributing + // with multi-window scenarios: if a SubWindow (or any other Dock-contributing // window) is still alive, it will keep the Dock visible. The override is lifted // in showMainWindow/toggleMainWindow when the user brings Main back. if (isMac && isTrayOnClose) { @@ -666,7 +666,7 @@ export class MainWindowService extends BaseService { if (application.get('PreferenceService').get('app.tray.on_close')) { // Same pattern as the close handler: tell WM to stop counting Main // toward Dock visibility BEFORE hiding, so the Dock coordinates with - // whatever else is alive (e.g. a DetachedTab) rather than blindly hiding. + // whatever else is alive (e.g. a SubWindow) rather than blindly hiding. if (isMac) { application.get('WindowManager').behavior.setMacShowInDockByType(WindowType.Main, false) } diff --git a/src/main/services/DetachedWindowManager.ts b/src/main/services/SubWindowService.ts similarity index 79% rename from src/main/services/DetachedWindowManager.ts rename to src/main/services/SubWindowService.ts index d24f634bdb..ab17d28bbb 100644 --- a/src/main/services/DetachedWindowManager.ts +++ b/src/main/services/SubWindowService.ts @@ -11,14 +11,14 @@ import { join } from 'path' import icon from '../../../build/icon.png?asset' -const logger = loggerService.withContext('DetachedWindowManager') +const logger = loggerService.withContext('SubWindowService') /** Height of the tab bar area used for drag-to-attach detection (must match CSS h-10) */ const TAB_BAR_HEIGHT = 40 /** Must match createWindow BrowserWindow width/height */ -const DETACHED_DEFAULT_WIDTH = 800 -const DETACHED_DEFAULT_HEIGHT = 600 +const SUB_WINDOW_DEFAULT_WIDTH = 800 +const SUB_WINDOW_DEFAULT_HEIGHT = 600 /** * After Tab_MoveWindow, ignore `resize` bursts briefly so DPI rounding noise is not written back @@ -27,10 +27,10 @@ const DETACHED_DEFAULT_HEIGHT = 600 */ const MOVE_RESIZE_IGNORE_MS = 280 -/** Win/Linux: move detached windows with setContentBounds + cached size (see electron#27651). */ +/** Win/Linux: move sub windows with setContentBounds + cached size (see electron#27651). */ const USE_CONTENT_BOUNDS_MOVE = isWin || isLinux -type DetachedWindowState = { +type SubWindowState = { /** Cached content size to avoid getBounds() round-trips during drag (electron#27651) */ width: number height: number @@ -38,12 +38,12 @@ type DetachedWindowState = { lastMoveAt: number } -@Injectable('DetachedWindowManager') +@Injectable('SubWindowService') @ServicePhase(Phase.WhenReady) @DependsOn(['WindowManager']) -export class DetachedWindowManager extends BaseService { +export class SubWindowService extends BaseService { private windows: Map = new Map() - private windowState: Map = new Map() + private windowState: Map = new Map() private windowUrls: Map = new Map() protected async onInit() { @@ -69,16 +69,16 @@ export class DetachedWindowManager extends BaseService { return false } - // Close sender detached window after successful broadcast. Main-window - // senders are skipped because they are not in the DetachedWindowManager - // pool (this.windows) and the check below only fires for detached tabs. + // Close sender sub window after successful broadcast. Main-window + // senders are skipped because they are not in the SubWindowService + // pool (this.windows) and the check below only fires for sub windows. const senderWindow = BrowserWindow.fromWebContents(event.sender) - const isDetachedTab = senderWindow ? Array.from(this.windows.values()).includes(senderWindow) : false - if (senderWindow && isDetachedTab && !senderWindow.isDestroyed()) { + const isSubWindow = senderWindow ? Array.from(this.windows.values()).includes(senderWindow) : false + if (senderWindow && isSubWindow && !senderWindow.isDestroyed()) { try { senderWindow.close() } catch (err: any) { - logger.error('Failed to close detached window after tab attach', err as Error) + logger.error('Failed to close sub window after tab attach', err as Error) } } return true @@ -86,7 +86,7 @@ export class DetachedWindowManager extends BaseService { this.ipcOn(IpcChannel.Tab_MoveWindow, (event, payload: { tabId: string; x: number; y: number }) => { // Prefer tabId lookup: when the main window sends this IPC, event.sender is the main window, - // but we want to move the detached window identified by tabId. + // but we want to move the sub window identified by tabId. const win = this.windows.get(payload.tabId) ?? BrowserWindow.fromWebContents(event.sender) if (win && !win.isDestroyed()) { const x = Math.round(payload.x) @@ -95,8 +95,8 @@ export class DetachedWindowManager extends BaseService { if (!win.isVisible()) { win.show() } - // Only apply opacity when the detached window is dragging its own tab (preparing to reattach). - // When the main window sends Tab_MoveWindow, event.sender differs from the detached window. + // Only apply opacity when the sub window is dragging its own tab (preparing to reattach). + // When the main window sends Tab_MoveWindow, event.sender differs from the sub window. const senderWindow = BrowserWindow.fromWebContents(event.sender) if (senderWindow === win && win.getOpacity() !== 0.85) { win.setOpacity(0.85) @@ -133,17 +133,17 @@ export class DetachedWindowManager extends BaseService { return false } - const detachedWin = this.windows.get(payload.tab.id) - if (detachedWin && !detachedWin.isDestroyed()) { - detachedWin.close() + const subWin = this.windows.get(payload.tab.id) + if (subWin && !subWin.isDestroyed()) { + subWin.close() } return true } // Not over tab bar — restore opacity - const detachedWin = this.windows.get(payload.tab.id) - if (detachedWin && !detachedWin.isDestroyed()) { - detachedWin.setOpacity(1) + const subWin = this.windows.get(payload.tab.id) + if (subWin && !subWin.isDestroyed()) { + subWin.setOpacity(1) } return false @@ -160,7 +160,7 @@ export class DetachedWindowManager extends BaseService { } /** - * Moves a detached window to (x, y). + * Moves a sub window to (x, y). * On Win/Linux uses setContentBounds with cached size to avoid electron#27651 outer-bounds creep. * On macOS uses setPosition (no reported creep issue). */ @@ -170,7 +170,7 @@ export class DetachedWindowManager extends BaseService { if (state) { state.lastMoveAt = Date.now() } - const { width, height } = state ?? { width: DETACHED_DEFAULT_WIDTH, height: DETACHED_DEFAULT_HEIGHT } + const { width, height } = state ?? { width: SUB_WINDOW_DEFAULT_WIDTH, height: SUB_WINDOW_DEFAULT_HEIGHT } // electron#27651: avoid outer getBounds/setBounds round-trips during drag win.setContentBounds({ x, y, width, height }) } else { @@ -179,11 +179,11 @@ export class DetachedWindowManager extends BaseService { } /** - * Tracks the content size of a detached window, keeping windowState in sync. + * Tracks the content size of a sub window, keeping windowState in sync. * Must be called once per created window; cleans up state on close. */ private trackWindowSize(tabId: string, win: BrowserWindow) { - this.windowState.set(tabId, { width: DETACHED_DEFAULT_WIDTH, height: DETACHED_DEFAULT_HEIGHT, lastMoveAt: 0 }) + this.windowState.set(tabId, { width: SUB_WINDOW_DEFAULT_WIDTH, height: SUB_WINDOW_DEFAULT_HEIGHT, lastMoveAt: 0 }) win.on('ready-to-show', () => { if (!win.isDestroyed() && USE_CONTENT_BOUNDS_MOVE) { @@ -234,8 +234,8 @@ export class DetachedWindowManager extends BaseService { const hasPosition = x !== undefined && y !== undefined const win = new BrowserWindow({ - width: DETACHED_DEFAULT_WIDTH, - height: DETACHED_DEFAULT_HEIGHT, + width: SUB_WINDOW_DEFAULT_WIDTH, + height: SUB_WINDOW_DEFAULT_HEIGHT, minWidth: 400, minHeight: 300, ...(hasPosition ? { x, y } : {}), @@ -267,18 +267,18 @@ export class DetachedWindowManager extends BaseService { }) if (is.dev && process.env['ELECTRON_RENDERER_URL']) { - win.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/detachedWindow.html?${params.toString()}`).catch((err) => { - logger.error(`Failed to load detached window URL for tab ${tabId}`, err) + win.loadURL(`${process.env['ELECTRON_RENDERER_URL']}/subWindow.html?${params.toString()}`).catch((err) => { + logger.error(`Failed to load sub window URL for tab ${tabId}`, err) this.windows.delete(tabId) if (!win.isDestroyed()) win.close() }) } else { win - .loadFile(join(__dirname, '../renderer/detachedWindow.html'), { + .loadFile(join(__dirname, '../renderer/subWindow.html'), { search: params.toString() }) .catch((err) => { - logger.error(`Failed to load detached window file for tab ${tabId}`, err) + logger.error(`Failed to load sub window file for tab ${tabId}`, err) this.windows.delete(tabId) if (!win.isDestroyed()) win.close() }) @@ -306,7 +306,7 @@ export class DetachedWindowManager extends BaseService { this.windows.set(tabId, win) this.windowUrls.set(tabId, url) - logger.info(`Created detached window for tab ${tabId}`, payload) + logger.info(`Created sub window for tab ${tabId}`, payload) return win } diff --git a/src/renderer/src/windows/detachedWindow/AppShell.tsx b/src/renderer/src/windows/subWindow/AppShell.tsx similarity index 96% rename from src/renderer/src/windows/detachedWindow/AppShell.tsx rename to src/renderer/src/windows/subWindow/AppShell.tsx index 254f5883be..5296a044bb 100644 --- a/src/renderer/src/windows/detachedWindow/AppShell.tsx +++ b/src/renderer/src/windows/subWindow/AppShell.tsx @@ -18,7 +18,7 @@ const WebviewContainer = ({ url, isActive }: { url: string; isActive: boolean }) ) -export const DetachedAppShell = () => { +export const SubWindowAppShell = () => { const { tabs, activeTabId, setActiveTab, closeTab, updateTab, addTab, reorderTabs, openTab, pinTab, unpinTab } = useTabs() const initialized = useRef(false) @@ -55,7 +55,7 @@ export const DetachedAppShell = () => { } }, [openTab, setActiveTab]) - // Close tab in detached window. closeTab handles both pinned and normal tabs correctly. + // Close tab in sub window. closeTab handles both pinned and normal tabs correctly. // Do NOT call unpinTab before closeTab — unpinTab moves the tab to normalTabs, // then closeTab's closure still sees isPinned=true and filters the wrong list. const handleCloseTab = (id: string) => { diff --git a/src/renderer/src/windows/detachedWindow/entryPoint.tsx b/src/renderer/src/windows/subWindow/entryPoint.tsx similarity index 89% rename from src/renderer/src/windows/detachedWindow/entryPoint.tsx rename to src/renderer/src/windows/subWindow/entryPoint.tsx index 92b5dc3073..4a1b553ea7 100644 --- a/src/renderer/src/windows/detachedWindow/entryPoint.tsx +++ b/src/renderer/src/windows/subWindow/entryPoint.tsx @@ -6,7 +6,7 @@ import '@renderer/databases' import { preferenceService } from '@data/PreferenceService' import { loggerService } from '@logger' import store, { persistor } from '@renderer/store' -import { DetachedAppShell } from '@renderer/windows/detachedWindow/AppShell' +import { SubWindowAppShell } from '@renderer/windows/subWindow/AppShell' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { createRoot } from 'react-dom/client' import { Provider } from 'react-redux' @@ -21,7 +21,7 @@ import { TabsProvider } from '../../context/TabsContext' import { ThemeProvider } from '../../context/ThemeProvider' // Initialize logger for this window -loggerService.initWindowSource('DetachedTab') +loggerService.initWindowSource('SubWindow') void preferenceService.preloadAll() @@ -35,7 +35,7 @@ const queryClient = new QueryClient({ } }) -function DetachedTabApp(): React.ReactElement { +function SubWindowApp(): React.ReactElement { return ( @@ -47,7 +47,7 @@ function DetachedTabApp(): React.ReactElement { - + @@ -62,4 +62,4 @@ function DetachedTabApp(): React.ReactElement { } const root = createRoot(document.getElementById('root') as HTMLElement) -root.render() +root.render() diff --git a/src/renderer/detachedWindow.html b/src/renderer/subWindow.html similarity index 89% rename from src/renderer/detachedWindow.html rename to src/renderer/subWindow.html index eeba657465..fbf1252ad9 100644 --- a/src/renderer/detachedWindow.html +++ b/src/renderer/subWindow.html @@ -19,6 +19,6 @@
- +