diff --git a/src/components/app/AppContent.tsx b/src/components/app/AppContent.tsx index db42a378..42005c18 100644 --- a/src/components/app/AppContent.tsx +++ b/src/components/app/AppContent.tsx @@ -6,6 +6,7 @@ import Sidebar from '../sidebar/view/Sidebar'; import MainContent from '../main-content/view/MainContent'; import CommandPalette from '../command-palette/CommandPalette'; import { useWebSocket } from '../../contexts/WebSocketContext'; +import { PaletteOpsProvider } from '../../contexts/PaletteOpsContext'; import { useDeviceSettings } from '../../hooks/useDeviceSettings'; import { useSessionProtection } from '../../hooks/useSessionProtection'; import { useProjectsState } from '../../hooks/useProjectsState'; @@ -146,6 +147,7 @@ export default function AppContent() { }, []); return ( +
{!isMobile ? (
@@ -212,5 +214,6 @@ export default function AppContent() { onShowTab={setActiveTab} />
+ ); } diff --git a/src/components/command-palette/CommandPalette.tsx b/src/components/command-palette/CommandPalette.tsx index 9520974b..11ae12a4 100644 --- a/src/components/command-palette/CommandPalette.tsx +++ b/src/components/command-palette/CommandPalette.tsx @@ -13,6 +13,7 @@ import { DialogTitle, } from '../../shared/view/ui'; import { useTheme } from '../../contexts/ThemeContext'; +import { usePaletteOps } from '../../contexts/PaletteOpsContext'; import type { AppTab, Project } from '../../types/app'; import { GROUPS, parseMode } from './registry'; @@ -35,6 +36,7 @@ export default function CommandPalette({ const [search, setSearch] = React.useState(''); const { toggleDarkMode } = useTheme(); const navigate = useNavigate(); + const ops = usePaletteOps(); React.useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -60,8 +62,8 @@ export default function CommandPalette({ }, []); const openFile = React.useCallback((path: string) => { - window.openFile?.(path); - }, []); + ops.openFile(path); + }, [ops]); const filter = React.useCallback( (value: string, rawSearch: string) => { diff --git a/src/components/main-content/view/MainContent.tsx b/src/components/main-content/view/MainContent.tsx index ba3d0840..a86dcbc9 100644 --- a/src/components/main-content/view/MainContent.tsx +++ b/src/components/main-content/view/MainContent.tsx @@ -7,6 +7,7 @@ import GitPanel from '../../git-panel/view/GitPanel'; import PluginTabContent from '../../plugins/view/PluginTabContent'; import type { MainContentProps } from '../types/types'; import { useTaskMaster } from '../../../contexts/TaskMasterContext'; +import { usePaletteOpsRegister } from '../../../contexts/PaletteOpsContext'; import { useTasksSettings } from '../../../contexts/TasksSettingsContext'; import { useUiPreferences } from '../../../hooks/useUiPreferences'; import { useEditorSidebar } from '../../code-editor/hooks/useEditorSidebar'; @@ -91,19 +92,12 @@ function MainContent({ } }, [shouldShowTasksTab, activeTab, setActiveTab]); - // Expose file-open to non-descendant features (command palette). - useEffect(() => { - const open = (filePath: string) => { + usePaletteOpsRegister({ + openFile: (filePath: string) => { setActiveTab('files'); handleFileOpen(filePath); - }; - window.openFile = open; - return () => { - if (window.openFile === open) { - delete window.openFile; - } - }; - }, [handleFileOpen, setActiveTab]); + }, + }); if (isLoading) { return ; diff --git a/src/contexts/PaletteOpsContext.tsx b/src/contexts/PaletteOpsContext.tsx new file mode 100644 index 00000000..c26f6a0c --- /dev/null +++ b/src/contexts/PaletteOpsContext.tsx @@ -0,0 +1,70 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import type { ReactNode } from 'react'; + +export type PaletteOps = { + openFile: (path: string) => void; + openSettings: (tab?: string) => void; + refreshProjects: () => Promise | void; +}; + +type Handle = { + handlersRef: React.MutableRefObject>; + call: PaletteOps; +}; + +const PaletteOpsContext = createContext(null); + +const noop = () => undefined; + +export function PaletteOpsProvider({ children }: { children: ReactNode }) { + const handlersRef = useRef>({}); + + const call = useMemo( + () => ({ + openFile: (path) => handlersRef.current.openFile?.(path), + openSettings: (tab) => handlersRef.current.openSettings?.(tab), + refreshProjects: () => handlersRef.current.refreshProjects?.() ?? undefined, + }), + [], + ); + + const value = useMemo(() => ({ handlersRef, call }), [call]); + + return {children}; +} + +export function usePaletteOps(): PaletteOps { + const handle = useContext(PaletteOpsContext); + if (!handle) { + return { openFile: noop, openSettings: noop, refreshProjects: noop }; + } + return handle.call; +} + +export function usePaletteOpsRegister(partial: Partial) { + const handle = useContext(PaletteOpsContext); + const openFile = partial.openFile; + const openSettings = partial.openSettings; + const refreshProjects = partial.refreshProjects; + + const installer = useCallback(() => { + if (!handle) return undefined; + const prev = { ...handle.handlersRef.current }; + if (openFile) handle.handlersRef.current.openFile = openFile; + if (openSettings) handle.handlersRef.current.openSettings = openSettings; + if (refreshProjects) handle.handlersRef.current.refreshProjects = refreshProjects; + return () => { + if (openFile && handle.handlersRef.current.openFile === openFile) { + handle.handlersRef.current.openFile = prev.openFile; + } + if (openSettings && handle.handlersRef.current.openSettings === openSettings) { + handle.handlersRef.current.openSettings = prev.openSettings; + } + if (refreshProjects && handle.handlersRef.current.refreshProjects === refreshProjects) { + handle.handlersRef.current.refreshProjects = prev.refreshProjects; + } + }; + }, [handle, openFile, openSettings, refreshProjects]); + + useEffect(() => installer(), [installer]); +} diff --git a/src/types/global.d.ts b/src/types/global.d.ts index aea2538a..ba368e4b 100644 --- a/src/types/global.d.ts +++ b/src/types/global.d.ts @@ -5,7 +5,6 @@ declare global { __ROUTER_BASENAME__?: string; refreshProjects?: () => void | Promise; openSettings?: (tab?: string) => void; - openFile?: (filePath: string) => void; } interface EventSourceEventMap {