Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
129 lines
4.8 KiB
TypeScript
129 lines
4.8 KiB
TypeScript
import { FileCode, Search, Zap } from 'lucide-react';
|
|
import React, { memo, useMemo, useState } from 'react';
|
|
import { useI18n } from '../../application/i18n/I18nProvider';
|
|
import { cn } from '../../lib/utils';
|
|
import type { Snippet } from '../../types';
|
|
import { FixedSizeVirtualList } from '../ui/FixedSizeVirtualList';
|
|
import { Input } from '../ui/input';
|
|
import { SnippetCommandTooltipContent } from './SnippetCommandTooltipContent';
|
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../ui/tooltip';
|
|
|
|
const ROW_HEIGHT = 34;
|
|
|
|
export interface SnippetCommandPickerProps {
|
|
snippets: Snippet[];
|
|
selectedId?: string | null;
|
|
onSelect: (snippet: Snippet) => void;
|
|
className?: string;
|
|
showTitle?: boolean;
|
|
}
|
|
|
|
export const SnippetCommandPicker = memo(function SnippetCommandPicker({
|
|
snippets,
|
|
selectedId,
|
|
onSelect,
|
|
className,
|
|
showTitle = true,
|
|
}: SnippetCommandPickerProps) {
|
|
const { t } = useI18n();
|
|
const [search, setSearch] = useState('');
|
|
|
|
const filtered = useMemo(() => {
|
|
const q = search.trim().toLowerCase();
|
|
const sorted = [...snippets].sort((a, b) => a.label.localeCompare(b.label));
|
|
if (!q) return sorted;
|
|
return sorted.filter(
|
|
(snippet) =>
|
|
snippet.label.toLowerCase().includes(q)
|
|
|| snippet.command.toLowerCase().includes(q)
|
|
|| (snippet.package || '').toLowerCase().includes(q),
|
|
);
|
|
}, [search, snippets]);
|
|
|
|
const listItems = useMemo(
|
|
() => filtered.map((snippet) => ({ key: snippet.id, snippet })),
|
|
[filtered],
|
|
);
|
|
|
|
return (
|
|
<TooltipProvider delayDuration={300}>
|
|
<div
|
|
className={cn(
|
|
'flex min-h-0 flex-col overflow-hidden rounded-md border border-border/60 bg-muted/20',
|
|
className,
|
|
)}
|
|
>
|
|
<div className="shrink-0 border-b border-border/50 px-2 py-1.5">
|
|
{showTitle && (
|
|
<p className="mb-1.5 px-0.5 text-[10px] font-medium text-muted-foreground">
|
|
{t('systemManager.tmux.pickSnippet')}
|
|
</p>
|
|
)}
|
|
<div className="relative">
|
|
<Search size={12} className="absolute left-2 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder={t('snippets.searchPlaceholder')}
|
|
className="h-7 border-none bg-background/60 pl-7 text-xs"
|
|
disabled={snippets.length === 0}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="min-h-0 flex-1">
|
|
{snippets.length === 0 ? (
|
|
<div className="flex h-full flex-col items-center justify-center px-4 py-6 text-center text-muted-foreground">
|
|
<Zap size={22} className="mb-2 opacity-40" />
|
|
<p className="text-xs">{t('systemManager.tmux.pickSnippetEmpty')}</p>
|
|
</div>
|
|
) : filtered.length === 0 ? (
|
|
<div className="px-3 py-4 text-center text-xs italic text-muted-foreground">
|
|
{t('common.noResultsFound')}
|
|
</div>
|
|
) : (
|
|
<FixedSizeVirtualList
|
|
className="h-full"
|
|
contentClassName="py-1"
|
|
items={listItems}
|
|
itemHeight={ROW_HEIGHT}
|
|
getItemKey={(item) => item.key}
|
|
renderItem={(item) => {
|
|
const { snippet } = item;
|
|
const selected = selectedId === snippet.id;
|
|
return (
|
|
<Tooltip>
|
|
<TooltipTrigger asChild>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSelect(snippet)}
|
|
className={cn(
|
|
'flex w-full items-center gap-1.5 px-2.5 py-1.5 text-left transition-colors',
|
|
selected
|
|
? 'bg-primary/10 text-foreground'
|
|
: 'hover:bg-accent/50',
|
|
)}
|
|
>
|
|
<FileCode size={12} className="shrink-0 text-muted-foreground" />
|
|
<span className="min-w-0 flex-1 truncate text-xs font-medium">{snippet.label}</span>
|
|
{snippet.package ? (
|
|
<span className="max-w-[42%] shrink-0 truncate text-[10px] text-muted-foreground">
|
|
{snippet.package}
|
|
</span>
|
|
) : null}
|
|
</button>
|
|
</TooltipTrigger>
|
|
<TooltipContent side="right" align="start">
|
|
<SnippetCommandTooltipContent label={snippet.label} command={snippet.command} />
|
|
</TooltipContent>
|
|
</Tooltip>
|
|
);
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</TooltipProvider>
|
|
);
|
|
});
|