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