File size: 6,524 Bytes
f59fbe2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import { useState } from "react";
import { IconFilePlus, IconFileMinus, IconFileX, IconArrowBackUp, IconCheck, IconGitCompare } from "@tabler/icons-react";
import { Button } from "@/components/ui/button";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import { useChatStore } from "@/stores";
import { bridge } from "@/services";
import { cn } from "@/lib/utils";
import { FileChange } from "shared/types";
import { toast } from "./ui/sonner";

const STATUS_CONFIG = {
  Added: { icon: IconFilePlus, color: "text-green-600 dark:text-green-400" },
  Deleted: { icon: IconFileX, color: "text-red-600 dark:text-red-400" },
  Modified: { icon: IconFileMinus, color: "text-yellow-600 dark:text-yellow-400" },
} as const;

function getTotalStats(changes: FileChange[]) {
  return changes.reduce(
    (a, c) => ({
      additions: a.additions + c.additions,
      deletions: a.deletions + c.deletions,
    }),
    { additions: 0, deletions: 0 },
  );
}

interface FileItemProps {
  file: FileChange;
  onRevert: () => void;
  onKeep: () => void;
  onViewDiff: () => void;
  disabled: boolean;
  isStreaming?: boolean;
}

function FileItem({ file, onRevert, onKeep, onViewDiff, disabled, isStreaming }: FileItemProps) {
  const { icon: Icon, color } = STATUS_CONFIG[file.status];
  const name = file.path.split("/").pop() || file.path;
  const dir = file.path.includes("/") ? file.path.slice(0, file.path.lastIndexOf("/")) : "";

  return (
    <div className="group flex items-center gap-1.5 px-2.5 py-1.5 hover:bg-accent/50 text-xs">
      <Icon className={cn("size-3.5 shrink-0", color)} />
      <div className="flex-1 min-w-0 text-left truncate hover:underline cursor-pointer" onClick={onViewDiff} title={file.path}>
        <span className="font-medium">{name}</span>
        {dir && <span className="ml-1 text-muted-foreground text-[10px]">{dir}</span>}
      </div>
      <div className="flex items-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity">
        <Tooltip>
          <TooltipTrigger asChild>
            <Button variant="ghost" size="icon" className="size-5 border-0! cursor-pointer hover:bg-accent" onClick={onViewDiff}>
              <IconGitCompare className="size-3" />
            </Button>
          </TooltipTrigger>
          <TooltipContent>View Changes</TooltipContent>
        </Tooltip>
        {!isStreaming && (
          <>
            <Tooltip>
              <TooltipTrigger asChild>
                <Button variant="ghost" size="icon" className="size-5 border-0! cursor-pointer hover:bg-accent" onClick={onRevert} disabled={disabled}>
                  <IconArrowBackUp className="size-3" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Undo Changes</TooltipContent>
            </Tooltip>
            <Tooltip>
              <TooltipTrigger asChild>
                <Button variant="ghost" size="icon" className="size-5 border-0! cursor-pointer hover:bg-accent" onClick={onKeep} disabled={disabled}>
                  <IconCheck className="size-3" />
                </Button>
              </TooltipTrigger>
              <TooltipContent>Keep Changes</TooltipContent>
            </Tooltip>
          </>
        )}
      </div>
      <div className="flex items-center gap-1 text-[10px] tabular-nums shrink-0">
        <span className="text-green-600 dark:text-green-400">+{file.additions}</span>
        <span className="text-red-600 dark:text-red-400">-{file.deletions}</span>
      </div>
    </div>
  );
}

interface FileChangesPanelProps {
  changes: FileChange[];
}

export function FileChangesPanel({ changes }: FileChangesPanelProps) {
  const { isStreaming } = useChatStore();
  const [loading, setLoading] = useState(false);

  const handleRevert = async (filePath?: string) => {
    setLoading(true);
    try {
      await bridge.revertFiles(filePath);
    } catch (error) {
      toast.error(`Unable to undo changes: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setLoading(false);
    }
  };

  const handleKeep = async (filePath?: string) => {
    setLoading(true);
    try {
      await bridge.keepChanges(filePath);
    } catch (error) {
      toast.error(`Unable to keep changes: ${error instanceof Error ? error.message : String(error)}`);
    } finally {
      setLoading(false);
    }
  };

  const stats = getTotalStats(changes);

  if (!changes.length) {
    return <div className="px-2.5 py-3 text-xs text-muted-foreground text-center">No file changes</div>;
  }

  return (
    <div className="flex flex-col bg-card shrink">
      {/* Header with actions */}
      <div className="flex items-center justify-between px-2.5 py-1.5 text-xs bg-muted/30">
        <div className="flex items-center gap-2">
          <span className="text-muted-foreground">
            {changes.length} file{changes.length !== 1 ? "s" : ""}
          </span>
          <div className="flex items-center gap-1 text-[10px] tabular-nums">
            <span className="text-green-600 dark:text-green-400">+{stats.additions}</span>
            <span className="text-red-600 dark:text-red-400">-{stats.deletions}</span>
          </div>
        </div>
        {!isStreaming && (
          <div className="flex gap-1">
            <Button
              variant="default"
              size="sm"
              className="h-5 px-2 text-[10px] cursor-pointer"
              onClick={() => {
                void handleKeep();
              }}
              disabled={loading}
            >
              Keep All
            </Button>
            <Button
              variant="secondary"
              size="sm"
              className="h-5 px-2 text-[10px] cursor-pointer"
              onClick={() => {
                void handleRevert();
              }}
              disabled={loading}
            >
              Undo All
            </Button>
          </div>
        )}
      </div>

      {/* File list */}
      <div className="overflow-y-auto flex-1">
        {changes.map((file) => (
          <FileItem
            key={file.path}
            file={file}
            onRevert={() => {
              void handleRevert(file.path);
            }}
            onKeep={() => {
              void handleKeep(file.path);
            }}
            onViewDiff={() => {
              void bridge.openFileDiff(file.path);
            }}
            disabled={loading}
            isStreaming={isStreaming}
          />
        ))}
      </div>
    </div>
  );
}