feat(palette): unify quick open into command palette with multi-source search
Merge file picker into command palette. Single Cmd+P entry searches files, sessions, settings pages and commands; groups re-order by best fuzzy score per source. Sessions show branch labels; git status is lazily fetched for all session directories on open. Drop QuickOpenDialog and Cmd+K shortcut.
This commit is contained in:
@@ -54,7 +54,6 @@ import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore';
|
||||
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
|
||||
import type { RuntimeAPIs } from '@/lib/api/types';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
import { QuickOpenDialog } from '@/components/ui/QuickOpenDialog';
|
||||
import { McpOAuthCallbackPage } from '@/components/sections/mcp/McpOAuthCallbackPage';
|
||||
import { MCP_OAUTH_CALLBACK_PATH } from '@/components/sections/mcp/mcpOAuth';
|
||||
import { lazyWithChunkRecovery } from '@/lib/chunkLoadRecovery';
|
||||
@@ -925,7 +924,6 @@ function App({ apis }: AppProps) {
|
||||
{!isBootShell && (
|
||||
<>
|
||||
<ConfigUpdateOverlay />
|
||||
<QuickOpenDialog />
|
||||
<AboutDialogWrapper />
|
||||
{showMemoryDebug && (
|
||||
<MemoryDebugPanel onClose={() => setShowMemoryDebug(false)} />
|
||||
|
||||
@@ -1,315 +1,530 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
CommandDialog,
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from '@/components/ui/command';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useUIStore } from '@/stores/useUIStore';
|
||||
import { useSessionUIStore } from '@/sync/session-ui-store';
|
||||
import { useGlobalSessionsStore, resolveGlobalSessionDirectory } from '@/stores/useGlobalSessionsStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useThemeSystem } from '@/contexts/useThemeSystem';
|
||||
import { useGitAllBranches, useGitStore } from '@/stores/useGitStore';
|
||||
import { useFileSearchStore } from '@/stores/useFileSearchStore';
|
||||
import { useDeviceInfo } from '@/lib/device';
|
||||
import { useDebouncedValue } from '@/hooks/useDebouncedValue';
|
||||
import { useEffectiveDirectory } from '@/hooks/useEffectiveDirectory';
|
||||
import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs';
|
||||
import { getContextFileOpenFailureMessage, validateContextFileOpen } from '@/lib/contextFileOpenGuard';
|
||||
import { toast } from '@/components/ui';
|
||||
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
|
||||
import {
|
||||
RiAddLine,
|
||||
RiChatAi3Line,
|
||||
RiCheckLine,
|
||||
RiClipboardLine,
|
||||
RiComputerLine,
|
||||
RiFileLine,
|
||||
RiFolderLine,
|
||||
RiGitBranchLine,
|
||||
RiLayoutLeftLine,
|
||||
RiLayoutRightLine,
|
||||
RiMoonLine,
|
||||
RiPieChartLine,
|
||||
RiQuestionLine,
|
||||
RiSettings3Line,
|
||||
RiSunLine,
|
||||
RiTerminalBoxLine,
|
||||
} from '@remixicon/react';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { createWorktreeSession } from '@/lib/worktreeSessionCreator';
|
||||
import { formatShortcutForDisplay, getEffectiveShortcutCombo } from '@/lib/shortcuts';
|
||||
import { isDesktopShell, isVSCodeRuntime, isWebRuntime } from '@/lib/desktop';
|
||||
import { SETTINGS_PAGE_METADATA, SETTINGS_GROUP_LABELS, type SettingsRuntimeContext } from '@/lib/settings/metadata';
|
||||
import { SETTINGS_PAGE_METADATA, type SettingsRuntimeContext } from '@/lib/settings/metadata';
|
||||
import { getSettingsNavIcon } from '@/components/views/SettingsView';
|
||||
import { rankByFuzzyQuery, scoreByFuzzyQuery } from '@/lib/search/fuzzySearch';
|
||||
import { truncatePathMiddle } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type CommandEntry = {
|
||||
id: string;
|
||||
title: string;
|
||||
icon: React.ReactNode;
|
||||
shortcutId?: string;
|
||||
searchText: string;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
type FileHit = { 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;
|
||||
};
|
||||
|
||||
export const CommandPalette: React.FC = () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
const isCommandPaletteOpen = useUIStore((s) => s.isCommandPaletteOpen);
|
||||
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen);
|
||||
const setHelpDialogOpen = useUIStore((s) => s.setHelpDialogOpen);
|
||||
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||
const setActiveMainTab = useUIStore((s) => s.setActiveMainTab);
|
||||
const setSettingsDialogOpen = useUIStore((s) => s.setSettingsDialogOpen);
|
||||
const setSettingsPage = useUIStore((s) => s.setSettingsPage);
|
||||
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const toggleRightSidebar = useUIStore((s) => s.toggleRightSidebar);
|
||||
const setRightSidebarOpen = useUIStore((s) => s.setRightSidebarOpen);
|
||||
const setRightSidebarTab = useUIStore((s) => s.setRightSidebarTab);
|
||||
const toggleBottomTerminal = useUIStore((s) => s.toggleBottomTerminal);
|
||||
const setBottomTerminalExpanded = useUIStore((s) => s.setBottomTerminalExpanded);
|
||||
const isBottomTerminalExpanded = useUIStore((s) => s.isBottomTerminalExpanded);
|
||||
const openContextOverview = useUIStore((s) => s.openContextOverview);
|
||||
const openContextPlan = useUIStore((s) => s.openContextPlan);
|
||||
const openContextFile = useUIStore((s) => s.openContextFile);
|
||||
const shortcutOverrides = useUIStore((s) => s.shortcutOverrides);
|
||||
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const setCurrentSession = useSessionUIStore((s) => s.setCurrentSession);
|
||||
const getSessionsByDirectory = useSessionUIStore((s) => s.getSessionsByDirectory);
|
||||
|
||||
const activeSessions = useGlobalSessionsStore((s) => s.activeSessions);
|
||||
const currentDirectory = useDirectoryStore((s) => s.currentDirectory);
|
||||
const { themeMode, setThemeMode } = useThemeSystem();
|
||||
const effectiveDirectory = useEffectiveDirectory();
|
||||
const searchFiles = useFileSearchStore((s) => s.searchFiles);
|
||||
const { files: filesApi, git: gitApi } = useRuntimeAPIs();
|
||||
const ensureGitStatus = useGitStore((s) => s.ensureStatus);
|
||||
const { isMobile } = useDeviceInfo();
|
||||
|
||||
const currentRoot = React.useMemo(
|
||||
() => (effectiveDirectory ? normalizePath(effectiveDirectory) : null),
|
||||
[effectiveDirectory],
|
||||
);
|
||||
|
||||
const [query, setQuery] = React.useState('');
|
||||
const debouncedQuery = useDebouncedValue(query, 200);
|
||||
const trimmedQuery = debouncedQuery.trim();
|
||||
const liveTrimmed = query.trim();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isCommandPaletteOpen) setQuery('');
|
||||
}, [isCommandPaletteOpen]);
|
||||
|
||||
// Lazy-load git status for every session directory we plan to display so that
|
||||
// branch labels become available across all projects, not only the active one.
|
||||
// Deferred to idle to keep the first render (and the file-search effect) free
|
||||
// from a flood of git store updates.
|
||||
React.useEffect(() => {
|
||||
if (!isCommandPaletteOpen || !gitApi) return;
|
||||
const handle = setTimeout(() => {
|
||||
const seen = new Set<string>();
|
||||
for (const session of activeSessions) {
|
||||
const dir = resolveGlobalSessionDirectory(session);
|
||||
if (!dir || seen.has(dir)) continue;
|
||||
seen.add(dir);
|
||||
void ensureGitStatus(dir, gitApi);
|
||||
}
|
||||
}, 0);
|
||||
return () => clearTimeout(handle);
|
||||
}, [isCommandPaletteOpen, activeSessions, gitApi, ensureGitStatus]);
|
||||
|
||||
const close = React.useCallback(() => setCommandPaletteOpen(false), [setCommandPaletteOpen]);
|
||||
const run = React.useCallback(
|
||||
(fn: () => void | Promise<void>) => () => {
|
||||
close();
|
||||
void fn();
|
||||
},
|
||||
[close],
|
||||
);
|
||||
|
||||
const run = React.useCallback((fn: () => void | Promise<void>) => async () => {
|
||||
close();
|
||||
await fn();
|
||||
}, [close]);
|
||||
|
||||
const handleCreateSession = run(() => {
|
||||
setActiveMainTab('chat');
|
||||
setSessionSwitcherOpen(false);
|
||||
openNewSessionDraft();
|
||||
});
|
||||
|
||||
const handleOpenQuickOpen = run(() => setQuickOpenOpen(true));
|
||||
|
||||
const handleCreateWorktreeSession = run(() => {
|
||||
createWorktreeSession();
|
||||
});
|
||||
|
||||
const handleOpenSessionList = run(() => {
|
||||
if (isMobile) {
|
||||
const { isSessionSwitcherOpen } = useUIStore.getState();
|
||||
setSessionSwitcherOpen(!isSessionSwitcherOpen);
|
||||
} else {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
const handleToggleRightSidebar = run(() => toggleRightSidebar());
|
||||
const handleOpenRightSidebarGit = run(() => { setRightSidebarOpen(true); setRightSidebarTab('git'); });
|
||||
const handleOpenRightSidebarFiles = run(() => { setRightSidebarOpen(true); setRightSidebarTab('files'); });
|
||||
const handleToggleTerminalDock = run(() => toggleBottomTerminal());
|
||||
const handleToggleTerminalExpanded = run(() => setBottomTerminalExpanded(!isBottomTerminalExpanded));
|
||||
const handleShowHelp = run(() => setHelpDialogOpen(true));
|
||||
const handleOpenSettings = run(() => setSettingsDialogOpen(true));
|
||||
const handleShowContextUsage = run(() => {
|
||||
if (currentDirectory) openContextOverview(currentDirectory);
|
||||
});
|
||||
const handleShowPlan = run(() => {
|
||||
if (currentDirectory) openContextPlan(currentDirectory);
|
||||
});
|
||||
|
||||
const handleOpenSettingsPage = (slug: string) => run(() => {
|
||||
setSettingsPage(slug);
|
||||
setSettingsDialogOpen(true);
|
||||
});
|
||||
|
||||
const handleOpenSession = (sessionId: string, directoryHint?: string | null) => run(() => {
|
||||
setCurrentSession(sessionId, directoryHint ?? null);
|
||||
});
|
||||
|
||||
const handleSetThemeMode = (mode: 'light' | 'dark' | 'system') => run(() => {
|
||||
setThemeMode(mode);
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Commands
|
||||
// ---------------------------------------------------------------------------
|
||||
const commands = React.useMemo<CommandEntry[]>(() => {
|
||||
const list: CommandEntry[] = [
|
||||
{
|
||||
id: 'new-session',
|
||||
title: t('commandPalette.item.newSession'),
|
||||
icon: <RiAddLine className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'new_chat',
|
||||
searchText: t('commandPalette.item.newSession'),
|
||||
onSelect: run(() => {
|
||||
setActiveMainTab('chat');
|
||||
setSessionSwitcherOpen(false);
|
||||
openNewSessionDraft();
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'new-worktree',
|
||||
title: t('commandPalette.item.newWorktreeDraft'),
|
||||
icon: <RiGitBranchLine className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'new_chat_worktree',
|
||||
searchText: t('commandPalette.item.newWorktreeDraft'),
|
||||
onSelect: run(() => {
|
||||
void createWorktreeSession();
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'toggle-sidebar',
|
||||
title: isMobile
|
||||
? t('commandPalette.item.showSessionSwitcher')
|
||||
: t('commandPalette.item.toggleSidebar'),
|
||||
icon: <RiLayoutLeftLine className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'toggle_sidebar',
|
||||
searchText: isMobile
|
||||
? t('commandPalette.item.showSessionSwitcher')
|
||||
: t('commandPalette.item.toggleSidebar'),
|
||||
onSelect: run(() => {
|
||||
if (isMobile) {
|
||||
const { isSessionSwitcherOpen } = useUIStore.getState();
|
||||
setSessionSwitcherOpen(!isSessionSwitcherOpen);
|
||||
} else {
|
||||
toggleSidebar();
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'toggle-right-sidebar',
|
||||
title: t('commandPalette.item.toggleRightSidebar'),
|
||||
icon: <RiLayoutRightLine className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'toggle_right_sidebar',
|
||||
searchText: t('commandPalette.item.toggleRightSidebar'),
|
||||
onSelect: run(() => toggleRightSidebar()),
|
||||
},
|
||||
{
|
||||
id: 'toggle-terminal',
|
||||
title: t('commandPalette.item.toggleTerminal'),
|
||||
icon: <RiTerminalBoxLine className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'toggle_terminal',
|
||||
searchText: t('commandPalette.item.toggleTerminal'),
|
||||
onSelect: run(() => toggleBottomTerminal()),
|
||||
},
|
||||
{
|
||||
id: 'context-usage',
|
||||
title: t('commandPalette.item.showContextUsage'),
|
||||
icon: <RiPieChartLine className="mr-2 h-4 w-4" />,
|
||||
searchText: t('commandPalette.item.showContextUsage'),
|
||||
onSelect: run(() => {
|
||||
if (currentDirectory) openContextOverview(currentDirectory);
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'open-settings',
|
||||
title: t('commandPalette.item.openSettings'),
|
||||
icon: <RiSettings3Line className="mr-2 h-4 w-4" />,
|
||||
shortcutId: 'open_settings',
|
||||
searchText: t('commandPalette.item.openSettings'),
|
||||
onSelect: run(() => setSettingsDialogOpen(true)),
|
||||
},
|
||||
];
|
||||
return list;
|
||||
}, [
|
||||
t,
|
||||
run,
|
||||
isMobile,
|
||||
setActiveMainTab,
|
||||
setSessionSwitcherOpen,
|
||||
openNewSessionDraft,
|
||||
toggleSidebar,
|
||||
toggleRightSidebar,
|
||||
toggleBottomTerminal,
|
||||
currentDirectory,
|
||||
openContextOverview,
|
||||
setSettingsDialogOpen,
|
||||
]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Settings sub-pages (only show when there's a query)
|
||||
// ---------------------------------------------------------------------------
|
||||
const settingsRuntimeCtx = React.useMemo<SettingsRuntimeContext>(() => {
|
||||
const isDesktop = isDesktopShell();
|
||||
return { isVSCode: isVSCodeRuntime(), isWeb: !isDesktop && isWebRuntime(), isDesktop };
|
||||
}, []);
|
||||
|
||||
const settingsItems = React.useMemo(() => {
|
||||
const groupLabel = (g: string) => (SETTINGS_GROUP_LABELS as Record<string, string>)[g] ?? g;
|
||||
const settingsEntries = React.useMemo<CommandEntry[]>(() => {
|
||||
return SETTINGS_PAGE_METADATA
|
||||
.filter((p) => p.slug !== 'home')
|
||||
.filter((p) => (p.isAvailable ? p.isAvailable(settingsRuntimeCtx) : true))
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
const g = groupLabel(a.group).localeCompare(groupLabel(b.group));
|
||||
if (g !== 0) return g;
|
||||
return a.title.localeCompare(b.title);
|
||||
.map((page) => {
|
||||
const Icon = getSettingsNavIcon(page.slug) ?? RiSettings3Line;
|
||||
const keywords = (page.keywords ?? []).join(' ');
|
||||
return {
|
||||
id: `settings:${page.slug}`,
|
||||
title: page.title,
|
||||
icon: <Icon className="mr-2 h-4 w-4" />,
|
||||
searchText: `${page.title} ${page.group} ${keywords}`,
|
||||
onSelect: run(() => {
|
||||
setSettingsPage(page.slug);
|
||||
setSettingsDialogOpen(true);
|
||||
}),
|
||||
} satisfies CommandEntry;
|
||||
});
|
||||
}, [settingsRuntimeCtx]);
|
||||
}, [settingsRuntimeCtx, run, setSettingsPage, setSettingsDialogOpen]);
|
||||
|
||||
const recentSessions = React.useMemo(() => {
|
||||
return getSessionsByDirectory(currentDirectory ?? '').slice(0, 5);
|
||||
}, [getSessionsByDirectory, currentDirectory]);
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sessions
|
||||
// ---------------------------------------------------------------------------
|
||||
const sortedActiveSessions = React.useMemo(() => {
|
||||
const getUpdated = (s: Session) =>
|
||||
(typeof s.time?.updated === 'number' ? s.time.updated : 0) ||
|
||||
(typeof s.time?.created === 'number' ? s.time.created : 0);
|
||||
return [...activeSessions].sort((a, b) => getUpdated(b) - getUpdated(a));
|
||||
}, [activeSessions]);
|
||||
|
||||
const shortcut = React.useCallback((actionId: string) => {
|
||||
return formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides));
|
||||
}, [shortcutOverrides]);
|
||||
const allBranches = useGitAllBranches();
|
||||
const worktreeMetadata = useSessionUIStore((s) => s.worktreeMetadata);
|
||||
|
||||
const settingsGroupLabelMap: Record<string, string> = {
|
||||
appearance: t('commandPalette.settingsGroup.appearance'),
|
||||
projects: t('commandPalette.settingsGroup.projects'),
|
||||
general: t('commandPalette.settingsGroup.general'),
|
||||
opencode: t('commandPalette.settingsGroup.opencode'),
|
||||
git: t('commandPalette.settingsGroup.git'),
|
||||
skills: t('commandPalette.settingsGroup.skills'),
|
||||
usage: t('commandPalette.settingsGroup.usage'),
|
||||
advanced: t('commandPalette.settingsGroup.advanced'),
|
||||
};
|
||||
const branchForSession = React.useCallback(
|
||||
(sessionId: string, dir: string | null): string | null => {
|
||||
const meta = worktreeMetadata.get(sessionId);
|
||||
if (meta?.branch) return meta.branch.trim() || null;
|
||||
if (dir) return allBranches.get(dir)?.trim() || null;
|
||||
return null;
|
||||
},
|
||||
[worktreeMetadata, allBranches],
|
||||
);
|
||||
|
||||
const settingsPageLabelMap: Record<string, string> = {
|
||||
home: t('commandPalette.settingsPage.home'),
|
||||
projects: t('commandPalette.settingsPage.projects'),
|
||||
'remote-instances': t('commandPalette.settingsPage.remoteInstances'),
|
||||
providers: t('commandPalette.settingsPage.providers'),
|
||||
usage: t('commandPalette.settingsPage.usage'),
|
||||
agents: t('commandPalette.settingsPage.agents'),
|
||||
commands: t('commandPalette.settingsPage.commands'),
|
||||
mcp: t('commandPalette.settingsPage.mcp'),
|
||||
'skills.installed': t('commandPalette.settingsPage.skillsInstalled'),
|
||||
'skills.catalog': t('commandPalette.settingsPage.skillsCatalog'),
|
||||
git: t('commandPalette.settingsPage.git'),
|
||||
appearance: t('commandPalette.settingsPage.appearance'),
|
||||
chat: t('commandPalette.settingsPage.chat'),
|
||||
shortcuts: t('commandPalette.settingsPage.shortcuts'),
|
||||
sessions: t('commandPalette.settingsPage.sessions'),
|
||||
'magic-prompts': t('commandPalette.settingsPage.magicPrompts'),
|
||||
notifications: t('commandPalette.settingsPage.notifications'),
|
||||
voice: t('commandPalette.settingsPage.voice'),
|
||||
tunnel: t('commandPalette.settingsPage.tunnel'),
|
||||
};
|
||||
// ---------------------------------------------------------------------------
|
||||
// File search
|
||||
// ---------------------------------------------------------------------------
|
||||
const [fileResults, setFileResults] = React.useState<FileHit[]>([]);
|
||||
const [isSearchingFiles, setIsSearchingFiles] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!isCommandPaletteOpen) {
|
||||
setFileResults([]);
|
||||
setIsSearchingFiles(false);
|
||||
return;
|
||||
}
|
||||
if (!currentRoot || trimmedQuery.length === 0) {
|
||||
setFileResults([]);
|
||||
setIsSearchingFiles(false);
|
||||
return;
|
||||
}
|
||||
let cancelled = false;
|
||||
setIsSearchingFiles(true);
|
||||
void searchFiles(currentRoot, trimmedQuery, 10, { type: 'file' })
|
||||
.then((results) => {
|
||||
if (cancelled) return;
|
||||
setFileResults(
|
||||
results.map((file) => ({
|
||||
path: normalizePath(file.path),
|
||||
name: file.name,
|
||||
relativePath: file.relativePath,
|
||||
})),
|
||||
);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setFileResults([]);
|
||||
})
|
||||
.finally(() => {
|
||||
if (!cancelled) setIsSearchingFiles(false);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [isCommandPaletteOpen, currentRoot, trimmedQuery, searchFiles]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Filter visible items
|
||||
// ---------------------------------------------------------------------------
|
||||
const hasQuery = liveTrimmed.length > 0;
|
||||
|
||||
const scoredCommands = React.useMemo(() => {
|
||||
if (!hasQuery) return commands.map((item) => ({ item, score: 0 }));
|
||||
return scoreByFuzzyQuery(commands, liveTrimmed, (c) => c.searchText, {
|
||||
limit: 7,
|
||||
noFuzzy: true,
|
||||
});
|
||||
}, [commands, liveTrimmed, hasQuery]);
|
||||
|
||||
const scoredSettings = React.useMemo(() => {
|
||||
if (!hasQuery) return [];
|
||||
return scoreByFuzzyQuery(settingsEntries, liveTrimmed, (c) => c.searchText, {
|
||||
limit: 7,
|
||||
noFuzzy: true,
|
||||
});
|
||||
}, [settingsEntries, liveTrimmed, hasQuery]);
|
||||
|
||||
const scoredSessions = React.useMemo(() => {
|
||||
if (!hasQuery) return sortedActiveSessions.slice(0, 5).map((item) => ({ item, score: 0 }));
|
||||
return scoreByFuzzyQuery(sortedActiveSessions, liveTrimmed, (s) => s.title || '', {
|
||||
limit: 7,
|
||||
threshold: 0.2,
|
||||
});
|
||||
}, [sortedActiveSessions, liveTrimmed, hasQuery]);
|
||||
|
||||
const scoredFiles = React.useMemo(() => {
|
||||
if (!hasQuery || fileResults.length === 0) return [];
|
||||
// Server already ranked by relevance; compute a comparable client score on
|
||||
// basename so we can decide file group placement vs sessions/commands.
|
||||
return scoreByFuzzyQuery(fileResults, liveTrimmed, (f) => f.name, {
|
||||
limit: 10,
|
||||
threshold: 0.4,
|
||||
});
|
||||
}, [fileResults, liveTrimmed, hasQuery]);
|
||||
|
||||
const visibleCommands = scoredCommands.map((x) => x.item);
|
||||
const visibleSettings = scoredSettings.map((x) => x.item);
|
||||
const visibleSessions = scoredSessions.map((x) => x.item);
|
||||
const visibleFiles = hasQuery ? scoredFiles.map((x) => x.item) : [];
|
||||
|
||||
const groupOrder = React.useMemo<('commands' | 'settings' | 'sessions' | 'files')[]>(() => {
|
||||
if (!hasQuery) return ['commands', 'sessions'];
|
||||
const best = (arr: { score: number }[]): number => (arr.length ? arr[0].score : Infinity);
|
||||
const groups: { key: 'commands' | 'settings' | 'sessions' | 'files'; score: number }[] = [
|
||||
{ key: 'commands', score: best(scoredCommands) },
|
||||
{ key: 'settings', score: best(scoredSettings) },
|
||||
{ key: 'sessions', score: best(scoredSessions) },
|
||||
{ key: 'files', score: best(scoredFiles) },
|
||||
];
|
||||
groups.sort((a, b) => a.score - b.score);
|
||||
return groups.map((g) => g.key);
|
||||
}, [hasQuery, scoredCommands, scoredSettings, scoredSessions, scoredFiles]);
|
||||
|
||||
const handleOpenSession = React.useCallback(
|
||||
(session: Session) => {
|
||||
close();
|
||||
setCurrentSession(session.id, resolveGlobalSessionDirectory(session));
|
||||
},
|
||||
[close, setCurrentSession],
|
||||
);
|
||||
|
||||
const handleOpenFile = React.useCallback(
|
||||
async (filePath: string) => {
|
||||
if (!currentRoot) return;
|
||||
const validation = await validateContextFileOpen(filesApi, filePath);
|
||||
if (!validation.ok) {
|
||||
toast.error(getContextFileOpenFailureMessage(validation.reason));
|
||||
return;
|
||||
}
|
||||
openContextFile(currentRoot, filePath);
|
||||
close();
|
||||
},
|
||||
[currentRoot, filesApi, openContextFile, close],
|
||||
);
|
||||
|
||||
const shortcut = React.useCallback(
|
||||
(actionId: string) =>
|
||||
formatShortcutForDisplay(getEffectiveShortcutCombo(actionId, shortcutOverrides)),
|
||||
[shortcutOverrides],
|
||||
);
|
||||
|
||||
return (
|
||||
<CommandDialog open={isCommandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
|
||||
<CommandInput placeholder={t('commandPalette.input.placeholder')} />
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('commandPalette.empty.noResults')}</CommandEmpty>
|
||||
<Dialog open={isCommandPaletteOpen} onOpenChange={setCommandPaletteOpen}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{t('commandPalette.title')}</DialogTitle>
|
||||
<DialogDescription>{t('commandPalette.description')}</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={t('commandPalette.input.placeholder')}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{t('commandPalette.empty.noResults')}</CommandEmpty>
|
||||
|
||||
<CommandGroup heading={t('commandPalette.section.sessions')}>
|
||||
<CommandItem onSelect={handleOpenQuickOpen}>
|
||||
<RiFileLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.quickOpen')}</span>
|
||||
<CommandShortcut>{shortcut('open_quick_open')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleCreateSession}>
|
||||
<RiAddLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.newSession')}</span>
|
||||
<CommandShortcut>{shortcut('new_chat')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleCreateWorktreeSession}>
|
||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.newWorktreeDraft')}</span>
|
||||
<CommandShortcut>{shortcut('new_chat_worktree')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenSessionList}>
|
||||
<RiLayoutLeftLine className="mr-2 h-4 w-4" />
|
||||
<span>{isMobile ? t('commandPalette.item.showSessionSwitcher') : t('commandPalette.item.toggleSidebar')}</span>
|
||||
<CommandShortcut>{shortcut('toggle_sidebar')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
{groupOrder.map((groupKey) => {
|
||||
if (groupKey === 'commands' && visibleCommands.length > 0) {
|
||||
return (
|
||||
<CommandGroup key="commands">
|
||||
{visibleCommands.map((cmd) => (
|
||||
<CommandItem key={cmd.id} value={cmd.id} onSelect={cmd.onSelect}>
|
||||
{cmd.icon}
|
||||
<span>{cmd.title}</span>
|
||||
{cmd.shortcutId ? (
|
||||
<CommandShortcut>{shortcut(cmd.shortcutId)}</CommandShortcut>
|
||||
) : null}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
}
|
||||
if (groupKey === 'settings' && visibleSettings.length > 0) {
|
||||
return (
|
||||
<CommandGroup key="settings">
|
||||
{visibleSettings.map((cmd) => (
|
||||
<CommandItem key={cmd.id} value={cmd.id} onSelect={cmd.onSelect}>
|
||||
{cmd.icon}
|
||||
<span>{cmd.title}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
);
|
||||
}
|
||||
if (groupKey === 'sessions' && visibleSessions.length > 0) {
|
||||
return (
|
||||
<CommandGroup key="sessions">
|
||||
{visibleSessions.map((session) => {
|
||||
const title = session.title || t('commandPalette.session.untitled');
|
||||
const dir = resolveGlobalSessionDirectory(session);
|
||||
const branch = branchForSession(session.id, dir);
|
||||
return (
|
||||
<CommandItem
|
||||
key={session.id}
|
||||
value={`session:${session.id}`}
|
||||
onSelect={() => handleOpenSession(session)}
|
||||
>
|
||||
<RiChatAi3Line className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{title}</span>
|
||||
{branch ? (
|
||||
<span className="ml-auto inline-flex items-center gap-1 text-muted-foreground typography-meta">
|
||||
<RiGitBranchLine className="h-3 w-3" />
|
||||
<span className="truncate max-w-[160px]">{branch}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
);
|
||||
}
|
||||
if (groupKey === 'files' && visibleFiles.length > 0) {
|
||||
return (
|
||||
<CommandGroup key="files">
|
||||
{visibleFiles.map((file) => {
|
||||
const display = truncatePathMiddle(file.relativePath || file.name, {
|
||||
maxLength: 80,
|
||||
});
|
||||
return (
|
||||
<CommandItem
|
||||
key={`file:${file.path}`}
|
||||
value={`file:${file.path}`}
|
||||
onSelect={() => {
|
||||
void handleOpenFile(file.path);
|
||||
}}
|
||||
>
|
||||
<FileTypeIcon filePath={file.path} className="mr-2 size-4 shrink-0" />
|
||||
<span className="truncate" aria-label={file.relativePath}>
|
||||
{display}
|
||||
</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading={t('commandPalette.section.view')}>
|
||||
<CommandItem onSelect={handleToggleRightSidebar}>
|
||||
<RiLayoutRightLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.toggleRightSidebar')}</span>
|
||||
<CommandShortcut>{shortcut('toggle_right_sidebar')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenRightSidebarGit}>
|
||||
<RiGitBranchLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.showGitRightSidebar')}</span>
|
||||
<CommandShortcut>{shortcut('open_right_sidebar_git')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleOpenRightSidebarFiles}>
|
||||
<RiFolderLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.showFilesRightSidebar')}</span>
|
||||
<CommandShortcut>{shortcut('open_right_sidebar_files')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleShowContextUsage}>
|
||||
<RiPieChartLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.showContextUsage')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleShowPlan}>
|
||||
<RiClipboardLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.showPlan')}</span>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleToggleTerminalDock}>
|
||||
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.toggleTerminal')}</span>
|
||||
<CommandShortcut>{shortcut('toggle_terminal')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleToggleTerminalExpanded}>
|
||||
<RiTerminalBoxLine className="mr-2 h-4 w-4" />
|
||||
<span>{isBottomTerminalExpanded ? t('commandPalette.item.collapseTerminal') : t('commandPalette.item.expandTerminal')}</span>
|
||||
<CommandShortcut>{shortcut('toggle_terminal_expanded')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={handleShowHelp}>
|
||||
<RiQuestionLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.keyboardShortcuts')}</span>
|
||||
<CommandShortcut>{shortcut('open_help')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading={t('commandPalette.section.theme')}>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('light')()}>
|
||||
<RiSunLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.themeLight')}</span>
|
||||
{themeMode === 'light' && <RiCheckLine className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('dark')()}>
|
||||
<RiMoonLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.themeDark')}</span>
|
||||
{themeMode === 'dark' && <RiCheckLine className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSetThemeMode('system')()}>
|
||||
<RiComputerLine className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.themeSystem')}</span>
|
||||
{themeMode === 'system' && <RiCheckLine className="ml-auto h-4 w-4" />}
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
|
||||
<CommandSeparator />
|
||||
|
||||
<CommandGroup heading={t('commandPalette.section.settings')}>
|
||||
<CommandItem onSelect={handleOpenSettings}>
|
||||
<RiSettings3Line className="mr-2 h-4 w-4" />
|
||||
<span>{t('commandPalette.item.openSettings')}</span>
|
||||
<CommandShortcut>{shortcut('open_settings')}</CommandShortcut>
|
||||
</CommandItem>
|
||||
{settingsItems.map((page) => {
|
||||
const Icon = getSettingsNavIcon(page.slug) ?? RiSettings3Line;
|
||||
return (
|
||||
<CommandItem key={page.slug} onSelect={() => handleOpenSettingsPage(page.slug)()}>
|
||||
<Icon className="mr-2 h-4 w-4" />
|
||||
<span>{settingsGroupLabelMap[page.group] ?? SETTINGS_GROUP_LABELS[page.group]}: {settingsPageLabelMap[page.slug] ?? page.title}</span>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
|
||||
{recentSessions.length > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup heading={t('commandPalette.section.recentSessions')}>
|
||||
{recentSessions.map((session) => (
|
||||
<CommandItem
|
||||
key={session.id}
|
||||
onSelect={() => handleOpenSession(session.id, currentDirectory ?? null)()}
|
||||
>
|
||||
<RiChatAi3Line className="mr-2 h-4 w-4" />
|
||||
<span className="truncate">{session.title || t('commandPalette.session.untitled')}</span>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
{hasQuery && isSearchingFiles && visibleFiles.length === 0 ? (
|
||||
<div className="px-3 py-2 typography-meta text-muted-foreground">
|
||||
{t('commandPalette.empty.searchingFiles')}
|
||||
</div>
|
||||
) : null}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,12 +70,6 @@ export const HelpDialog: React.FC = () => {
|
||||
icon: RiCommandLine,
|
||||
keys: '',
|
||||
},
|
||||
{
|
||||
id: 'open_quick_open',
|
||||
descriptionKey: 'helpDialog.item.quickOpenFile',
|
||||
icon: RiFileLine,
|
||||
keys: '',
|
||||
},
|
||||
{
|
||||
id: 'open_help',
|
||||
descriptionKey: "helpDialog.item.showKeyboardShortcuts",
|
||||
@@ -298,7 +292,7 @@ export const HelpDialog: React.FC = () => {
|
||||
<ul className="space-y-0.5 typography-meta">
|
||||
<li>
|
||||
• {t('helpDialog.proTips.commandPalette', {
|
||||
shortcut: renderShortcut('open_command_palette', `${mod} K`, shortcutOverrides),
|
||||
shortcut: renderShortcut('open_command_palette', `${mod} P`, shortcutOverrides),
|
||||
})}
|
||||
</li>
|
||||
<li>
|
||||
|
||||
@@ -1,257 +0,0 @@
|
||||
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';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
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 { t } = useI18n();
|
||||
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
|
||||
? t('quickOpenDialog.empty.openProjectFirst')
|
||||
: hasTypedQuery
|
||||
? (isSearching ? t('quickOpenDialog.empty.searchingFiles') : t('quickOpenDialog.empty.noMatchingFiles'))
|
||||
: t('quickOpenDialog.empty.noMatchingRecentFiles');
|
||||
|
||||
return (
|
||||
<Dialog open={isQuickOpenOpen} onOpenChange={setQuickOpenOpen}>
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>{t('quickOpenDialog.title')}</DialogTitle>
|
||||
<DialogDescription>{t('quickOpenDialog.description')}</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={t('quickOpenDialog.input.placeholder')}
|
||||
disabled={!currentRoot}
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>{emptyMessage}</CommandEmpty>
|
||||
|
||||
{currentRoot && visibleFiles.length > 0 && (
|
||||
<CommandGroup heading={hasTypedQuery ? t('quickOpenDialog.group.files') : t('quickOpenDialog.group.recentFiles')}>
|
||||
{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,7 +18,6 @@ export const useKeyboardShortcuts = () => {
|
||||
const currentSessionId = useSessionUIStore((s) => s.currentSessionId);
|
||||
const abortCurrentOperation = sessionActions.abortCurrentOperation;;
|
||||
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
||||
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const toggleRightSidebar = useUIStore((s) => s.toggleRightSidebar);
|
||||
@@ -62,12 +61,6 @@ export const useKeyboardShortcuts = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMatchesShortcut(e, combo('open_quick_open'))) {
|
||||
e.preventDefault();
|
||||
setQuickOpenOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (eventMatchesShortcut(e, combo('open_status'))) {
|
||||
e.preventDefault();
|
||||
void showOpenCodeStatus();
|
||||
@@ -425,7 +418,6 @@ export const useKeyboardShortcuts = () => {
|
||||
openNewSessionDraft,
|
||||
abortCurrentOperation,
|
||||
toggleCommandPalette,
|
||||
setQuickOpenOpen,
|
||||
toggleHelpDialog,
|
||||
toggleSidebar,
|
||||
toggleRightSidebar,
|
||||
|
||||
@@ -90,7 +90,7 @@ export const useMenuActions = (
|
||||
) => {
|
||||
const openNewSessionDraft = useSessionUIStore((s) => s.openNewSessionDraft);
|
||||
const toggleCommandPalette = useUIStore((s) => s.toggleCommandPalette);
|
||||
const setQuickOpenOpen = useUIStore((s) => s.setQuickOpenOpen);
|
||||
const setCommandPaletteOpen = useUIStore((s) => s.setCommandPaletteOpen);
|
||||
const toggleHelpDialog = useUIStore((s) => s.toggleHelpDialog);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const setSessionSwitcherOpen = useUIStore((s) => s.setSessionSwitcherOpen);
|
||||
@@ -146,7 +146,7 @@ export const useMenuActions = (
|
||||
break;
|
||||
|
||||
case 'quick-open':
|
||||
setQuickOpenOpen(true);
|
||||
setCommandPaletteOpen(true);
|
||||
break;
|
||||
|
||||
case 'new-session':
|
||||
@@ -237,7 +237,7 @@ export const useMenuActions = (
|
||||
setAboutDialogOpen,
|
||||
setActiveMainTab,
|
||||
setSessionSwitcherOpen,
|
||||
setQuickOpenOpen,
|
||||
setCommandPaletteOpen,
|
||||
setSettingsDialogOpen,
|
||||
setThemeMode,
|
||||
toggleCommandPalette,
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.field.pressKeys': 'Press keys...',
|
||||
'settings.openchamber.keyboardShortcuts.error.captureFirst': 'Capture a shortcut first.',
|
||||
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': 'This shortcut can conflict with browser defaults. It is still saved.',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_quick_open.label': 'Open quick open',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': 'Go to line (files editor)',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': 'Open command palette',
|
||||
'settings.openchamber.keyboardShortcuts.action.focus_input.label': 'Focus input',
|
||||
|
||||
@@ -1103,7 +1103,6 @@ export const dict = {
|
||||
'helpDialog.section.panels': 'Panels',
|
||||
'helpDialog.section.interface': 'Interface',
|
||||
'helpDialog.item.openCommandPalette': 'Open Command Palette',
|
||||
'helpDialog.item.quickOpenFile': 'Quick open file',
|
||||
'helpDialog.item.showKeyboardShortcuts': 'Show Keyboard Shortcuts (this dialog)',
|
||||
'helpDialog.item.toggleSessionSidebar': 'Toggle Session Sidebar',
|
||||
'helpDialog.item.cycleAgent': 'Cycle Agent (chat input)',
|
||||
@@ -1586,68 +1585,20 @@ export const dict = {
|
||||
'chat.messageBody.shellCommand.showOutput': 'Show output',
|
||||
'chat.messageBody.shellCommand.copied': 'Copied',
|
||||
'chat.messageBody.shellCommand.copyOutput': 'Copy output',
|
||||
'commandPalette.input.placeholder': 'Type a command or search...',
|
||||
'commandPalette.title': 'Command Palette',
|
||||
'commandPalette.description': 'Search files, sessions, and commands.',
|
||||
'commandPalette.input.placeholder': 'Search files, sessions, commands...',
|
||||
'commandPalette.empty.noResults': 'No results found.',
|
||||
'commandPalette.section.sessions': 'Sessions',
|
||||
'commandPalette.section.view': 'View',
|
||||
'commandPalette.section.theme': 'Theme',
|
||||
'commandPalette.section.settings': 'Settings',
|
||||
'commandPalette.section.recentSessions': 'Recent Sessions',
|
||||
'commandPalette.item.quickOpen': 'Quick Open',
|
||||
'commandPalette.empty.searchingFiles': 'Searching files...',
|
||||
'commandPalette.item.newSession': 'New Session',
|
||||
'commandPalette.item.newWorktreeDraft': 'New Worktree Draft',
|
||||
'commandPalette.item.showSessionSwitcher': 'Show Session Switcher',
|
||||
'commandPalette.item.toggleSidebar': 'Toggle Sidebar',
|
||||
'commandPalette.item.toggleRightSidebar': 'Toggle Right Sidebar',
|
||||
'commandPalette.item.showGitRightSidebar': 'Show Git in Right Sidebar',
|
||||
'commandPalette.item.showFilesRightSidebar': 'Show Files in Right Sidebar',
|
||||
'commandPalette.item.showContextUsage': 'Show Context Usage',
|
||||
'commandPalette.item.showPlan': 'Show Plan',
|
||||
'commandPalette.item.toggleTerminal': 'Toggle Terminal',
|
||||
'commandPalette.item.collapseTerminal': 'Collapse Terminal',
|
||||
'commandPalette.item.expandTerminal': 'Expand Terminal',
|
||||
'commandPalette.item.keyboardShortcuts': 'Keyboard Shortcuts',
|
||||
'commandPalette.item.themeLight': 'Light',
|
||||
'commandPalette.item.themeDark': 'Dark',
|
||||
'commandPalette.item.themeSystem': 'System',
|
||||
'commandPalette.item.openSettings': 'Open Settings...',
|
||||
'commandPalette.session.untitled': 'Untitled Session',
|
||||
'commandPalette.settingsGroup.appearance': 'Appearance',
|
||||
'commandPalette.settingsGroup.projects': 'Projects',
|
||||
'commandPalette.settingsGroup.general': 'General',
|
||||
'commandPalette.settingsGroup.opencode': 'OpenChamber',
|
||||
'commandPalette.settingsGroup.git': 'Git',
|
||||
'commandPalette.settingsGroup.skills': 'Skills',
|
||||
'commandPalette.settingsGroup.usage': 'Usage',
|
||||
'commandPalette.settingsGroup.advanced': 'Advanced',
|
||||
'commandPalette.settingsPage.home': 'Home',
|
||||
'commandPalette.settingsPage.projects': 'Projects',
|
||||
'commandPalette.settingsPage.remoteInstances': 'Remote Instances',
|
||||
'commandPalette.settingsPage.providers': 'Providers',
|
||||
'commandPalette.settingsPage.usage': 'Usage',
|
||||
'commandPalette.settingsPage.agents': 'Agents',
|
||||
'commandPalette.settingsPage.commands': 'Commands',
|
||||
'commandPalette.settingsPage.mcp': 'MCP',
|
||||
'commandPalette.settingsPage.skillsInstalled': 'Installed Skills',
|
||||
'commandPalette.settingsPage.skillsCatalog': 'Skills Catalog',
|
||||
'commandPalette.settingsPage.git': 'Git',
|
||||
'commandPalette.settingsPage.appearance': 'Appearance',
|
||||
'commandPalette.settingsPage.chat': 'Chat',
|
||||
'commandPalette.settingsPage.shortcuts': 'Keyboard Shortcuts',
|
||||
'commandPalette.settingsPage.sessions': 'Sessions',
|
||||
'commandPalette.settingsPage.magicPrompts': 'Magic Prompts',
|
||||
'commandPalette.settingsPage.notifications': 'Notifications',
|
||||
'commandPalette.settingsPage.voice': 'Voice',
|
||||
'commandPalette.settingsPage.tunnel': 'Remote Tunnel',
|
||||
'quickOpenDialog.title': 'Quick Open',
|
||||
'quickOpenDialog.description': 'Quickly open a file in the current project.',
|
||||
'quickOpenDialog.input.placeholder': 'Search files...',
|
||||
'quickOpenDialog.empty.openProjectFirst': 'Open a project first',
|
||||
'quickOpenDialog.empty.searchingFiles': 'Searching files...',
|
||||
'quickOpenDialog.empty.noMatchingFiles': 'No matching files found',
|
||||
'quickOpenDialog.empty.noMatchingRecentFiles': 'No recent files found',
|
||||
'quickOpenDialog.group.files': 'Files',
|
||||
'quickOpenDialog.group.recentFiles': 'Recent Files',
|
||||
'openCodeStatusDialog.title': 'OpenCode Status',
|
||||
'openCodeStatusDialog.description': 'Inspect current OpenCode status and diagnostics.',
|
||||
'openCodeStatusDialog.actions.copy': 'Copy',
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Pulsa las teclas...",
|
||||
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura un atajo primero.",
|
||||
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atajo puede entrar en conflicto con los predeterminados del navegador. Todavía se guarda.",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_quick_open.label": "Abrir búsqueda rápida",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir a línea (editor de archivos)",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos",
|
||||
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Enfocar entrada",
|
||||
|
||||
@@ -1069,7 +1069,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.section.panels": "Paneles",
|
||||
"helpDialog.section.interface": "Interfaz",
|
||||
"helpDialog.item.openCommandPalette": "Abrir paleta de comandos",
|
||||
"helpDialog.item.quickOpenFile": "Abrir archivo rápido",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Mostrar atajos de teclado (este diálogo)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Mostrar u ocultar barra lateral de sesión",
|
||||
"helpDialog.item.cycleAgent": "Cambiar agente (entrada de chat)",
|
||||
@@ -1552,68 +1551,20 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.messageBody.shellCommand.showOutput": "Mostrar salida",
|
||||
"chat.messageBody.shellCommand.copied": "Copiado",
|
||||
"chat.messageBody.shellCommand.copyOutput": "Copiar salida",
|
||||
"commandPalette.input.placeholder": "Escribe un comando o busca...",
|
||||
"commandPalette.title": "Paleta de comandos",
|
||||
"commandPalette.description": "Buscar archivos, sesiones y comandos.",
|
||||
"commandPalette.input.placeholder": "Buscar archivos, sesiones, comandos...",
|
||||
"commandPalette.empty.noResults": "No se encontraron resultados.",
|
||||
"commandPalette.section.sessions": "Sesiones",
|
||||
"commandPalette.section.view": "Ver",
|
||||
"commandPalette.section.theme": "Tema",
|
||||
"commandPalette.section.settings": "Configuración",
|
||||
"commandPalette.section.recentSessions": "Sesiones recientes",
|
||||
"commandPalette.item.quickOpen": "Abrir rápido",
|
||||
"commandPalette.empty.searchingFiles": "Buscando archivos...",
|
||||
"commandPalette.item.newSession": "Nueva sesión",
|
||||
"commandPalette.item.newWorktreeDraft": "Nuevo borrador de worktree",
|
||||
"commandPalette.item.showSessionSwitcher": "Mostrar cambiador de sesiones",
|
||||
"commandPalette.item.toggleSidebar": "Mostrar u ocultar barra lateral",
|
||||
"commandPalette.item.toggleRightSidebar": "Mostrar u ocultar barra lateral derecha",
|
||||
"commandPalette.item.showGitRightSidebar": "Mostrar Git en barra lateral derecha",
|
||||
"commandPalette.item.showFilesRightSidebar": "Mostrar archivos en barra lateral derecha",
|
||||
"commandPalette.item.showContextUsage": "Mostrar uso del contexto",
|
||||
"commandPalette.item.showPlan": "Mostrar plan",
|
||||
"commandPalette.item.toggleTerminal": "Mostrar u ocultar terminal",
|
||||
"commandPalette.item.collapseTerminal": "Colapsar terminal",
|
||||
"commandPalette.item.expandTerminal": "Expandir terminal",
|
||||
"commandPalette.item.keyboardShortcuts": "Atajos de teclado",
|
||||
"commandPalette.item.themeLight": "Claro",
|
||||
"commandPalette.item.themeDark": "Oscuro",
|
||||
"commandPalette.item.themeSystem": "Sistema",
|
||||
"commandPalette.item.openSettings": "Abrir configuración...",
|
||||
"commandPalette.session.untitled": "Sesión sin título",
|
||||
"commandPalette.settingsGroup.appearance": "Apariencia",
|
||||
"commandPalette.settingsGroup.projects": "Proyectos",
|
||||
"commandPalette.settingsGroup.general": "General",
|
||||
"commandPalette.settingsGroup.opencode": "OpenChamber",
|
||||
"commandPalette.settingsGroup.git": "Git",
|
||||
"commandPalette.settingsGroup.skills": "Habilidades",
|
||||
"commandPalette.settingsGroup.usage": "Uso",
|
||||
"commandPalette.settingsGroup.advanced": "Avanzado",
|
||||
"commandPalette.settingsPage.home": "Inicio",
|
||||
"commandPalette.settingsPage.projects": "Proyectos",
|
||||
"commandPalette.settingsPage.remoteInstances": "Instancias remotas",
|
||||
"commandPalette.settingsPage.providers": "Proveedores",
|
||||
"commandPalette.settingsPage.usage": "Uso",
|
||||
"commandPalette.settingsPage.agents": "Agentes",
|
||||
"commandPalette.settingsPage.commands": "Comandos",
|
||||
"commandPalette.settingsPage.mcp": "MCP",
|
||||
"commandPalette.settingsPage.skillsInstalled": "Habilidades instaladas",
|
||||
"commandPalette.settingsPage.skillsCatalog": "Catálogo de habilidades",
|
||||
"commandPalette.settingsPage.git": "Git",
|
||||
"commandPalette.settingsPage.appearance": "Apariencia",
|
||||
"commandPalette.settingsPage.chat": "Chat",
|
||||
"commandPalette.settingsPage.shortcuts": "Atajos de teclado",
|
||||
"commandPalette.settingsPage.sessions": "Sesiones",
|
||||
"commandPalette.settingsPage.magicPrompts": "Prompts mágicos",
|
||||
"commandPalette.settingsPage.notifications": "Notificaciones",
|
||||
"commandPalette.settingsPage.voice": "Voz",
|
||||
"commandPalette.settingsPage.tunnel": "Túnel remoto",
|
||||
"quickOpenDialog.title": "Apertura rápida",
|
||||
"quickOpenDialog.description": "Abre rápidamente un archivo del proyecto actual.",
|
||||
"quickOpenDialog.input.placeholder": "Buscar archivos...",
|
||||
"quickOpenDialog.empty.openProjectFirst": "Abre un proyecto primero",
|
||||
"quickOpenDialog.empty.searchingFiles": "Buscando archivos...",
|
||||
"quickOpenDialog.empty.noMatchingFiles": "No se encontraron archivos coincidentes",
|
||||
"quickOpenDialog.empty.noMatchingRecentFiles": "No se encontraron archivos recientes",
|
||||
"quickOpenDialog.group.files": "Archivos",
|
||||
"quickOpenDialog.group.recentFiles": "Archivos recientes",
|
||||
"openCodeStatusDialog.title": "Estado de OpenCode",
|
||||
"openCodeStatusDialog.description": "Inspecciona el estado actual de OpenCode y diagnósticos.",
|
||||
"openCodeStatusDialog.actions.copy": "Copiar",
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.field.pressKeys': '키를 누르세요...',
|
||||
'settings.openchamber.keyboardShortcuts.error.captureFirst': '먼저 단축키를 입력하세요.',
|
||||
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '이 단축키는 브라우저 기본값과 충돌할 수 있습니다. 그래도 저장됩니다.',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_quick_open.label': '빠른 열기 열기',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '줄로 이동(파일 편집기)',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '명령 팔레트 열기',
|
||||
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '입력에 포커스',
|
||||
|
||||
@@ -1105,7 +1105,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.section.panels': '패널',
|
||||
'helpDialog.section.interface': '인터페이스',
|
||||
'helpDialog.item.openCommandPalette': '명령 팔레트 열기',
|
||||
'helpDialog.item.quickOpenFile': '빠른 파일 열기',
|
||||
'helpDialog.item.showKeyboardShortcuts': '키보드 단축키 보기(이 대화상자)',
|
||||
'helpDialog.item.toggleSessionSidebar': '토글 세션 사이드바',
|
||||
'helpDialog.item.cycleAgent': '에이전트 순환(채팅 입력)',
|
||||
@@ -1586,68 +1585,20 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.messageBody.shellCommand.showOutput': '표시 출력',
|
||||
'chat.messageBody.shellCommand.copied': '복사됨',
|
||||
'chat.messageBody.shellCommand.copyOutput': '출력 복사',
|
||||
'commandPalette.input.placeholder': '명령 입력 또는 검색…',
|
||||
'commandPalette.title': '명령 팔레트',
|
||||
'commandPalette.description': '파일, 세션, 명령을 검색합니다.',
|
||||
'commandPalette.input.placeholder': '파일, 세션, 명령 검색…',
|
||||
'commandPalette.empty.noResults': '결과 없음',
|
||||
'commandPalette.section.sessions': '세션',
|
||||
'commandPalette.section.view': '보기',
|
||||
'commandPalette.section.theme': '테마',
|
||||
'commandPalette.section.settings': '설정',
|
||||
'commandPalette.section.recentSessions': '최근 세션',
|
||||
'commandPalette.item.quickOpen': '빠른 열기',
|
||||
'commandPalette.empty.searchingFiles': '파일 검색 중…',
|
||||
'commandPalette.item.newSession': '새 세션',
|
||||
'commandPalette.item.newWorktreeDraft': '새 워크트리 드래프트',
|
||||
'commandPalette.item.showSessionSwitcher': '세션 전환기 표시',
|
||||
'commandPalette.item.toggleSidebar': '토글 사이드바',
|
||||
'commandPalette.item.toggleRightSidebar': '오른쪽 사이드바 전환',
|
||||
'commandPalette.item.showGitRightSidebar': '오른쪽 사이드바에 Git 표시',
|
||||
'commandPalette.item.showFilesRightSidebar': '오른쪽 사이드바에 파일 표시',
|
||||
'commandPalette.item.showContextUsage': '컨텍스트 사용량 표시',
|
||||
'commandPalette.item.showPlan': '플랜 표시',
|
||||
'commandPalette.item.toggleTerminal': '토글 터미널',
|
||||
'commandPalette.item.collapseTerminal': '접기 터미널',
|
||||
'commandPalette.item.expandTerminal': '펼치기 터미널',
|
||||
'commandPalette.item.keyboardShortcuts': '키보드 단축키',
|
||||
'commandPalette.item.themeLight': '라이트',
|
||||
'commandPalette.item.themeDark': '다크',
|
||||
'commandPalette.item.themeSystem': '시스템',
|
||||
'commandPalette.item.openSettings': '설정... 열기',
|
||||
'commandPalette.session.untitled': '제목 없는 세션',
|
||||
'commandPalette.settingsGroup.appearance': '외관',
|
||||
'commandPalette.settingsGroup.projects': '프로젝트',
|
||||
'commandPalette.settingsGroup.general': '일반',
|
||||
'commandPalette.settingsGroup.opencode': 'OpenChamber',
|
||||
'commandPalette.settingsGroup.git': 'Git',
|
||||
'commandPalette.settingsGroup.skills': '스킬',
|
||||
'commandPalette.settingsGroup.usage': '사용량',
|
||||
'commandPalette.settingsGroup.advanced': '고급',
|
||||
'commandPalette.settingsPage.home': '홈',
|
||||
'commandPalette.settingsPage.projects': '프로젝트',
|
||||
'commandPalette.settingsPage.remoteInstances': '원격 인스턴스',
|
||||
'commandPalette.settingsPage.providers': '프로바이더',
|
||||
'commandPalette.settingsPage.usage': '사용량',
|
||||
'commandPalette.settingsPage.agents': '에이전트',
|
||||
'commandPalette.settingsPage.commands': '명령',
|
||||
'commandPalette.settingsPage.mcp': 'MCP',
|
||||
'commandPalette.settingsPage.skillsInstalled': '설치된 스킬',
|
||||
'commandPalette.settingsPage.skillsCatalog': '스킬 카탈로그',
|
||||
'commandPalette.settingsPage.git': 'Git',
|
||||
'commandPalette.settingsPage.appearance': '외관',
|
||||
'commandPalette.settingsPage.chat': '채팅',
|
||||
'commandPalette.settingsPage.shortcuts': '키보드 단축키',
|
||||
'commandPalette.settingsPage.sessions': '세션',
|
||||
'commandPalette.settingsPage.magicPrompts': 'Magic 프롬프트',
|
||||
'commandPalette.settingsPage.notifications': '알림',
|
||||
'commandPalette.settingsPage.voice': '음성',
|
||||
'commandPalette.settingsPage.tunnel': '리모트 터널',
|
||||
'quickOpenDialog.title': '빠른 열기',
|
||||
'quickOpenDialog.description': '현재 프로젝트에서 파일을 빠르게 여세요.',
|
||||
'quickOpenDialog.input.placeholder': '파일 검색…',
|
||||
'quickOpenDialog.empty.openProjectFirst': '먼저 프로젝트를 여세요',
|
||||
'quickOpenDialog.empty.searchingFiles': '파일 검색 중…',
|
||||
'quickOpenDialog.empty.noMatchingFiles': '일치하는 파일이 없습니다',
|
||||
'quickOpenDialog.empty.noMatchingRecentFiles': '최근 파일이 없습니다',
|
||||
'quickOpenDialog.group.files': '파일',
|
||||
'quickOpenDialog.group.recentFiles': '최근 파일',
|
||||
'openCodeStatusDialog.title': 'OpenCode 상태',
|
||||
'openCodeStatusDialog.description': '현재 OpenCode 상태와 진단 정보를 확인합니다.',
|
||||
'openCodeStatusDialog.actions.copy': '복사',
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Pressione as teclas...",
|
||||
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Captura um atalho primeiro.",
|
||||
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Este atalho pode entrar em conflito com os padrões do navegador. Ainda assim, ele será salvo.",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_quick_open.label": "Abrir busca rápida",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Ir para linha (editor de arquivos)",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Abrir paleta de comandos",
|
||||
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Focar entrada",
|
||||
|
||||
@@ -1069,7 +1069,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.section.panels": "Painéis",
|
||||
"helpDialog.section.interface": "Interface",
|
||||
"helpDialog.item.openCommandPalette": "Abrir paleta de comandos",
|
||||
"helpDialog.item.quickOpenFile": "Abrir arquivo rápido",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Mostrar atalhos de teclado (este diálogo)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Mostrar ou ocultar barra lateral de sessão",
|
||||
"helpDialog.item.cycleAgent": "Alternar agente (entrada do chat)",
|
||||
@@ -1552,68 +1551,20 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.messageBody.shellCommand.showOutput": "Mostrar saída",
|
||||
"chat.messageBody.shellCommand.copied": "Copiado",
|
||||
"chat.messageBody.shellCommand.copyOutput": "Copiar saída",
|
||||
"commandPalette.input.placeholder": "Digite um comando ou pesquise...",
|
||||
"commandPalette.title": "Paleta de comandos",
|
||||
"commandPalette.description": "Buscar arquivos, sessões e comandos.",
|
||||
"commandPalette.input.placeholder": "Buscar arquivos, sessões, comandos...",
|
||||
"commandPalette.empty.noResults": "Nenhum resultado encontrado.",
|
||||
"commandPalette.section.sessions": "Sessões",
|
||||
"commandPalette.section.view": "Ver",
|
||||
"commandPalette.section.theme": "Tema",
|
||||
"commandPalette.section.settings": "Configurações",
|
||||
"commandPalette.section.recentSessions": "Sessões recentes",
|
||||
"commandPalette.item.quickOpen": "Abrir rápido",
|
||||
"commandPalette.empty.searchingFiles": "Buscando arquivos...",
|
||||
"commandPalette.item.newSession": "Nova sessão",
|
||||
"commandPalette.item.newWorktreeDraft": "Novo rascunho de worktree",
|
||||
"commandPalette.item.showSessionSwitcher": "Mostrar seletor de sessões",
|
||||
"commandPalette.item.toggleSidebar": "Mostrar ou ocultar barra lateral",
|
||||
"commandPalette.item.toggleRightSidebar": "Mostrar ou ocultar barra lateral direita",
|
||||
"commandPalette.item.showGitRightSidebar": "Mostrar Git em barra lateral direita",
|
||||
"commandPalette.item.showFilesRightSidebar": "Mostrar arquivos em barra lateral direita",
|
||||
"commandPalette.item.showContextUsage": "Mostrar uso do contexto",
|
||||
"commandPalette.item.showPlan": "Mostrar plano",
|
||||
"commandPalette.item.toggleTerminal": "Mostrar ou ocultar terminal",
|
||||
"commandPalette.item.collapseTerminal": "Recolher terminal",
|
||||
"commandPalette.item.expandTerminal": "Expandir terminal",
|
||||
"commandPalette.item.keyboardShortcuts": "Atalhos de teclado",
|
||||
"commandPalette.item.themeLight": "Claro",
|
||||
"commandPalette.item.themeDark": "Escuro",
|
||||
"commandPalette.item.themeSystem": "Sistema",
|
||||
"commandPalette.item.openSettings": "Abrir configurações...",
|
||||
"commandPalette.session.untitled": "Sessão sem título",
|
||||
"commandPalette.settingsGroup.appearance": "Aparência",
|
||||
"commandPalette.settingsGroup.projects": "Projetos",
|
||||
"commandPalette.settingsGroup.general": "Geral",
|
||||
"commandPalette.settingsGroup.opencode": "OpenChamber",
|
||||
"commandPalette.settingsGroup.git": "Git",
|
||||
"commandPalette.settingsGroup.skills": "Habilidades",
|
||||
"commandPalette.settingsGroup.usage": "Uso",
|
||||
"commandPalette.settingsGroup.advanced": "Avançado",
|
||||
"commandPalette.settingsPage.home": "Início",
|
||||
"commandPalette.settingsPage.projects": "Projetos",
|
||||
"commandPalette.settingsPage.remoteInstances": "Instâncias remotas",
|
||||
"commandPalette.settingsPage.providers": "Provedores",
|
||||
"commandPalette.settingsPage.usage": "Uso",
|
||||
"commandPalette.settingsPage.agents": "Agentes",
|
||||
"commandPalette.settingsPage.commands": "Comandos",
|
||||
"commandPalette.settingsPage.mcp": "MCP",
|
||||
"commandPalette.settingsPage.skillsInstalled": "Habilidades instaladas",
|
||||
"commandPalette.settingsPage.skillsCatalog": "Catálogo de habilidades",
|
||||
"commandPalette.settingsPage.git": "Git",
|
||||
"commandPalette.settingsPage.appearance": "Aparência",
|
||||
"commandPalette.settingsPage.chat": "Chat",
|
||||
"commandPalette.settingsPage.shortcuts": "Atalhos de teclado",
|
||||
"commandPalette.settingsPage.sessions": "Sessões",
|
||||
"commandPalette.settingsPage.magicPrompts": "Prompts mágicos",
|
||||
"commandPalette.settingsPage.notifications": "Notificações",
|
||||
"commandPalette.settingsPage.voice": "Voz",
|
||||
"commandPalette.settingsPage.tunnel": "Túnel remoto",
|
||||
"quickOpenDialog.title": "Abertura rápida",
|
||||
"quickOpenDialog.description": "Abra rapidamente um arquivo do projeto atual.",
|
||||
"quickOpenDialog.input.placeholder": "Pesquisar arquivos...",
|
||||
"quickOpenDialog.empty.openProjectFirst": "Abra um projeto primeiro",
|
||||
"quickOpenDialog.empty.searchingFiles": "Buscando arquivos...",
|
||||
"quickOpenDialog.empty.noMatchingFiles": "Nenhum arquivo correspondente encontrado",
|
||||
"quickOpenDialog.empty.noMatchingRecentFiles": "Nenhum arquivo recente correspondente encontrado",
|
||||
"quickOpenDialog.group.files": "Arquivos",
|
||||
"quickOpenDialog.group.recentFiles": "Arquivos recentes",
|
||||
"openCodeStatusDialog.title": "Status do OpenCode",
|
||||
"openCodeStatusDialog.description": "Inspecione o status atual do OpenCode e os diagnósticos.",
|
||||
"openCodeStatusDialog.actions.copy": "Copiar",
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
"settings.openchamber.keyboardShortcuts.field.pressKeys": "Натисніть клавіші...",
|
||||
"settings.openchamber.keyboardShortcuts.error.captureFirst": "Спочатку запишіть комбінацію клавіш.",
|
||||
"settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut": "Ця комбінація клавіш може конфліктувати зі стандартними скороченнями браузера. Її все одно збережено.",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_quick_open.label": "Швидке відкриття",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_go_to_line.label": "Перейти до рядка (редактор файлів)",
|
||||
"settings.openchamber.keyboardShortcuts.action.open_command_palette.label": "Відкрити палітру команд",
|
||||
"settings.openchamber.keyboardShortcuts.action.focus_input.label": "Фокус на полі вводу",
|
||||
|
||||
@@ -1069,7 +1069,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
"helpDialog.section.panels": "Панелі",
|
||||
"helpDialog.section.interface": "Інтерфейс",
|
||||
"helpDialog.item.openCommandPalette": "Відкрити палітру команд",
|
||||
"helpDialog.item.quickOpenFile": "Швидке відкриття файлу",
|
||||
"helpDialog.item.showKeyboardShortcuts": "Показати комбінації клавіш (це діалогове вікно)",
|
||||
"helpDialog.item.toggleSessionSidebar": "Перемкнути бічну панель сесій",
|
||||
"helpDialog.item.cycleAgent": "Перемкнути агента (введення в чат)",
|
||||
@@ -1552,68 +1551,20 @@ export const dict: Record<I18nKey, string> = {
|
||||
"chat.messageBody.shellCommand.showOutput": "Показати результат",
|
||||
"chat.messageBody.shellCommand.copied": "Скопійовано",
|
||||
"chat.messageBody.shellCommand.copyOutput": "Копіювати вивід",
|
||||
"commandPalette.input.placeholder": "Введіть команду або знайдіть...",
|
||||
"commandPalette.title": "Палітра команд",
|
||||
"commandPalette.description": "Пошук файлів, сесій і команд.",
|
||||
"commandPalette.input.placeholder": "Пошук файлів, сесій, команд...",
|
||||
"commandPalette.empty.noResults": "Результатів не знайдено.",
|
||||
"commandPalette.section.sessions": "Сесії",
|
||||
"commandPalette.section.view": "Переглянути",
|
||||
"commandPalette.section.theme": "Тема",
|
||||
"commandPalette.section.settings": "Налаштування",
|
||||
"commandPalette.section.recentSessions": "Останні сесії",
|
||||
"commandPalette.item.quickOpen": "Швидке відкриття",
|
||||
"commandPalette.empty.searchingFiles": "Пошук файлів...",
|
||||
"commandPalette.item.newSession": "Нова сесія",
|
||||
"commandPalette.item.newWorktreeDraft": "Чернетка нового worktree",
|
||||
"commandPalette.item.showSessionSwitcher": "Показати перемикач сесій",
|
||||
"commandPalette.item.toggleSidebar": "Перемкнути бічну панель",
|
||||
"commandPalette.item.toggleRightSidebar": "Перемкнути праву бічну панель",
|
||||
"commandPalette.item.showGitRightSidebar": "Показати Git на правій бічній панелі",
|
||||
"commandPalette.item.showFilesRightSidebar": "Показати файли на правій бічній панелі",
|
||||
"commandPalette.item.showContextUsage": "Показати використання контексту",
|
||||
"commandPalette.item.showPlan": "Показати план",
|
||||
"commandPalette.item.toggleTerminal": "Перемкнути термінал",
|
||||
"commandPalette.item.collapseTerminal": "Згорнути термінал",
|
||||
"commandPalette.item.expandTerminal": "Розгорнути термінал",
|
||||
"commandPalette.item.keyboardShortcuts": "Комбінації клавіш",
|
||||
"commandPalette.item.themeLight": "Світла тема",
|
||||
"commandPalette.item.themeDark": "Темна тема",
|
||||
"commandPalette.item.themeSystem": "Системна тема",
|
||||
"commandPalette.item.openSettings": "Відкрити налаштування...",
|
||||
"commandPalette.session.untitled": "Сесія без назви",
|
||||
"commandPalette.settingsGroup.appearance": "Зовнішній вигляд",
|
||||
"commandPalette.settingsGroup.projects": "Проєкти",
|
||||
"commandPalette.settingsGroup.general": "Загальний",
|
||||
"commandPalette.settingsGroup.opencode": "OpenChamber",
|
||||
"commandPalette.settingsGroup.git": "Git",
|
||||
"commandPalette.settingsGroup.skills": "Навички",
|
||||
"commandPalette.settingsGroup.usage": "Використання",
|
||||
"commandPalette.settingsGroup.advanced": "Просунутий",
|
||||
"commandPalette.settingsPage.home": "додому",
|
||||
"commandPalette.settingsPage.projects": "Проєкти",
|
||||
"commandPalette.settingsPage.remoteInstances": "Віддалені інстанси",
|
||||
"commandPalette.settingsPage.providers": "Провайдери",
|
||||
"commandPalette.settingsPage.usage": "Використання",
|
||||
"commandPalette.settingsPage.agents": "Агенти",
|
||||
"commandPalette.settingsPage.commands": "Команди",
|
||||
"commandPalette.settingsPage.mcp": "MCP",
|
||||
"commandPalette.settingsPage.skillsInstalled": "Встановлені навички",
|
||||
"commandPalette.settingsPage.skillsCatalog": "Каталог навичок",
|
||||
"commandPalette.settingsPage.git": "Git",
|
||||
"commandPalette.settingsPage.appearance": "Зовнішній вигляд",
|
||||
"commandPalette.settingsPage.chat": "Чат",
|
||||
"commandPalette.settingsPage.shortcuts": "Комбінації клавіш",
|
||||
"commandPalette.settingsPage.sessions": "Сесії",
|
||||
"commandPalette.settingsPage.magicPrompts": "Магічні промпти",
|
||||
"commandPalette.settingsPage.notifications": "Сповіщення",
|
||||
"commandPalette.settingsPage.voice": "Голос",
|
||||
"commandPalette.settingsPage.tunnel": "Віддалений тунель",
|
||||
"quickOpenDialog.title": "Швидке відкриття",
|
||||
"quickOpenDialog.description": "Швидко відкрити файл у поточному проєкті.",
|
||||
"quickOpenDialog.input.placeholder": "Пошук файлів...",
|
||||
"quickOpenDialog.empty.openProjectFirst": "Спочатку відкрийте проєкт",
|
||||
"quickOpenDialog.empty.searchingFiles": "Пошук файлів...",
|
||||
"quickOpenDialog.empty.noMatchingFiles": "Відповідних файлів не знайдено",
|
||||
"quickOpenDialog.empty.noMatchingRecentFiles": "Останні файли не знайдено",
|
||||
"quickOpenDialog.group.files": "Файли",
|
||||
"quickOpenDialog.group.recentFiles": "Останні файли",
|
||||
"openCodeStatusDialog.title": "Статус OpenCode",
|
||||
"openCodeStatusDialog.description": "Перегляньте поточний стан OpenCode і діагностику.",
|
||||
"openCodeStatusDialog.actions.copy": "Копіювати",
|
||||
|
||||
@@ -718,7 +718,6 @@ export const settingsDict = {
|
||||
'settings.openchamber.keyboardShortcuts.field.pressKeys': '按下按键...',
|
||||
'settings.openchamber.keyboardShortcuts.error.captureFirst': '请先录入一个快捷键。',
|
||||
'settings.openchamber.keyboardShortcuts.warning.riskyBrowserShortcut': '该快捷键可能与浏览器默认快捷键冲突,但仍已保存。',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_quick_open.label': '打开快速打开',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_go_to_line.label': '跳转到行(文件编辑器)',
|
||||
'settings.openchamber.keyboardShortcuts.action.open_command_palette.label': '打开命令面板',
|
||||
'settings.openchamber.keyboardShortcuts.action.focus_input.label': '聚焦输入框',
|
||||
|
||||
@@ -1069,7 +1069,6 @@ export const dict: Record<I18nKey, string> = {
|
||||
'helpDialog.section.panels': '面板',
|
||||
'helpDialog.section.interface': '界面',
|
||||
'helpDialog.item.openCommandPalette': '打开命令面板',
|
||||
'helpDialog.item.quickOpenFile': '快速打开文件',
|
||||
'helpDialog.item.showKeyboardShortcuts': '显示键盘快捷键(此对话框)',
|
||||
'helpDialog.item.toggleSessionSidebar': '切换会话侧边栏',
|
||||
'helpDialog.item.cycleAgent': '循环切换智能体(聊天输入)',
|
||||
@@ -1552,68 +1551,20 @@ export const dict: Record<I18nKey, string> = {
|
||||
'chat.messageBody.shellCommand.showOutput': '显示输出',
|
||||
'chat.messageBody.shellCommand.copied': '已复制',
|
||||
'chat.messageBody.shellCommand.copyOutput': '复制输出',
|
||||
'commandPalette.input.placeholder': '输入命令或搜索...',
|
||||
'commandPalette.title': '命令面板',
|
||||
'commandPalette.description': '搜索文件、会话和命令。',
|
||||
'commandPalette.input.placeholder': '搜索文件、会话、命令...',
|
||||
'commandPalette.empty.noResults': '未找到结果。',
|
||||
'commandPalette.section.sessions': '会话',
|
||||
'commandPalette.section.view': '视图',
|
||||
'commandPalette.section.theme': '主题',
|
||||
'commandPalette.section.settings': '设置',
|
||||
'commandPalette.section.recentSessions': '最近会话',
|
||||
'commandPalette.item.quickOpen': '快速打开',
|
||||
'commandPalette.empty.searchingFiles': '正在搜索文件...',
|
||||
'commandPalette.item.newSession': '新建会话',
|
||||
'commandPalette.item.newWorktreeDraft': '新建工作树草稿',
|
||||
'commandPalette.item.showSessionSwitcher': '显示会话切换器',
|
||||
'commandPalette.item.toggleSidebar': '切换侧边栏',
|
||||
'commandPalette.item.toggleRightSidebar': '切换右侧边栏',
|
||||
'commandPalette.item.showGitRightSidebar': '在右侧边栏显示 Git',
|
||||
'commandPalette.item.showFilesRightSidebar': '在右侧边栏显示文件',
|
||||
'commandPalette.item.showContextUsage': '显示上下文用量',
|
||||
'commandPalette.item.showPlan': '显示计划',
|
||||
'commandPalette.item.toggleTerminal': '切换终端',
|
||||
'commandPalette.item.collapseTerminal': '收起终端',
|
||||
'commandPalette.item.expandTerminal': '展开终端',
|
||||
'commandPalette.item.keyboardShortcuts': '键盘快捷键',
|
||||
'commandPalette.item.themeLight': '浅色',
|
||||
'commandPalette.item.themeDark': '深色',
|
||||
'commandPalette.item.themeSystem': '跟随系统',
|
||||
'commandPalette.item.openSettings': '打开设置...',
|
||||
'commandPalette.session.untitled': '未命名会话',
|
||||
'commandPalette.settingsGroup.appearance': '外观',
|
||||
'commandPalette.settingsGroup.projects': '项目',
|
||||
'commandPalette.settingsGroup.general': '通用',
|
||||
'commandPalette.settingsGroup.opencode': 'OpenChamber',
|
||||
'commandPalette.settingsGroup.git': 'Git',
|
||||
'commandPalette.settingsGroup.skills': '技能',
|
||||
'commandPalette.settingsGroup.usage': '用量',
|
||||
'commandPalette.settingsGroup.advanced': '高级',
|
||||
'commandPalette.settingsPage.home': '首页',
|
||||
'commandPalette.settingsPage.projects': '项目',
|
||||
'commandPalette.settingsPage.remoteInstances': '远程实例',
|
||||
'commandPalette.settingsPage.providers': '提供商',
|
||||
'commandPalette.settingsPage.usage': '用量',
|
||||
'commandPalette.settingsPage.agents': 'Agents',
|
||||
'commandPalette.settingsPage.commands': '命令',
|
||||
'commandPalette.settingsPage.mcp': 'MCP',
|
||||
'commandPalette.settingsPage.skillsInstalled': '已安装技能',
|
||||
'commandPalette.settingsPage.skillsCatalog': '技能目录',
|
||||
'commandPalette.settingsPage.git': 'Git',
|
||||
'commandPalette.settingsPage.appearance': '外观',
|
||||
'commandPalette.settingsPage.chat': '聊天',
|
||||
'commandPalette.settingsPage.shortcuts': '键盘快捷键',
|
||||
'commandPalette.settingsPage.sessions': '会话',
|
||||
'commandPalette.settingsPage.magicPrompts': '魔法提示词',
|
||||
'commandPalette.settingsPage.notifications': '通知',
|
||||
'commandPalette.settingsPage.voice': '语音',
|
||||
'commandPalette.settingsPage.tunnel': '远程隧道',
|
||||
'quickOpenDialog.title': '快速打开',
|
||||
'quickOpenDialog.description': '在当前项目中快速打开文件。',
|
||||
'quickOpenDialog.input.placeholder': '搜索文件...',
|
||||
'quickOpenDialog.empty.openProjectFirst': '请先打开项目',
|
||||
'quickOpenDialog.empty.searchingFiles': '正在搜索文件...',
|
||||
'quickOpenDialog.empty.noMatchingFiles': '未找到匹配文件',
|
||||
'quickOpenDialog.empty.noMatchingRecentFiles': '未找到最近文件',
|
||||
'quickOpenDialog.group.files': '文件',
|
||||
'quickOpenDialog.group.recentFiles': '最近文件',
|
||||
'openCodeStatusDialog.title': 'OpenCode 状态',
|
||||
'openCodeStatusDialog.description': '查看当前 OpenCode 状态与诊断信息。',
|
||||
'openCodeStatusDialog.actions.copy': '复制',
|
||||
|
||||
@@ -107,6 +107,67 @@ export function filterByFuzzyQuery<T>(
|
||||
return matching;
|
||||
}
|
||||
|
||||
/**
|
||||
* Score-sorted fuzzy ranking. Strict (low threshold), prioritizes substring
|
||||
* matches (especially prefix matches), and returns the top N.
|
||||
*
|
||||
* Use this for command-palette-style result ranking where order matters more
|
||||
* than recall.
|
||||
*/
|
||||
export function scoreByFuzzyQuery<T>(
|
||||
items: T[],
|
||||
query: string,
|
||||
getText: (item: T) => string,
|
||||
options?: { limit?: number; threshold?: number; noFuzzy?: boolean },
|
||||
): { item: T; score: number }[] {
|
||||
if (!query) return items.map((item) => ({ item, score: 0 }));
|
||||
const limit = options?.limit ?? items.length;
|
||||
const threshold = options?.threshold ?? 0.3;
|
||||
const noFuzzy = options?.noFuzzy ?? false;
|
||||
const queryLower = query.toLowerCase();
|
||||
|
||||
const scored: { item: T; score: number }[] = [];
|
||||
const fuzzyCandidates: { item: T; text: string }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const text = getText(item);
|
||||
if (!text) continue;
|
||||
const lower = text.toLowerCase();
|
||||
const idx = lower.indexOf(queryLower);
|
||||
if (idx === 0) {
|
||||
scored.push({ item, score: -1 });
|
||||
continue;
|
||||
}
|
||||
if (idx > 0) {
|
||||
scored.push({ item, score: idx / 1000 });
|
||||
continue;
|
||||
}
|
||||
if (!noFuzzy) fuzzyCandidates.push({ item, text });
|
||||
}
|
||||
|
||||
if (fuzzyCandidates.length > 0) {
|
||||
const fuse = new Fuse(
|
||||
fuzzyCandidates.map((c) => c.text),
|
||||
{ threshold, ignoreLocation: true, distance: 100, includeScore: true, minMatchCharLength: 2 },
|
||||
);
|
||||
for (const result of fuse.search(query)) {
|
||||
scored.push({ item: fuzzyCandidates[result.refIndex].item, score: result.score ?? 1 });
|
||||
}
|
||||
}
|
||||
|
||||
scored.sort((a, b) => a.score - b.score);
|
||||
return scored.slice(0, limit);
|
||||
}
|
||||
|
||||
export function rankByFuzzyQuery<T>(
|
||||
items: T[],
|
||||
query: string,
|
||||
getText: (item: T) => string,
|
||||
options?: { limit?: number; threshold?: number; noFuzzy?: boolean },
|
||||
): T[] {
|
||||
return scoreByFuzzyQuery(items, query, getText, options).map((x) => x.item);
|
||||
}
|
||||
|
||||
export function partitionByFuzzyQuery<T>(
|
||||
items: T[],
|
||||
query: string,
|
||||
|
||||
@@ -104,13 +104,6 @@ export function keyToShortcutToken(key: string): string {
|
||||
}
|
||||
|
||||
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_go_to_line',
|
||||
defaultCombo: 'alt+g',
|
||||
@@ -120,7 +113,7 @@ const SHORTCUT_ACTIONS: ReadonlyArray<ShortcutAction> = [
|
||||
},
|
||||
{
|
||||
id: 'open_command_palette',
|
||||
defaultCombo: 'mod+k',
|
||||
defaultCombo: 'mod+p',
|
||||
label: 'Open command palette',
|
||||
description: 'Open the command palette',
|
||||
customizable: true,
|
||||
|
||||
@@ -487,7 +487,6 @@ interface UIStore {
|
||||
pendingFileNavigation: PendingFileNavigation | null;
|
||||
pendingFileFocusPath: string | null;
|
||||
isMobile: boolean;
|
||||
isQuickOpenOpen: boolean;
|
||||
isCommandPaletteOpen: boolean;
|
||||
isHelpDialogOpen: boolean;
|
||||
isAboutDialogOpen: boolean;
|
||||
@@ -613,8 +612,6 @@ interface UIStore {
|
||||
navigateToDiff: (filePath: string) => void;
|
||||
consumePendingDiffFile: () => string | null;
|
||||
setIsMobile: (isMobile: boolean) => void;
|
||||
setQuickOpenOpen: (open: boolean) => void;
|
||||
toggleQuickOpen: () => void;
|
||||
toggleCommandPalette: () => void;
|
||||
setCommandPaletteOpen: (open: boolean) => void;
|
||||
toggleHelpDialog: () => void;
|
||||
@@ -743,7 +740,6 @@ export const useUIStore = create<UIStore>()(
|
||||
pendingFileNavigation: null,
|
||||
pendingFileFocusPath: null,
|
||||
isMobile: false,
|
||||
isQuickOpenOpen: false,
|
||||
isCommandPaletteOpen: false,
|
||||
isHelpDialogOpen: false,
|
||||
isAboutDialogOpen: false,
|
||||
@@ -1286,14 +1282,6 @@ export const useUIStore = create<UIStore>()(
|
||||
set({ isMobile });
|
||||
},
|
||||
|
||||
setQuickOpenOpen: (open) => {
|
||||
set({ isQuickOpenOpen: open });
|
||||
},
|
||||
|
||||
toggleQuickOpen: () => {
|
||||
set((state) => ({ isQuickOpenOpen: !state.isQuickOpenOpen }));
|
||||
},
|
||||
|
||||
toggleCommandPalette: () => {
|
||||
set((state) => ({ isCommandPaletteOpen: !state.isCommandPaletteOpen }));
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user