import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Button, ConfigProvider, Spin, Tooltip, theme as antdTheme } from 'antd'; import { CloseOutlined, CompressOutlined } from '@ant-design/icons'; import { EventsOn } from '../../wailsjs/runtime'; import { t as defaultTranslate } from '../i18n'; import { getAntdLocale } from '../i18n/frameworkLocale'; import { useOptionalI18n } from '../i18n/provider'; import { flushAIChatSessionPersistence, type SqlLog, useStore } from '../store'; import type { TabData } from '../types'; import type { DetachedQueryResultWindow } from '../utils/detachedWindow'; import { attachNativeDetachedWindow, advanceNativeDetachedStoreSource, applyNativeDetachedHostStateCommand, buildNativeDetachedAIChatSyncStoreSnapshot, buildNativeDetachedChangedWorkbenchStoreSnapshot, buildNativeDetachedStoreSnapshot, buildNativeDetachedSyncStoreSnapshot, buildNativeDetachedWorkbenchMutableStoreSnapshot, cancelNativeDetachedWindowClose, cancelCurrentNativeDetachedWindowClose, closeCurrentNativeDetachedWindow, closeNativeDetachedWindow, fetchNativeDetachedWindowBootstrap, hydrateNativeDetachedStore, hideCurrentNativeDetachedWindow, hideNativeDetachedWindow, openNativeDetachedAISettings, presentCurrentNativeDetachedWindow, readyNativeDetachedWindow, sendNativeDetachedHostEvent, syncNativeDetachedWindow, type NativeDetachedHostEvent, type NativeDetachedHostEventName, type NativeDetachedStoreSnapshot, type NativeDetachedWindowActionPayload, type NativeDetachedWindowBootstrap, NATIVE_DETACHED_WINDOW_COMMAND_EVENT, type NativeDetachedHostStateCommand, } from '../utils/nativeDetachedWindowClient'; import { isMacLikePlatform } from '../utils/appearance'; import { peekQueryEditorResultSession, saveQueryEditorResultSession, subscribeQueryEditorResultSession, type QueryEditorResultSessionSnapshot, } from '../utils/queryEditorResultSessionCache'; import { buildOverlayWorkbenchTheme } from '../utils/overlayWorkbenchTheme'; import { resolveLiveQueryTab, resolveLiveQueryTabs } from '../utils/liveQueryTabs'; import { subscribeQueryTabDraftChanges } from '../utils/sqlFileTabDrafts'; import { getShortcutPlatform, installGlobalImeCompositionTracking, isShortcutMatch, resolveShortcutBinding, } from '../utils/shortcuts'; const AIChatPanel = React.lazy(() => import('./AIChatPanel')); const DataGrid = React.lazy(() => import('./DataGrid')); const WorkbenchTabContent = React.lazy(() => import('./WorkbenchTabContent')); const NativeDetachedWindowController = React.lazy( () => import('./NativeDetachedWindowController'), ); export const NATIVE_DETACHED_SYNC_DEBOUNCE_MS = 180; export const NATIVE_DETACHED_PAINT_FALLBACK_MS = 250; export const waitForNativeDetachedContentPaint = (): Promise => { if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') { return Promise.resolve(); } return new Promise((resolve) => { let settled = false; let fallbackTimer: ReturnType | undefined; const finish = () => { if (settled) return; settled = true; if (fallbackTimer !== undefined) clearTimeout(fallbackTimer); resolve(); }; fallbackTimer = setTimeout(finish, NATIVE_DETACHED_PAINT_FALLBACK_MS); window.requestAnimationFrame(() => { window.requestAnimationFrame(finish); }); }); }; type NativeDetachedWindowClient = { load: () => Promise; present?: () => Promise; ready: (payload: NativeDetachedWindowActionPayload) => Promise; sync: (payload: NativeDetachedWindowActionPayload) => Promise; attach: (payload: NativeDetachedWindowActionPayload) => Promise; hide?: (payload: NativeDetachedWindowActionPayload) => Promise; close: (payload: NativeDetachedWindowActionPayload) => Promise; cancelCloseRequest?: (payload: NativeDetachedWindowActionPayload) => Promise; openAISettings: (payload: NativeDetachedWindowActionPayload) => Promise; hostEvent?: (payload: NativeDetachedWindowActionPayload) => Promise; closeCurrentWindow: () => Promise; hideCurrentWindow?: (visibilityRevision: number) => Promise; cancelClose?: () => Promise; }; const defaultClient: NativeDetachedWindowClient = { load: fetchNativeDetachedWindowBootstrap, present: presentCurrentNativeDetachedWindow, ready: readyNativeDetachedWindow, sync: syncNativeDetachedWindow, attach: attachNativeDetachedWindow, hide: hideNativeDetachedWindow, close: closeNativeDetachedWindow, cancelCloseRequest: cancelNativeDetachedWindowClose, openAISettings: openNativeDetachedAISettings, hostEvent: sendNativeDetachedHostEvent, closeCurrentWindow: closeCurrentNativeDetachedWindow, hideCurrentWindow: hideCurrentNativeDetachedWindow, cancelClose: cancelCurrentNativeDetachedWindowClose, }; export interface NativeDetachedWindowAppProps { client?: NativeDetachedWindowClient; } const isAffectedRowsResult = (columns: string[]): boolean => columns.length === 1 && columns[0] === 'affectedRows'; const buildActionPayload = ( bootstrap: NativeDetachedWindowBootstrap, tab?: TabData, resultSession?: QueryEditorResultSessionSnapshot | null, includeResultSession = false, newSqlLogs: SqlLog[] = [], revision?: number, workbenchState?: NativeDetachedStoreSnapshot, workbenchStateBase?: NativeDetachedStoreSnapshot, openedTabs: TabData[] = [], clearSqlLogs = false, resultWindow?: DetachedQueryResultWindow | null, ): NativeDetachedWindowActionPayload => { const storeState = bootstrap.kind === 'ai-chat' ? buildNativeDetachedAIChatSyncStoreSnapshot(useStore.getState(), newSqlLogs) : buildNativeDetachedSyncStoreSnapshot( useStore.getState(), bootstrap.kind === 'workbench' ? bootstrap.payload.tab?.id || '' : '', newSqlLogs, ); const screenX = typeof window === 'undefined' ? Number.NaN : Number(window.screenX); const screenY = typeof window === 'undefined' ? Number.NaN : Number(window.screenY); const width = typeof window === 'undefined' ? Number.NaN : Number(window.outerWidth || window.innerWidth); const height = typeof window === 'undefined' ? Number.NaN : Number(window.outerHeight || window.innerHeight); const bounds = [screenX, screenY, width, height].every(Number.isFinite) && width > 0 && height > 0 ? { x: Math.round(screenX), y: Math.round(screenY), width: Math.round(width), height: Math.round(height), } : undefined; return { id: bootstrap.id, kind: bootstrap.kind, ...(revision && revision > 0 ? { revision } : {}), ...(bounds ? { bounds } : {}), ...(workbenchState && Object.keys(workbenchState).length > 0 ? { workbenchState } : {}), ...(workbenchState && Object.keys(workbenchState).length > 0 ? { workbenchStateBase: workbenchStateBase ?? {} } : {}), ...(openedTabs.length > 0 ? { openedTabs } : {}), ...(clearSqlLogs ? { clearSqlLogs: true } : {}), ...(bootstrap.kind === 'workbench' || Object.keys(storeState).length > 0 ? { storeState } : {}), ...(tab ? { tab } : {}), ...(bootstrap.kind === 'query-result' && resultWindow ? { resultWindow } : {}), ...(bootstrap.kind === 'workbench' && includeResultSession ? { resultSession: resultSession ?? null } : {}), }; }; const NativeDetachedQueryResult: React.FC<{ windowState: DetachedQueryResultWindow; onDataChange: (rows: Array>) => void; }> = ({ windowState, onDataChange }) => { const result = windowState.result; const isMessage = result.resultType === 'message' || isAffectedRowsResult(result.columns || []); const messageText = (result.messages || []).join('\n') || (isAffectedRowsResult(result.columns || []) ? String(result.rows?.[0]?.affectedRows ?? '') : ''); if (isMessage) { return (