agent-ecosystem/src/renderer/components/team/editor/QuickOpenDialog.tsx
iliya 2ceed41e00 fix: resolve all CI lint errors and flaky test
- Fix React hooks violations: ref updates during render (useDraftPersistence,
  useChipDraftPersistence, useAttachments), setState in effects across 15+
  components, useCallback self-reference TDZ in useResizableColumns
- Fix TypeScript lint: remove unnecessary type assertions, replace inline
  import() annotations with direct imports, remove unused variables/imports
- Fix SonarJS issues: prefer-regexp-exec, slow-regex in SubagentResolver,
  no-misleading-array-reverse in TeamProvisioningService, use-type-alias
  in ClaudeLogsSection, variable shadowing in ChangeExtractorService
- Fix accessibility: associate labels with controls in filter popovers
- Fix template expression safety: wrap unknown errors with String()
- Fix flaky FileWatcher test: floor instanceCreatedAt to second granularity
  to match filesystem birthtimeMs resolution on Linux
- Replace TODO comments with NOTE where features are intentionally disabled
- Remove unused leadContextByTeam from TeamDetailView store selector

62 files changed across main process, renderer, shared types, and hooks.
All 1646 tests pass, typecheck clean, 0 lint errors.
2026-03-05 21:09:45 +02:00

162 lines
5 KiB
TypeScript

/**
* Quick Open dialog (Cmd+P) — fuzzy file search using cmdk.
*
* Escape closes dialog (not the editor overlay).
* Loads ALL project files via backend API on mount (not limited to expanded dirs).
*/
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useStore } from '@renderer/store';
import { Command } from 'cmdk';
import { Loader2 } from 'lucide-react';
import { getFileIcon } from './fileIcons';
import type { QuickOpenFile } from '@shared/types/editor';
// =============================================================================
// Types
// =============================================================================
interface QuickOpenDialogProps {
onClose: () => void;
onSelectFile: (filePath: string) => void;
}
// =============================================================================
// Component
// =============================================================================
export const QuickOpenDialog = ({
onClose,
onSelectFile,
}: QuickOpenDialogProps): React.ReactElement => {
const projectPath = useStore((s) => s.editorProjectPath);
const dialogRef = useRef<HTMLDivElement>(null);
const [allFiles, setAllFiles] = useState<QuickOpenFile[]>([]);
const [loading, setLoading] = useState(true);
// Load all project files on mount via backend API
useEffect(() => {
let cancelled = false;
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change
setLoading(true);
window.electronAPI.editor
.listFiles()
.then((files) => {
if (!cancelled) {
setAllFiles(files);
setLoading(false);
}
})
.catch(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [projectPath]);
// Escape to close dialog (not overlay)
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
onClose();
}
},
[onClose]
);
useEffect(() => {
window.addEventListener('keydown', handleKeyDown, true);
return () => window.removeEventListener('keydown', handleKeyDown, true);
}, [handleKeyDown]);
const handleSelect = useCallback(
(value: string) => {
// value is relativePath from cmdk — look up full path
const file = allFiles.find((f) => f.relativePath === value);
if (file) {
onSelectFile(file.path);
onClose();
}
},
[allFiles, onSelectFile, onClose]
);
// Memoize file icon lookups
const fileItems = useMemo(
() =>
allFiles.map((file) => ({
...file,
iconInfo: getFileIcon(file.name),
})),
[allFiles]
);
return (
<div className="fixed inset-0 z-[60] flex items-start justify-center pt-[15vh]">
{/* Backdrop */}
<div
className="fixed inset-0 bg-black/40"
onClick={onClose}
onKeyDown={(e) => {
if (e.key === 'Escape') onClose();
}}
role="presentation"
/>
{/* Dialog */}
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-label="Quick Open"
className="relative z-10 w-[520px] overflow-hidden rounded-lg border border-border-emphasis bg-surface shadow-2xl"
>
<Command label="Quick Open" shouldFilter={true}>
<Command.Input
placeholder="Search files by name..."
className="w-full border-b border-border bg-transparent px-4 py-3 text-sm text-text outline-none placeholder:text-text-muted"
autoFocus
/>
<Command.List className="max-h-80 overflow-y-auto p-1">
{loading && (
<div className="flex items-center justify-center gap-2 p-6 text-sm text-text-muted">
<Loader2 className="size-4 animate-spin" />
<span>Loading files...</span>
</div>
)}
{!loading && (
<Command.Empty className="p-6 text-center text-sm text-text-muted">
No files found
</Command.Empty>
)}
{fileItems.map((file) => {
const Icon = file.iconInfo.icon;
return (
<Command.Item
key={file.path}
value={file.relativePath}
onSelect={() => handleSelect(file.relativePath)}
className="flex cursor-pointer items-center gap-2 rounded px-3 py-1.5 text-sm text-text-secondary aria-selected:bg-surface-raised aria-selected:text-text"
>
<Icon className="size-4 shrink-0" style={{ color: file.iconInfo.color }} />
<span className="truncate font-medium">{file.name}</span>
<span className="ml-auto truncate text-xs text-text-muted">
{file.relativePath}
</span>
</Command.Item>
);
})}
</Command.List>
</Command>
</div>
</div>
);
};