sonance-brand-mcp 1.3.110 → 1.3.112

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/dist/assets/api/sonance-ai-edit/route.ts +30 -7
  2. package/dist/assets/api/sonance-save-image/route.ts +625 -0
  3. package/dist/assets/api/sonance-vision-apply/image-styling-detection.ts +1360 -0
  4. package/dist/assets/api/sonance-vision-apply/route.ts +1020 -64
  5. package/dist/assets/api/sonance-vision-apply/styling-detection.ts +730 -0
  6. package/dist/assets/api/sonance-vision-apply/theme-discovery.ts +1 -1
  7. package/dist/assets/api/sonance-vision-edit/route.ts +33 -8
  8. package/dist/assets/brand-system.ts +13 -12
  9. package/dist/assets/components/accordion.tsx +15 -7
  10. package/dist/assets/components/alert-dialog.tsx +35 -10
  11. package/dist/assets/components/alert.tsx +11 -10
  12. package/dist/assets/components/avatar.tsx +4 -4
  13. package/dist/assets/components/badge.tsx +16 -12
  14. package/dist/assets/components/button.stories.tsx +3 -3
  15. package/dist/assets/components/button.tsx +50 -31
  16. package/dist/assets/components/calendar.tsx +12 -8
  17. package/dist/assets/components/card.tsx +35 -29
  18. package/dist/assets/components/checkbox.tsx +9 -8
  19. package/dist/assets/components/code.tsx +19 -11
  20. package/dist/assets/components/command.tsx +32 -13
  21. package/dist/assets/components/context-menu.tsx +37 -16
  22. package/dist/assets/components/dialog.tsx +8 -5
  23. package/dist/assets/components/divider.tsx +15 -5
  24. package/dist/assets/components/drawer.tsx +4 -3
  25. package/dist/assets/components/dropdown-menu.tsx +15 -13
  26. package/dist/assets/components/hover-card.tsx +4 -1
  27. package/dist/assets/components/image.tsx +1 -1
  28. package/dist/assets/components/input.tsx +29 -14
  29. package/dist/assets/components/kbd.stories.tsx +3 -3
  30. package/dist/assets/components/kbd.tsx +29 -13
  31. package/dist/assets/components/listbox.tsx +8 -8
  32. package/dist/assets/components/menubar.tsx +50 -23
  33. package/dist/assets/components/navbar.stories.tsx +140 -13
  34. package/dist/assets/components/navbar.tsx +22 -5
  35. package/dist/assets/components/navigation-menu.tsx +28 -6
  36. package/dist/assets/components/pagination.tsx +10 -10
  37. package/dist/assets/components/popover.tsx +10 -8
  38. package/dist/assets/components/progress.tsx +6 -4
  39. package/dist/assets/components/radio-group.tsx +5 -5
  40. package/dist/assets/components/select.tsx +49 -29
  41. package/dist/assets/components/separator.tsx +3 -3
  42. package/dist/assets/components/sheet.tsx +4 -4
  43. package/dist/assets/components/sidebar.tsx +10 -10
  44. package/dist/assets/components/skeleton.tsx +13 -5
  45. package/dist/assets/components/slider.tsx +12 -10
  46. package/dist/assets/components/switch.tsx +4 -4
  47. package/dist/assets/components/table.tsx +5 -5
  48. package/dist/assets/components/tabs.tsx +8 -8
  49. package/dist/assets/components/textarea.tsx +11 -9
  50. package/dist/assets/components/toast.tsx +7 -7
  51. package/dist/assets/components/toggle.tsx +27 -7
  52. package/dist/assets/components/tooltip.tsx +10 -8
  53. package/dist/assets/components/user.tsx +8 -6
  54. package/dist/assets/dev-tools/SonanceDevTools.tsx +851 -708
  55. package/dist/assets/dev-tools/components/ApplyFirstPreview.tsx +10 -10
  56. package/dist/assets/dev-tools/components/ChatHistory.tsx +145 -0
  57. package/dist/assets/dev-tools/components/ChatInterface.tsx +444 -295
  58. package/dist/assets/dev-tools/components/ChatTabBar.tsx +82 -0
  59. package/dist/assets/dev-tools/components/DiffPreview.tsx +1 -1
  60. package/dist/assets/dev-tools/components/InlineDiffPreview.tsx +528 -0
  61. package/dist/assets/dev-tools/components/InspectorOverlay.tsx +21 -18
  62. package/dist/assets/dev-tools/components/PropertiesPanel.tsx +1345 -0
  63. package/dist/assets/dev-tools/components/ScreenshotAnnotator.tsx +1 -1
  64. package/dist/assets/dev-tools/components/SectionHighlight.tsx +1 -1
  65. package/dist/assets/dev-tools/components/VisionDiffPreview.tsx +7 -7
  66. package/dist/assets/dev-tools/components/VisionModeBorder.tsx +12 -63
  67. package/dist/assets/dev-tools/constants.ts +38 -6
  68. package/dist/assets/dev-tools/hooks/index.ts +69 -0
  69. package/dist/assets/dev-tools/hooks/useComponentDetection.ts +132 -0
  70. package/dist/assets/dev-tools/hooks/useComputedStyles.ts +471 -0
  71. package/dist/assets/dev-tools/hooks/useContentHash.ts +212 -0
  72. package/dist/assets/dev-tools/hooks/useElementScanner.ts +398 -0
  73. package/dist/assets/dev-tools/hooks/useImageDetection.ts +162 -0
  74. package/dist/assets/dev-tools/hooks/useTextDetection.ts +217 -0
  75. package/dist/assets/dev-tools/index.ts +3 -0
  76. package/dist/assets/dev-tools/panels/AnalysisPanel.tsx +32 -32
  77. package/dist/assets/dev-tools/panels/ComponentsPanel.tsx +384 -131
  78. package/dist/assets/dev-tools/panels/TextPanel.tsx +10 -10
  79. package/dist/assets/dev-tools/types.ts +93 -2
  80. package/dist/assets/globals.css +225 -9
  81. package/dist/assets/styles/brand-overrides.css +3 -2
  82. package/dist/assets/utils.ts +2 -1
  83. package/dist/index.js +22 -3
  84. package/package.json +2 -1
