返回 DeepSeek-Reasonix
PinnedFilesShelf.tsx
根目录 / desktop / frontend / src / components / PinnedFilesShelf.tsx
1 import { useCallback } from "react";
2 import { AlertTriangle, FileText, Pin, X } from "lucide-react";
3 import { app } from "../lib/bridge";
4 import { useT } from "../lib/i18n";
5 import { useToast } from "../lib/toast";
6 import type { PinnedFileInfo } from "../lib/pinnedContextBridge";
7
8 export function PinnedFilesShelf({
9 tabId,
10 pinnedFiles,
11 }: {
12 tabId: string;
13 pinnedFiles?: PinnedFileInfo[];
14 }) {
15 const t = useT();
16 const { showToast } = useToast();
17
18 const handleUnpin = useCallback(
19 (path: string, e: React.MouseEvent) => {
20 e.stopPropagation();
21 if (!tabId) return;
22 void app.UnpinFileForTab(tabId, path).catch((err: unknown) => {
23 showToast(String((err as Error)?.message || err), "error");
24 });
25 },
26 [tabId, showToast],
27 );
28
29 const handleOpenFile = useCallback(
30 (path: string) => {
31 if (!tabId) return;
32 void app.OpenWorkspacePathForTab(tabId, path).catch(() => {});
33 },
34 [tabId],
35 );
36
37 if (!pinnedFiles || pinnedFiles.length === 0) {
38 return null;
39 }
40
41 return (
42 <div
43 className="pinned-files-shelf flex items-center flex-wrap gap-1.5 px-3 py-1.5 border-b border-border/40 bg-surface-raised/40 text-xs select-none transition-all"
44 role="region"
45 aria-label={t("pinnedFiles.shelfTitle")}
46 >
47 <div className="flex items-center gap-1 text-muted font-medium mr-1 text-[11px]">
48 <Pin size={12} className="text-accent" />
49 <span>{t("pinnedFiles.shelfTitle")}</span>
50 </div>
51 {pinnedFiles.map((file) => {
52 const basename = file.path.split("/").pop() || file.path;
53 return (
54 <div
55 key={file.path}
56 onClick={() => handleOpenFile(file.path)}
57 className={`group flex items-center gap-1.5 px-2 py-0.5 rounded-full bg-surface hover:bg-surface-hover border text-foreground cursor-pointer transition-colors shadow-2xs ${file.error ? "border-red-500/60" : "border-border/60"}`}
58 title={file.error || `${file.path} (${file.sizeBytes} B · ~${file.tokenEstimate} tok)`}
59 >
60 {file.error ? (
61 <AlertTriangle size={11} className="text-red-500" aria-label={file.error} />
62 ) : (
63 <FileText size={11} className="text-muted-foreground group-hover:text-foreground transition-colors" />
64 )}
65 <span className="font-mono text-[11px] max-w-[160px] truncate">{basename}</span>
66 {file.tokenEstimate > 0 && (
67 <span className="text-[10px] text-muted-foreground">
68 ~{file.tokenEstimate}t
69 </span>
70 )}
71 <button
72 type="button"
73 onClick={(e) => handleUnpin(file.path, e)}
74 className="p-0.5 hover:bg-surface-raised rounded-full text-muted-foreground hover:text-foreground transition-colors"
75 aria-label={`${t("pinnedFiles.unpin")} ${basename}`}
76 >
77 <X size={11} />
78 </button>
79 </div>
80 );
81 })}
82 </div>
83 );
84 }
85
85 lines Plain Text