feat: add desktop quick open workflow (#925)
Introduce a dedicated Quick Open dialog, wire it to Cmd+P and the macOS app menu, and show file-type icons in quick-open results so file navigation matches the rest of the app.
This commit is contained in:
@@ -115,6 +115,8 @@ const MENU_ITEM_SETTINGS_ID: &str = "menu_settings";
|
|||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const MENU_ITEM_COMMAND_PALETTE_ID: &str = "menu_command_palette";
|
const MENU_ITEM_COMMAND_PALETTE_ID: &str = "menu_command_palette";
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
|
const MENU_ITEM_QUICK_OPEN_ID: &str = "menu_quick_open";
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
const MENU_ITEM_NEW_SESSION_ID: &str = "menu_new_session";
|
const MENU_ITEM_NEW_SESSION_ID: &str = "menu_new_session";
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
const MENU_ITEM_WORKTREE_CREATOR_ID: &str = "menu_worktree_creator";
|
const MENU_ITEM_WORKTREE_CREATOR_ID: &str = "menu_worktree_creator";
|
||||||
@@ -395,6 +397,14 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
Some("Cmd+K"),
|
Some("Cmd+K"),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
let quick_open = MenuItem::with_id(
|
||||||
|
app,
|
||||||
|
MENU_ITEM_QUICK_OPEN_ID,
|
||||||
|
"Quick Open…",
|
||||||
|
true,
|
||||||
|
Some("Cmd+P"),
|
||||||
|
)?;
|
||||||
|
|
||||||
let new_window = MenuItem::with_id(
|
let new_window = MenuItem::with_id(
|
||||||
app,
|
app,
|
||||||
MENU_ITEM_NEW_WINDOW_ID,
|
MENU_ITEM_NEW_WINDOW_ID,
|
||||||
@@ -582,6 +592,7 @@ fn build_macos_menu<R: tauri::Runtime>(
|
|||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
&settings,
|
&settings,
|
||||||
&command_palette,
|
&command_palette,
|
||||||
|
&quick_open,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
&PredefinedMenuItem::services(app, None)?,
|
&PredefinedMenuItem::services(app, None)?,
|
||||||
&PredefinedMenuItem::separator(app)?,
|
&PredefinedMenuItem::separator(app)?,
|
||||||
@@ -3825,6 +3836,10 @@ fn main() {
|
|||||||
dispatch_menu_action(app, "command-palette");
|
dispatch_menu_action(app, "command-palette");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if id == MENU_ITEM_QUICK_OPEN_ID {
|
||||||
|
dispatch_menu_action(app, "quick-open");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if id == MENU_ITEM_NEW_SESSION_ID {
|
if id == MENU_ITEM_NEW_SESSION_ID {
|
||||||
dispatch_menu_action(app, "new-session");
|
dispatch_menu_action(app, "new-session");
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
|||||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||||
import type { RuntimeAPIs } from '@/lib/api/types';
|
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
|
import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog';
|
||||||
|
|
||||||
const AboutDialogWrapper: React.FC = () => {
|
const AboutDialogWrapper: React.FC = () => {
|
||||||
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
const isAboutDialogOpen = useUIStore((s) => s.isAboutDialogOpen);
|
||||||
@@ -704,6 +705,7 @@ function App({ apis }: AppProps) {
|
|||||||
<MainLayout />
|
<MainLayout />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ConfigUpdateOverlay />
|
<ConfigUpdateOverlay />
|
||||||
|
<QuickOpenDialog />
|
||||||
<AboutDialogWrapper />
|
<AboutDialogWrapper />
|
||||||
{showMemoryDebug && (
|
{showMemoryDebug && (
|
||||||
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
|
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useSessionUIStore } from '@/sync/session-ui-store';
|
|||||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||||
import { useDeviceInfo } from '@/lib/device';
|
import { useDeviceInfo } from '@/lib/device';
|
||||||
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiGitBranchLine, RiLayoutLeftLine, RiLayoutRightLine, RiMoonLine, RiQuestionLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
import { RiAddLine, RiChatAi3Line, RiCheckLine, RiCodeLine, RiComputerLine, RiFileLine, RiGitBranchLine, RiLayoutLeftLine, RiLayoutRightLine, RiMoonLine, RiQuestionLine, RiSettings3Line, RiSunLine, RiTerminalBoxLine } from '@remixicon/react';
|
||||||
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
||||||
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||||
@@ -24,6 +24,7 @@ export const CommandPalette: React.FC = () => {
|
|||||||
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
|
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
|
||||||
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen);
|
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen);
|
||||||
const setHelpDialogOpen = useUIStore((s) => s.setHelpDialogOpen);
|
const setHelpDialogOpen = useUIStore((s) => s.setHelpDialogOpen);
|
||||||
|
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||||
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
|
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
|
||||||
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
|
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
|
||||||
const setSettingsPage = useUIStore((s) => s.setSettingsPage);
|
const setSettingsPage = useUIStore((s) => s.setSettingsPage);
|
||||||
@@ -70,6 +71,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
handleClose();
|
handleClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOpenQuickOpen = () => {
|
||||||
|
setQuickOpenOpen(true);
|
||||||
|
handleClose();
|
||||||
|
};
|
||||||
|
|
||||||
const handleCreateWorktreeSession = () => {
|
const handleCreateWorktreeSession = () => {
|
||||||
handleClose();
|
handleClose();
|
||||||
createWorktreeSession();
|
createWorktreeSession();
|
||||||
@@ -183,6 +189,11 @@ export const CommandPalette: React.FC = () => {
|
|||||||
<span>Open Session List</span>
|
<span>Open Session List</span>
|
||||||
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
|
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
|
<CommandItem onSelect={handleOpenQuickOpen}>
|
||||||
|
<RiFileLine className="mr-2 h-4 w-4" />
|
||||||
|
<span>Quick Open</span>
|
||||||
|
<CommandShortcut>{shortcut('open_quick_open')}</CommandShortcut>
|
||||||
|
</CommandItem>
|
||||||
<CommandItem onSelect={handleCreateSession}>
|
<CommandItem onSelect={handleCreateSession}>
|
||||||
<RiAddLine className="mr-2 h-4 w-4" />
|
<RiAddLine className="mr-2 h-4 w-4" />
|
||||||
<span>New Session</span>
|
<span>New Session</span>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
RiBrainAi3Line,
|
RiBrainAi3Line,
|
||||||
RiCloseCircleLine,
|
RiCloseCircleLine,
|
||||||
RiCommandLine,
|
RiCommandLine,
|
||||||
|
RiFileLine,
|
||||||
RiGitBranchLine,
|
RiGitBranchLine,
|
||||||
RiLayoutLeftLine,
|
RiLayoutLeftLine,
|
||||||
RiLayoutRightLine,
|
RiLayoutRightLine,
|
||||||
@@ -67,6 +68,12 @@ export const HelpDialog: React.FC = () => {
|
|||||||
icon: RiCommandLine,
|
icon: RiCommandLine,
|
||||||
keys: '',
|
keys: '',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'open_quick_open',
|
||||||
|
description: 'Quick open file',
|
||||||
|
icon: RiFileLine,
|
||||||
|
keys: '',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'open_help',
|
id: 'open_help',
|
||||||
description: "Show Keyboard Shortcuts (this dialog)",
|
description: "Show Keyboard Shortcuts (this dialog)",
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { toast } from '@/components/ui';
|
||||||
|
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||||
|
import {
|
||||||
|
Command,
|
||||||
|
CommandEmpty,
|
||||||
|
CommandGroup,
|
||||||
|
CommandInput,
|
||||||
|
CommandItem,
|
||||||
|
CommandList,
|
||||||
|
} from '@/components/ui/command';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog';
|
||||||
|
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||||
|
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||||
|
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||||
|
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
|
||||||
|
import { useDirectoryShowHidden } from '@/lib/directoryShowHidden';
|
||||||
|
import { useFilesViewShowGitignored } from '@/lib/filesViewShowGitignored';
|
||||||
|
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||||
|
import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore';
|
||||||
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
|
||||||
|
type RecentQuickOpenFile = {
|
||||||
|
path: string;
|
||||||
|
name: string;
|
||||||
|
relativePath: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizePath = (value: string): string => {
|
||||||
|
if (!value) return '';
|
||||||
|
|
||||||
|
const raw = value.replace(/\\/g, '/');
|
||||||
|
const hadUncPrefix = raw.startsWith('//');
|
||||||
|
|
||||||
|
let normalized = raw.replace(/\/+/g, '/');
|
||||||
|
if (hadUncPrefix && !normalized.startsWith('//')) {
|
||||||
|
normalized = `/${normalized}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUnixRoot = normalized === '/';
|
||||||
|
const isWindowsDriveRoot = /^[A-Za-z]:\/$/.test(normalized);
|
||||||
|
if (!isUnixRoot && !isWindowsDriveRoot) {
|
||||||
|
normalized = normalized.replace(/\/+$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelativePath = (root: string, filePath: string): string => {
|
||||||
|
const normalizedRoot = normalizePath(root);
|
||||||
|
const normalizedPath = normalizePath(filePath);
|
||||||
|
|
||||||
|
if (!normalizedRoot || !normalizedPath) {
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPath === normalizedRoot) {
|
||||||
|
return normalizedPath.split('/').filter(Boolean).pop() || normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedPath.startsWith(`${normalizedRoot}/`)) {
|
||||||
|
return normalizedPath.slice(normalizedRoot.length + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedPath;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const QuickOpenDialog: React.FC = () => {
|
||||||
|
const { files } = useRuntimeAPIs();
|
||||||
|
const isQuickOpenOpen = useUIStore((state) => state.isQuickOpenOpen);
|
||||||
|
const setQuickOpenOpen = useUIStore((state) => state.setQuickOpenOpen);
|
||||||
|
const openContextFile = useUIStore((state) => state.openContextFile);
|
||||||
|
const effectiveDirectory = useEffectiveDirectory();
|
||||||
|
const showHidden = useDirectoryShowHidden();
|
||||||
|
const showGitignored = useFilesViewShowGitignored();
|
||||||
|
const searchFiles = useFileSearchStore((state) => state.searchFiles);
|
||||||
|
const currentRoot = React.useMemo(
|
||||||
|
() => (effectiveDirectory ? normalizePath(effectiveDirectory) : undefined),
|
||||||
|
[effectiveDirectory],
|
||||||
|
);
|
||||||
|
const [query, setQuery] = React.useState('');
|
||||||
|
const debouncedQuery = useDebouncedValue(query, 200);
|
||||||
|
const [searchResults, setSearchResults] = React.useState<RecentQuickOpenFile[]>([]);
|
||||||
|
const [isSearching, setIsSearching] = React.useState(false);
|
||||||
|
const rootTabs = useFilesViewTabsStore(
|
||||||
|
React.useCallback(
|
||||||
|
(state) => (currentRoot ? state.byRoot[currentRoot] : undefined),
|
||||||
|
[currentRoot],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentFiles = React.useMemo(() => {
|
||||||
|
if (!currentRoot || !rootTabs) {
|
||||||
|
return [] as RecentQuickOpenFile[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedPaths = [
|
||||||
|
rootTabs.selectedPath,
|
||||||
|
...rootTabs.openPaths,
|
||||||
|
].filter((value): value is string => typeof value === 'string' && value.length > 0);
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
|
||||||
|
return orderedPaths
|
||||||
|
.map((filePath) => normalizePath(filePath))
|
||||||
|
.filter((filePath) => {
|
||||||
|
if (!filePath || seen.has(filePath)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
seen.add(filePath);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.slice(0, 10)
|
||||||
|
.map((filePath) => {
|
||||||
|
const name = filePath.split('/').filter(Boolean).pop() || filePath;
|
||||||
|
return {
|
||||||
|
path: filePath,
|
||||||
|
name,
|
||||||
|
relativePath: getRelativePath(currentRoot, filePath),
|
||||||
|
} satisfies RecentQuickOpenFile;
|
||||||
|
});
|
||||||
|
}, [currentRoot, rootTabs]);
|
||||||
|
|
||||||
|
const trimmedQuery = debouncedQuery.trim();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!isQuickOpenOpen) {
|
||||||
|
setQuery('');
|
||||||
|
setSearchResults([]);
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
}, [isQuickOpenOpen]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!currentRoot || trimmedQuery.length === 0) {
|
||||||
|
setSearchResults([]);
|
||||||
|
setIsSearching(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
setIsSearching(true);
|
||||||
|
|
||||||
|
void searchFiles(currentRoot, trimmedQuery, 150, {
|
||||||
|
includeHidden: showHidden,
|
||||||
|
respectGitignore: !showGitignored,
|
||||||
|
type: 'file',
|
||||||
|
})
|
||||||
|
.then((results) => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchResults(results.map((file) => ({
|
||||||
|
path: normalizePath(file.path),
|
||||||
|
name: file.name,
|
||||||
|
relativePath: file.relativePath,
|
||||||
|
})));
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
if (cancelled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSearchResults([]);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
if (!cancelled) {
|
||||||
|
setIsSearching(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [currentRoot, searchFiles, showGitignored, showHidden, trimmedQuery]);
|
||||||
|
|
||||||
|
const handleSelectFile = React.useCallback(async (filePath: string) => {
|
||||||
|
if (!currentRoot) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const openValidation = await validateContextFileOpen(files, filePath);
|
||||||
|
if (!openValidation.ok) {
|
||||||
|
toast.error(getContextFileOpenFailureMessage(openValidation.reason));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
openContextFile(currentRoot, filePath);
|
||||||
|
setQuickOpenOpen(false);
|
||||||
|
}, [currentRoot, files, openContextFile, setQuickOpenOpen]);
|
||||||
|
|
||||||
|
const hasTypedQuery = query.trim().length > 0;
|
||||||
|
const visibleFiles = hasTypedQuery ? searchResults : recentFiles;
|
||||||
|
const emptyMessage = !currentRoot
|
||||||
|
? 'Open a project to quick-open files'
|
||||||
|
: hasTypedQuery
|
||||||
|
? (isSearching ? 'Searching files…' : 'No matching files')
|
||||||
|
: 'No matching recent files';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isQuickOpenOpen} onOpenChange={setQuickOpenOpen}>
|
||||||
|
<DialogHeader className="sr-only">
|
||||||
|
<DialogTitle>Quick Open</DialogTitle>
|
||||||
|
<DialogDescription>Type a file name or path</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogContent
|
||||||
|
className="overflow-hidden p-0 transform-gpu will-change-transform"
|
||||||
|
showCloseButton
|
||||||
|
>
|
||||||
|
<Command
|
||||||
|
shouldFilter={false}
|
||||||
|
className="[&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-4 [&_[cmdk-input-wrapper]_svg]:w-4 [&_[cmdk-input]]:h-8 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-1.5 [&_[cmdk-item]_svg]:h-4 [&_[cmdk-item]_svg]:w-4 [&_[cmdk-item]]:typography-meta"
|
||||||
|
>
|
||||||
|
<CommandInput
|
||||||
|
value={query}
|
||||||
|
onValueChange={setQuery}
|
||||||
|
placeholder="Type a file name or path"
|
||||||
|
disabled={!currentRoot}
|
||||||
|
/>
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||||
|
|
||||||
|
{currentRoot && visibleFiles.length > 0 && (
|
||||||
|
<CommandGroup heading={hasTypedQuery ? 'Files' : 'Recent Files'}>
|
||||||
|
{visibleFiles.map((file) => (
|
||||||
|
<CommandItem
|
||||||
|
key={file.path}
|
||||||
|
value={file.path}
|
||||||
|
onSelect={() => {
|
||||||
|
void handleSelectFile(file.path);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FileTypeIcon filePath={file.path} className="size-4 shrink-0" />
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
|
||||||
|
<span className="truncate">{file.name}</span>
|
||||||
|
<span className="truncate text-muted-foreground">{file.relativePath}</span>
|
||||||
|
</div>
|
||||||
|
</CommandItem>
|
||||||
|
))}
|
||||||
|
</CommandGroup>
|
||||||
|
)}
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -18,6 +18,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||||
const abortCurrentOperation = sessionActions.abortCurrentOperation;;
|
const abortCurrentOperation = sessionActions.abortCurrentOperation;;
|
||||||
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
||||||
|
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||||
const toggleRightSidebar = useUIStore((s) => s.toggleRightSidebar);
|
const toggleRightSidebar = useUIStore((s) => s.toggleRightSidebar);
|
||||||
@@ -61,6 +62,12 @@ export const useKeyboardShortcuts = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventMatchesShortcut(e, combo('open_quick_open'))) {
|
||||||
|
e.preventDefault();
|
||||||
|
setQuickOpenOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (eventMatchesShortcut(e, combo('open_status'))) {
|
if (eventMatchesShortcut(e, combo('open_status'))) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
void showOpenCodeStatus();
|
void showOpenCodeStatus();
|
||||||
@@ -414,6 +421,7 @@ export const useKeyboardShortcuts = () => {
|
|||||||
openNewSessionDraft,
|
openNewSessionDraft,
|
||||||
abortCurrentOperation,
|
abortCurrentOperation,
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
|
setQuickOpenOpen,
|
||||||
toggleHelpDialog,
|
toggleHelpDialog,
|
||||||
toggleSidebar,
|
toggleSidebar,
|
||||||
toggleRightSidebar,
|
toggleRightSidebar,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ type MenuAction =
|
|||||||
| 'about'
|
| 'about'
|
||||||
| 'settings'
|
| 'settings'
|
||||||
| 'command-palette'
|
| 'command-palette'
|
||||||
|
| 'quick-open'
|
||||||
| 'new-session'
|
| 'new-session'
|
||||||
| 'new-worktree-session'
|
| 'new-worktree-session'
|
||||||
| 'change-workspace'
|
| 'change-workspace'
|
||||||
@@ -50,6 +51,7 @@ export const useMenuActions = (
|
|||||||
) => {
|
) => {
|
||||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||||
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
||||||
|
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||||
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
||||||
@@ -139,6 +141,10 @@ export const useMenuActions = (
|
|||||||
toggleCommandPalette();
|
toggleCommandPalette();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'quick-open':
|
||||||
|
setQuickOpenOpen(true);
|
||||||
|
break;
|
||||||
|
|
||||||
case 'new-session':
|
case 'new-session':
|
||||||
setActiveMainTab('chat');
|
setActiveMainTab('chat');
|
||||||
setSessionSwitcherOpen(false);
|
setSessionSwitcherOpen(false);
|
||||||
@@ -227,6 +233,7 @@ export const useMenuActions = (
|
|||||||
setAboutDialogOpen,
|
setAboutDialogOpen,
|
||||||
setActiveMainTab,
|
setActiveMainTab,
|
||||||
setSessionSwitcherOpen,
|
setSessionSwitcherOpen,
|
||||||
|
setQuickOpenOpen,
|
||||||
setSettingsDialogOpen,
|
setSettingsDialogOpen,
|
||||||
setThemeMode,
|
setThemeMode,
|
||||||
toggleCommandPalette,
|
toggleCommandPalette,
|
||||||
|
|||||||
@@ -104,6 +104,13 @@ export function keyToShortcutToken(key: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
|
const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
|
||||||
|
{
|
||||||
|
id: 'open_quick_open',
|
||||||
|
defaultCombo: 'mod+p',
|
||||||
|
label: 'Open quick open',
|
||||||
|
description: 'Open the quick open dialog',
|
||||||
|
customizable: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'open_command_palette',
|
id: 'open_command_palette',
|
||||||
defaultCombo: 'mod+k',
|
defaultCombo: 'mod+k',
|
||||||
|
|||||||
@@ -483,6 +483,7 @@ interface UIStore {
|
|||||||
pendingFileFocusPath: string | null;
|
pendingFileFocusPath: string | null;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isKeyboardOpen: boolean;
|
isKeyboardOpen: boolean;
|
||||||
|
isQuickOpenOpen: boolean;
|
||||||
isCommandPaletteOpen: boolean;
|
isCommandPaletteOpen: boolean;
|
||||||
isHelpDialogOpen: boolean;
|
isHelpDialogOpen: boolean;
|
||||||
isAboutDialogOpen: boolean;
|
isAboutDialogOpen: boolean;
|
||||||
@@ -603,6 +604,8 @@ interface UIStore {
|
|||||||
navigateToDiff: (filePath: string) => void;
|
navigateToDiff: (filePath: string) => void;
|
||||||
consumePendingDiffFile: () => string | null;
|
consumePendingDiffFile: () => string | null;
|
||||||
setIsMobile: (isMobile: boolean) => void;
|
setIsMobile: (isMobile: boolean) => void;
|
||||||
|
setQuickOpenOpen: (open: boolean) => void;
|
||||||
|
toggleQuickOpen: () => void;
|
||||||
toggleCommandPalette: () => void;
|
toggleCommandPalette: () => void;
|
||||||
setCommandPaletteOpen: (open: boolean) => void;
|
setCommandPaletteOpen: (open: boolean) => void;
|
||||||
toggleHelpDialog: () => void;
|
toggleHelpDialog: () => void;
|
||||||
@@ -722,6 +725,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
pendingFileFocusPath: null,
|
pendingFileFocusPath: null,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
isKeyboardOpen: false,
|
isKeyboardOpen: false,
|
||||||
|
isQuickOpenOpen: false,
|
||||||
isCommandPaletteOpen: false,
|
isCommandPaletteOpen: false,
|
||||||
isHelpDialogOpen: false,
|
isHelpDialogOpen: false,
|
||||||
isAboutDialogOpen: false,
|
isAboutDialogOpen: false,
|
||||||
@@ -1235,6 +1239,14 @@ export const useUIStore = create<UIStore>()(
|
|||||||
set({ isMobile });
|
set({ isMobile });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setQuickOpenOpen: (open) => {
|
||||||
|
set({ isQuickOpenOpen: open });
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleQuickOpen: () => {
|
||||||
|
set((state) => ({ isQuickOpenOpen: !state.isQuickOpenOpen }));
|
||||||
|
},
|
||||||
|
|
||||||
toggleCommandPalette: () => {
|
toggleCommandPalette: () => {
|
||||||
set((state) => ({ isCommandPaletteOpen: !state.isCommandPaletteOpen }));
|
set((state) => ({ isCommandPaletteOpen: !state.isCommandPaletteOpen }));
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user