@@ -0,0 +1,82 @@
1
+ "use client";
2
+
3
+ import React from "react";
4
+ import { Plus, X, MessageSquare } from "lucide-react";
5
+ import { cn } from "../../../lib/utils";
6
+ import { ChatSession } from "../types";
7
+
8
+ export interface ChatTabBarProps {
9
+ sessions: ChatSession[];
10
+ activeSessionId: string | null;
11
+ onSelectSession: (sessionId: string) => void;
12
+ onCreateSession: () => void;
13
+ onCloseSession: (sessionId: string) => void;
14
+ }
15
+
16
+ export function ChatTabBar({
17
+ sessions,
18
+ activeSessionId,
19
+ onSelectSession,
20
+ onCreateSession,
21
+ onCloseSession,
22
+ }: ChatTabBarProps) {
23
+ // Generate a short name for the session
24
+ const getSessionName = (session: ChatSession, index: number) => {
25
+ if (session.name && session.name !== "New Chat") {
26
+ return session.name.length > 12 ? session.name.slice(0, 12) + "..." : session.name;
27
+ }
28
+ return `Chat ${index + 1}`;
29
+ };
30
+
31
+ return (
32
+ <div className="flex items-center gap-0.5 px-3 py-1 bg-transparent border-b border-white/10 overflow-x-auto scrollbar-hide">
33
+ {/* Session Tabs - Cursor-style underline (always on dark bg) */}
34
+ {sessions.map((session, index) => {
35
+ const isActive = session.id === activeSessionId;
36
+ return (
37
+ <div
38
+ key={session.id}
39
+ className={cn(
40
+ "group relative flex items-center gap-1 px-2 py-1 text-[11px] cursor-pointer transition-all duration-150 bg-transparent",
41
+ isActive
42
+ ? "font-medium text-white after:absolute after:bottom-0 after:left-0.5 after:right-0.5 after:h-[2px] after:bg-[#00A3E1] after:rounded-full"
43
+ : "text-gray-400 hover:text-white"
44
+ )}
45
+ onClick={() => onSelectSession(session.id)}
46
+ >
47
+ <MessageSquare className="h-3 w-3 flex-shrink-0" />
48
+ <span id="chat-tab-bar-span-getsessionnamesessio" className="truncate max-w-[72px]">
49
+ {getSessionName(session, index)}
50
+ </span>
51
+ {/* Close button - only show if more than 1 session */}
52
+ {sessions.length > 1 && (
53
+ <button
54
+ onClick={(e) => {
55
+ e.stopPropagation();
56
+ onCloseSession(session.id);
57
+ }}
58
+ className={cn(
59
+ "p-0.5 rounded-sm hover:bg-white/10 transition-colors",
60
+ "opacity-0 group-hover:opacity-100",
61
+ isActive && "opacity-60 group-hover:opacity-100"
62
+ )}
63
+ >
64
+ <X className="h-2.5 w-2.5" />
65
+ </button>
66
+ )}
67
+ </div>
68
+ );
69
+ })}
70
+
71
+ {/* New Chat Button - minimal icon */}
72
+ <button
73
+ onClick={onCreateSession}
74
+ className="flex items-center justify-center p-1 text-gray-400 hover:text-white hover:bg-white/10 transition-colors ml-0.5"
75
+ title="New chat"
76
+ >
77
+ <Plus className="h-3 w-3" />
78
+ </button>
79
+ </div>
80
+ );
81
+ }
82
+
@@ -118,7 +118,7 @@ export function DiffPreview({
118
118
  {/* Live Preview Indicator */}
119
119
  <div className="flex items-center gap-2 p-2 rounded bg-[#00A3E1]/10 border border-[#00A3E1]/30">
120
120
  <Eye className="h-3.5 w-3.5 text-[#00A3E1]" />
121
- <span className="text-xs text-[#00A3E1]">
121
+ <span id="span-live-preview-active-" className="text-xs text-[#00A3E1]">
122
122
  Live preview active - scroll to see changes on the page
123
123
  </span>
124
124
  </div>
@@ -0,0 +1,528 @@
1
+ "use client";
2
+
3
+ import React, { useState, useMemo } from "react";
4
+ import {
5
+ ChevronDown,
6
+ ChevronRight,
7
+ Check,
8
+ RotateCcw,
9
+ FileCode,
10
+ CheckCircle,
11
+ XCircle,
12
+ Loader2,
13
+ AlertTriangle,
14
+ Info,
15
+ RefreshCw,
16
+ Plus,
17
+ Minus,
18
+ Code,
19
+ } from "lucide-react";
20
+ import { cn } from "../../../lib/utils";
21
+ import { ChatMessageAction, ApplyFirstStatus } from "../types";
22
+
23
+ export interface InlineDiffPreviewProps {
24
+ action: ChatMessageAction;
25
+ onAccept?: () => void;
26
+ onRevert?: () => void;
27
+ // Live status from parent (for HMR indicator, loading states)
28
+ liveStatus?: ApplyFirstStatus;
29
+ isProcessing?: boolean;
30
+ }
31
+
32
+ // Helper to refresh the page to see component changes
33
+ function handleRefreshPage() {
34
+ // Session is already persisted to localStorage, so it will survive the reload
35
+ window.location.reload();
36
+ }
37
+
38
+ /**
39
+ * Extract meaningful changes from a diff string
40
+ * Instead of showing full lines, extract what was actually modified
41
+ */
42
+ interface ChangeSummary {
43
+ type: 'added' | 'removed' | 'modified';
44
+ element?: string; // e.g., "Card", "Button"
45
+ change: string; // e.g., "rounded-none", "bg-blue-500"
46
+ fullLine?: string; // Original full line for tooltip
47
+ }
48
+
49
+ function extractChangeSummary(diff: string): ChangeSummary[] {
50
+ const lines = diff.split('\n');
51
+ const changes: ChangeSummary[] = [];
52
+
53
+ // Group consecutive - and + lines for comparison
54
+ const removedLines: string[] = [];
55
+ const addedLines: string[] = [];
56
+
57
+ for (const line of lines) {
58
+ if (line.startsWith('-') && !line.startsWith('---')) {
59
+ removedLines.push(line.substring(1).trim());
60
+ } else if (line.startsWith('+') && !line.startsWith('+++')) {
61
+ addedLines.push(line.substring(1).trim());
62
+ }
63
+ }
64
+
65
+ // If we have both removed and added lines, find the differences
66
+ if (removedLines.length > 0 && addedLines.length > 0) {
67
+ for (let i = 0; i < Math.max(removedLines.length, addedLines.length); i++) {
68
+ const removed = removedLines[i] || '';
69
+ const added = addedLines[i] || '';
70
+
71
+ if (removed && added) {
72
+ // Find what changed between the lines
73
+ const diff = findLineDifference(removed, added);
74
+ if (diff) {
75
+ changes.push({
76
+ type: 'modified',
77
+ element: diff.element,
78
+ change: diff.change,
79
+ fullLine: added
80
+ });
81
+ }
82
+ } else if (added && !removed) {
83
+ changes.push({
84
+ type: 'added',
85
+ change: truncateChange(added),
86
+ fullLine: added
87
+ });
88
+ } else if (removed && !added) {
89
+ changes.push({
90
+ type: 'removed',
91
+ change: truncateChange(removed),
92
+ fullLine: removed
93
+ });
94
+ }
95
+ }
96
+ } else if (addedLines.length > 0) {
97
+ // Only additions
98
+ for (const line of addedLines) {
99
+ changes.push({
100
+ type: 'added',
101
+ change: truncateChange(line),
102
+ fullLine: line
103
+ });
104
+ }
105
+ } else if (removedLines.length > 0) {
106
+ // Only removals
107
+ for (const line of removedLines) {
108
+ changes.push({
109
+ type: 'removed',
110
+ change: truncateChange(line),
111
+ fullLine: line
112
+ });
113
+ }
114
+ }
115
+
116
+ return changes;
117
+ }
118
+
119
+ /**
120
+ * Find the actual difference between two similar lines
121
+ */
122
+ function findLineDifference(removed: string, added: string): { element?: string; change: string } | null {
123
+ // Try to find className differences
124
+ const classNameRegex = /className=["']([^"']+)["']/;
125
+ const removedClass = removed.match(classNameRegex);
126
+ const addedClass = added.match(classNameRegex);
127
+
128
+ if (removedClass && addedClass) {
129
+ const removedClasses = new Set(removedClass[1].split(/\s+/));
130
+ const addedClasses = new Set(addedClass[1].split(/\s+/));
131
+
132
+ // Find added classes
133
+ const newClasses = [...addedClasses].filter(c => !removedClasses.has(c));
134
+ // Find removed classes
135
+ const deletedClasses = [...removedClasses].filter(c => !addedClasses.has(c));
136
+
137
+ if (newClasses.length > 0 || deletedClasses.length > 0) {
138
+ // Try to extract element name (e.g., <Card, <Button)
139
+ const elementMatch = added.match(/<(\w+)/);
140
+ const element = elementMatch ? elementMatch[1] : undefined;
141
+
142
+ const changes: string[] = [];
143
+ if (newClasses.length > 0) {
144
+ changes.push(`+${newClasses.join(' ')}`);
145
+ }
146
+ if (deletedClasses.length > 0) {
147
+ changes.push(`-${deletedClasses.join(' ')}`);
148
+ }
149
+
150
+ return { element, change: changes.join(', ') };
151
+ }
152
+ }
153
+
154
+ // Try to find text content differences
155
+ const textRegex = />([^<]+)</;
156
+ const removedText = removed.match(textRegex);
157
+ const addedText = added.match(textRegex);
158
+
159
+ if (removedText && addedText && removedText[1] !== addedText[1]) {
160
+ const elementMatch = added.match(/<(\w+)/);
161
+ return {
162
+ element: elementMatch ? elementMatch[1] : undefined,
163
+ change: `"${addedText[1].substring(0, 30)}${addedText[1].length > 30 ? '...' : ''}"`
164
+ };
165
+ }
166
+
167
+ // Generic difference - show a summary
168
+ if (removed !== added) {
169
+ const elementMatch = added.match(/<(\w+)/);
170
+ return {
171
+ element: elementMatch ? elementMatch[1] : undefined,
172
+ change: 'modified'
173
+ };
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * Truncate a change for display
181
+ */
182
+ function truncateChange(line: string): string {
183
+ // Try to extract just the meaningful part
184
+ const elementMatch = line.match(/<(\w+)/);
185
+ if (elementMatch) {
186
+ return `<${elementMatch[1]}...>`;
187
+ }
188
+ return line.length > 40 ? line.substring(0, 40) + '...' : line;
189
+ }
190
+
191
+ export function InlineDiffPreview({
192
+ action,
193
+ onAccept,
194
+ onRevert,
195
+ liveStatus,
196
+ isProcessing = false,
197
+ }: InlineDiffPreviewProps) {
198
+ const [expandedFiles, setExpandedFiles] = useState<Set<string>>(
199
+ // Expand first file by default
200
+ new Set(action.files?.slice(0, 1).map((f) => f.path) || [])
201
+ );
202
+ // Track which files show full diff vs summary
203
+ const [showFullDiff, setShowFullDiff] = useState<Set<string>>(new Set());
204
+
205
+ const toggleFile = (path: string) => {
206
+ setExpandedFiles((prev) => {
207
+ const next = new Set(prev);
208
+ if (next.has(path)) {
209
+ next.delete(path);
210
+ } else {
211
+ next.add(path);
212
+ }
213
+ return next;
214
+ });
215
+ };
216
+
217
+ const toggleFullDiff = (path: string) => {
218
+ setShowFullDiff((prev) => {
219
+ const next = new Set(prev);
220
+ if (next.has(path)) {
221
+ next.delete(path);
222
+ } else {
223
+ next.add(path);
224
+ }
225
+ return next;
226
+ });
227
+ };
228
+
229
+ // Pre-compute change summaries for all files
230
+ const fileSummaries = useMemo(() => {
231
+ const summaries: Record<string, ChangeSummary[]> = {};
232
+ for (const file of action.files || []) {
233
+ if (file.diff) {
234
+ summaries[file.path] = extractChangeSummary(file.diff);
235
+ }
236
+ }
237
+ return summaries;
238
+ }, [action.files]);
239
+
240
+ // Derive loading state from liveStatus or isProcessing
241
+ const isLoading = isProcessing || liveStatus === "accepting" || liveStatus === "reverting";
242
+ const isWaitingHMR = liveStatus === "waiting-hmr";
243
+ const isLive = liveStatus === "reviewing" || action.status === "pending";
244
+
245
+ const getStatusColor = () => {
246
+ switch (action.status) {
247
+ case "pending":
248
+ return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
249
+ case "accepted":
250
+ return "border-green-300 dark:border-green-600 bg-green-50 dark:bg-green-900/20";
251
+ case "reverted":
252
+ return "border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800/50";
253
+ case "error":
254
+ return "border-red-300 dark:border-red-600 bg-red-50 dark:bg-red-900/20";
255
+ default:
256
+ return "border-border bg-background";
257
+ }
258
+ };
259
+
260
+ const getStatusBadge = () => {
261
+ // Show HMR status first if applicable
262
+ if (isWaitingHMR) {
263
+ return (
264
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400 rounded-full">
265
+ <Loader2 className="h-3 w-3 animate-spin" />
266
+ Refreshing...
267
+ </span>
268
+ );
269
+ }
270
+
271
+ switch (action.status) {
272
+ case "pending":
273
+ return (
274
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 rounded-full">
275
+ <Check className="h-3 w-3" />
276
+ Changes Live
277
+ </span>
278
+ );
279
+ case "accepted":
280
+ return (
281
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400 rounded-full">
282
+ <CheckCircle className="h-3 w-3" />
283
+ Accepted
284
+ </span>
285
+ );
286
+ case "reverted":
287
+ return (
288
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 rounded-full">
289
+ <XCircle className="h-3 w-3" />
290
+ Reverted
291
+ </span>
292
+ );
293
+ case "error":
294
+ return (
295
+ <span className="inline-flex items-center gap-1 px-2 py-0.5 text-[10px] font-medium bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400 rounded-full">
296
+ <XCircle className="h-3 w-3" />
297
+ Error
298
+ </span>
299
+ );
300
+ default:
301
+ return null;
302
+ }
303
+ };
304
+
305
+ // Parse diff into lines with proper styling
306
+ const renderDiff = (diff: string) => {
307
+ const lines = diff.split("\n");
308
+ return (
309
+ <pre className="text-[11px] font-mono leading-relaxed overflow-x-auto">
310
+ {lines.map((line, i) => {
311
+ let lineClass = "text-foreground-secondary";
312
+ let bgClass = "";
313
+
314
+ if (line.startsWith("+") && !line.startsWith("+++")) {
315
+ lineClass = "text-green-700 dark:text-green-400";
316
+ bgClass = "bg-green-50 dark:bg-green-900/30";
317
+ } else if (line.startsWith("-") && !line.startsWith("---")) {
318
+ lineClass = "text-red-700 dark:text-red-400";
319
+ bgClass = "bg-red-50 dark:bg-red-900/30";
320
+ } else if (line.startsWith("@@")) {
321
+ lineClass = "text-blue-600 dark:text-blue-400";
322
+ bgClass = "bg-blue-50 dark:bg-blue-900/30";
323
+ }
324
+
325
+ return (
326
+ <div key={i} className={cn("px-2 py-0.5", bgClass)}>
327
+ <span id="inline-diff-preview-span-line" className={lineClass}>{line}</span>
328
+ </div>
329
+ );
330
+ })}
331
+ </pre>
332
+ );
333
+ };
334
+
335
+ return (
336
+ <div className={cn("rounded-lg border overflow-hidden", getStatusColor())}>
337
+ {/* Header */}
338
+ <div className="flex items-center justify-between px-3 py-2 border-b border-inherit bg-background/50">
339
+ <div className="flex items-center gap-2">
340
+ <FileCode className="h-4 w-4 text-foreground-secondary" />
341
+ <span className="text-xs font-medium text-foreground">
342
+ {action.files?.length || 0} file{(action.files?.length || 0) !== 1 ? "s" : ""} changed
343
+ </span>
344
+ {getStatusBadge()}
345
+ </div>
346
+ </div>
347
+
348
+ {/* Info Banner - Show when changes are live */}
349
+ {action.status === "pending" && (
350
+ <div className="flex items-start justify-between gap-2 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 border-b border-inherit">
351
+ <div className="flex items-start gap-2">
352
+ <Info className="h-3.5 w-3.5 text-blue-600 dark:text-blue-400 mt-0.5 flex-shrink-0" />
353
+ <span className="text-[11px] text-blue-700 dark:text-blue-300">
354
+ <strong>Changes saved!</strong> If you don&apos;t see them, click Refresh.
355
+ </span>
356
+ </div>
357
+ <button
358
+ onClick={handleRefreshPage}
359
+ className="flex items-center gap-1 px-2 py-1 text-[10px] font-medium text-blue-700 dark:text-blue-300 bg-blue-100 dark:bg-blue-800/50 hover:bg-blue-200 dark:hover:bg-blue-700/50 rounded transition-colors flex-shrink-0"
360
+ >
361
+ <RefreshCw className="h-3 w-3" />
362
+ Refresh
363
+ </button>
364
+ </div>
365
+ )}
366
+
367
+ {/* File List */}
368
+ <div className="divide-y divide-inherit">
369
+ {action.files?.map((file) => {
370
+ const isExpanded = expandedFiles.has(file.path);
371
+ const isShowingFullDiff = showFullDiff.has(file.path);
372
+ const fileName = file.path.split("/").pop() || file.path;
373
+ const summary = fileSummaries[file.path] || [];
374
+
375
+ return (
376
+ <div key={file.path}>
377
+ {/* File Header */}
378
+ <button
379
+ onClick={() => toggleFile(file.path)}
380
+ className="w-full flex items-center gap-2 px-3 py-2 text-left hover:bg-background/50 transition-colors"
381
+ >
382
+ {isExpanded ? (
383
+ <ChevronDown className="h-3.5 w-3.5 text-foreground-muted" />
384
+ ) : (
385
+ <ChevronRight className="h-3.5 w-3.5 text-foreground-muted" />
386
+ )}
387
+ <span className="text-[11px] font-mono text-foreground truncate">
388
+ {fileName}
389
+ </span>
390
+ <span className="text-[10px] text-foreground-muted truncate flex-1">
391
+ {file.path}
392
+ </span>
393
+ </button>
394
+
395
+ {/* Change Summary (default view) */}
396
+ {isExpanded && file.diff && !isShowingFullDiff && (
397
+ <div className="bg-background border-t border-inherit px-3 py-2 space-y-1">
398
+ {summary.length > 0 ? (
399
+ <>
400
+ {summary.map((change, i) => (
401
+ <div
402
+ key={i}
403
+ className="flex items-center gap-2 text-[11px]"
404
+ title={change.fullLine}
405
+ >
406
+ {change.type === 'added' || change.type === 'modified' ? (
407
+ <Plus className="h-3 w-3 text-green-600 dark:text-green-400 flex-shrink-0" />
408
+ ) : (
409
+ <Minus className="h-3 w-3 text-red-600 dark:text-red-400 flex-shrink-0" />
410
+ )}
411
+ {change.element && (
412
+ <span className="font-mono text-blue-600 dark:text-blue-400 flex-shrink-0">
413
+ {change.element}:
414
+ </span>
415
+ )}
416
+ <span className={cn(
417
+ "font-mono",
418
+ change.type === 'removed'
419
+ ? "text-red-600 dark:text-red-400"
420
+ : "text-green-600 dark:text-green-400"
421
+ )}>
422
+ {change.change}
423
+ </span>
424
+ </div>
425
+ ))}
426
+ {/* Toggle to show full diff */}
427
+ <button
428
+ onClick={(e) => {
429
+ e.stopPropagation();
430
+ toggleFullDiff(file.path);
431
+ }}
432
+ className="flex items-center gap-1 mt-2 text-[10px] text-foreground-muted hover:text-foreground transition-colors"
433
+ >
434
+ <Code className="h-3 w-3" />
435
+ Show full diff
436
+ </button>
437
+ </>
438
+ ) : (
439
+ <span className="text-[11px] text-foreground-muted">No significant changes detected</span>
440
+ )}
441
+ </div>
442
+ )}
443
+
444
+ {/* Full Diff Content (toggle view) */}
445
+ {isExpanded && file.diff && isShowingFullDiff && (
446
+ <div className="bg-background border-t border-inherit">
447
+ <div className="overflow-x-auto">
448
+ {renderDiff(file.diff)}
449
+ </div>
450
+ {/* Toggle back to summary */}
451
+ <button
452
+ onClick={(e) => {
453
+ e.stopPropagation();
454
+ toggleFullDiff(file.path);
455
+ }}
456
+ className="flex items-center gap-1 px-3 py-2 text-[10px] text-foreground-muted hover:text-foreground transition-colors border-t border-inherit w-full"
457
+ >
458
+ <ChevronRight className="h-3 w-3" />
459
+ Show summary
460
+ </button>
461
+ </div>
462
+ )}
463
+ </div>
464
+ );
465
+ })}
466
+ </div>
467
+
468
+ {/* Warning about navigation - only show for pending */}
469
+ {action.status === "pending" && (
470
+ <div className="flex items-start gap-2 px-3 py-2 bg-amber-50 dark:bg-amber-900/20 border-t border-inherit">
471
+ <AlertTriangle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 mt-0.5 flex-shrink-0" />
472
+ <span className="text-[11px] text-amber-700 dark:text-amber-300">
473
+ Navigating away will automatically revert changes. Make sure to accept or revert first.
474
+ </span>
475
+ </div>
476
+ )}
477
+
478
+ {/* Action Buttons - only show for pending */}
479
+ {action.status === "pending" && (
480
+ <div className="flex gap-2 px-3 py-2 border-t border-inherit bg-background/50">
481
+ <button
482
+ onClick={onAccept}
483
+ disabled={isLoading}
484
+ className={cn(
485
+ "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-[11px] font-medium rounded-md transition-colors",
486
+ "bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-600",
487
+ "disabled:opacity-50 disabled:cursor-not-allowed"
488
+ )}
489
+ >
490
+ {liveStatus === "accepting" ? (
491
+ <>
492
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
493
+ Accepting...
494
+ </>
495
+ ) : (
496
+ <>
497
+ <Check className="h-3.5 w-3.5" />
498
+ Keep Changes
499
+ </>
500
+ )}
501
+ </button>
502
+ <button
503
+ onClick={onRevert}
504
+ disabled={isLoading}
505
+ className={cn(
506
+ "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-[11px] font-medium rounded-md transition-colors",
507
+ "bg-background border border-border text-foreground-secondary hover:bg-secondary",
508
+ "disabled:opacity-50 disabled:cursor-not-allowed"
509
+ )}
510
+ >
511
+ {liveStatus === "reverting" ? (
512
+ <>
513
+ <Loader2 className="h-3.5 w-3.5 animate-spin" />
514
+ Reverting...
515
+ </>
516
+ ) : (
517
+ <>
518
+ <RotateCcw className="h-3.5 w-3.5" />
519
+ Revert
520
+ </>
521
+ )}
522
+ </button>
523
+ </div>
524
+ )}
525
+ </div>
526
+ );
527
+ }
528
+