refactor(command-palette): wire openFile through PaletteOpsContext

This commit is contained in:
simosmik
2026-04-30 07:59:00 +00:00
parent 9179ca2f00
commit dc281774b5
5 changed files with 82 additions and 14 deletions

View File

@@ -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 (
<PaletteOpsProvider>
<div className="fixed inset-0 flex bg-background" style={{ bottom: 'var(--keyboard-height, 0px)' }}>
{!isMobile ? (
<div className="h-full flex-shrink-0 border-r border-border/50">
@@ -212,5 +214,6 @@ export default function AppContent() {
onShowTab={setActiveTab}
/>
</div>
</PaletteOpsProvider>
);
}

View File

@@ -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) => {

View File

@@ -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 <MainContentStateView mode="loading" isMobile={isMobile} onMenuClick={onMenuClick} />;

View File

@@ -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> | void;
};
type Handle = {
handlersRef: React.MutableRefObject<Partial<PaletteOps>>;
call: PaletteOps;
};
const PaletteOpsContext = createContext<Handle | null>(null);
const noop = () => undefined;
export function PaletteOpsProvider({ children }: { children: ReactNode }) {
const handlersRef = useRef<Partial<PaletteOps>>({});
const call = useMemo<PaletteOps>(
() => ({
openFile: (path) => handlersRef.current.openFile?.(path),
openSettings: (tab) => handlersRef.current.openSettings?.(tab),
refreshProjects: () => handlersRef.current.refreshProjects?.() ?? undefined,
}),
[],
);
const value = useMemo<Handle>(() => ({ handlersRef, call }), [call]);
return <PaletteOpsContext.Provider value={value}>{children}</PaletteOpsContext.Provider>;
}
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<PaletteOps>) {
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]);
}

View File

@@ -5,7 +5,6 @@ declare global {
__ROUTER_BASENAME__?: string;
refreshProjects?: () => void | Promise<void>;
openSettings?: (tab?: string) => void;
openFile?: (filePath: string) => void;
}
interface EventSourceEventMap {