@myrialabs/clopen 0.2.3 → 0.2.5

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 (53) hide show
  1. package/backend/engine/adapters/claude/stream.ts +107 -0
  2. package/backend/engine/adapters/opencode/message-converter.ts +37 -2
  3. package/backend/engine/adapters/opencode/stream.ts +81 -1
  4. package/backend/engine/types.ts +17 -0
  5. package/backend/git/git-service.ts +2 -1
  6. package/backend/ws/git/commit-message.ts +108 -0
  7. package/backend/ws/git/index.ts +3 -1
  8. package/backend/ws/system/index.ts +7 -1
  9. package/backend/ws/system/operations.ts +28 -2
  10. package/backend/ws/user/crud.ts +6 -3
  11. package/frontend/App.svelte +3 -0
  12. package/frontend/components/auth/SetupPage.svelte +2 -2
  13. package/frontend/components/chat/input/ChatInput.svelte +1 -1
  14. package/frontend/components/chat/message/ChatMessage.svelte +64 -16
  15. package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
  16. package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
  17. package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
  18. package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
  19. package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
  20. package/frontend/components/common/feedback/UpdateBanner.svelte +17 -6
  21. package/frontend/components/common/media/MediaPreview.svelte +187 -0
  22. package/frontend/components/files/FileViewer.svelte +11 -143
  23. package/frontend/components/git/BranchManager.svelte +143 -155
  24. package/frontend/components/git/CommitForm.svelte +61 -11
  25. package/frontend/components/git/DiffViewer.svelte +50 -130
  26. package/frontend/components/git/FileChangeItem.svelte +22 -0
  27. package/frontend/components/settings/SettingsModal.svelte +1 -1
  28. package/frontend/components/settings/SettingsView.svelte +1 -1
  29. package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
  30. package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
  31. package/frontend/components/settings/git/GitSettings.svelte +392 -0
  32. package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
  33. package/frontend/components/settings/model/ModelSettings.svelte +172 -289
  34. package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
  35. package/frontend/components/workspace/PanelHeader.svelte +1 -3
  36. package/frontend/components/workspace/WorkspaceLayout.svelte +14 -2
  37. package/frontend/components/workspace/panels/FilesPanel.svelte +76 -1
  38. package/frontend/components/workspace/panels/GitPanel.svelte +84 -33
  39. package/frontend/main.ts +4 -0
  40. package/frontend/stores/core/files.svelte.ts +15 -1
  41. package/frontend/stores/features/settings.svelte.ts +13 -2
  42. package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
  43. package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
  44. package/frontend/stores/ui/update.svelte.ts +45 -4
  45. package/frontend/utils/file-type.ts +68 -0
  46. package/index.html +1 -0
  47. package/package.json +1 -1
  48. package/shared/constants/binary-extensions.ts +40 -0
  49. package/shared/types/git.ts +15 -0
  50. package/shared/types/messaging/tool.ts +1 -0
  51. package/shared/types/stores/settings.ts +12 -0
  52. package/shared/utils/file-type-detection.ts +9 -1
  53. package/static/manifest.json +16 -0
@@ -24,7 +24,9 @@
24
24
  import TokenUsageModal from '../modal/TokenUsageModal.svelte';
25
25
  import DebugModal from '../modal/DebugModal.svelte';
26
26
  import Dialog from '$frontend/components/common/overlay/Dialog.svelte';
27
- import ws from '$frontend/utils/ws';
27
+ import ConflictResolutionModal from '$frontend/components/checkpoint/ConflictResolutionModal.svelte';
28
+ import { snapshotService } from '$frontend/services/snapshot/snapshot.service';
29
+ import type { RestoreConflict, ConflictResolution } from '$frontend/services/snapshot/snapshot.service';
28
30
 
29
31
  const {
30
32
  message,
@@ -39,6 +41,11 @@
39
41
  let showTokenUsagePopup = $state(false);
40
42
  let showRestoreConfirm = $state(false);
41
43
 
44
+ // Conflict resolution state
45
+ let showConflictModal = $state(false);
46
+ let conflictList = $state<RestoreConflict[]>([]);
47
+ let processingRestore = $state(false);
48
+
42
49
  // Format timestamp
43
50
  const formatTime = (timestamp?: string) => {
44
51
  if (!timestamp) return 'Unknown';
@@ -268,15 +275,8 @@
268
275
  showDebugPopup = false;
269
276
  }
270
277
 
271
- // Handle restore button click
278
+ // Handle restore button click - check conflicts first
272
279
  async function handleRestore() {
273
- showRestoreConfirm = true;
274
- }
275
-
276
- // Confirm restore action
277
- async function confirmRestore() {
278
- showRestoreConfirm = false;
279
-
280
280
  if (!messageId) {
281
281
  addNotification({
282
282
  type: 'error',
@@ -287,16 +287,41 @@
287
287
  return;
288
288
  }
289
289
 
290
+ const currentSessionId = sessionState.currentSession?.id;
291
+ if (!currentSessionId) return;
292
+
290
293
  try {
291
- // Send restore request via WebSocket HTTP
292
- await ws.http('snapshot:restore', {
294
+ const conflictCheck = await snapshotService.checkConflicts(messageId, currentSessionId);
295
+
296
+ if (conflictCheck.hasConflicts) {
297
+ // Show conflict resolution modal
298
+ conflictList = conflictCheck.conflicts;
299
+ showConflictModal = true;
300
+ } else {
301
+ // No conflicts - show simple confirmation
302
+ showRestoreConfirm = true;
303
+ }
304
+ } catch (error) {
305
+ debug.error('chat', 'Error checking conflicts:', error);
306
+ // Fallback to simple confirmation
307
+ showRestoreConfirm = true;
308
+ }
309
+ }
310
+
311
+ // Execute restore with optional conflict resolutions
312
+ async function executeRestore(resolutions?: ConflictResolution) {
313
+ if (!messageId || !sessionState.currentSession?.id) return;
314
+
315
+ processingRestore = true;
316
+
317
+ try {
318
+ await snapshotService.restore(
293
319
  messageId,
294
- sessionId: sessionState.currentSession?.id || ''
295
- });
320
+ sessionState.currentSession.id,
321
+ resolutions
322
+ );
296
323
 
297
- if (sessionState.currentSession?.id) {
298
- await loadMessagesForSession(sessionState.currentSession.id);
299
- }
324
+ await loadMessagesForSession(sessionState.currentSession.id);
300
325
  } catch (error) {
301
326
  debug.error('chat', 'Restore error:', error);
302
327
  addNotification({
@@ -305,9 +330,22 @@
305
330
  message: error instanceof Error ? error.message : 'Unknown error',
306
331
  duration: 5000
307
332
  });
333
+ } finally {
334
+ processingRestore = false;
308
335
  }
309
336
  }
310
337
 
338
+ // Confirm simple restore (no conflicts)
339
+ async function confirmRestore() {
340
+ showRestoreConfirm = false;
341
+ await executeRestore();
342
+ }
343
+
344
+ // Confirm restore with conflict resolutions
345
+ async function confirmConflictRestore(resolutions: ConflictResolution) {
346
+ await executeRestore(resolutions);
347
+ }
348
+
311
349
  // Handle edit button click
312
350
  async function handleEdit() {
313
351
  if (!messageId) {
@@ -473,6 +511,16 @@ This will restore your conversation to this point.`}
473
511
  }}
474
512
  />
475
513
 
514
+ <!-- Conflict Resolution Modal -->
515
+ <ConflictResolutionModal
516
+ bind:isOpen={showConflictModal}
517
+ conflicts={conflictList}
518
+ onConfirm={confirmConflictRestore}
519
+ onClose={() => {
520
+ conflictList = [];
521
+ }}
522
+ />
523
+
476
524
  <style>
477
525
  /* Animate chevron icon when details are opened */
478
526
  :global(details[open] summary .iconify) {
@@ -3,6 +3,15 @@
3
3
  import type { IconName } from '$shared/types/ui/icons';
4
4
  import { getFileIcon } from '$frontend/utils/file-icon-mappings';
5
5
  import { formatPath } from '../../shared/utils';
6
+ import { requestRevealFile } from '$frontend/stores/core/files.svelte';
7
+ import { getVisiblePanels, workspaceState } from '$frontend/stores/ui/workspace.svelte';
8
+
9
+ function handleClick() {
10
+ const visiblePanels = getVisiblePanels(workspaceState.layout);
11
+ if (visiblePanels.includes('files')) {
12
+ requestRevealFile(filePath);
13
+ }
14
+ }
6
15
 
7
16
  interface Props {
8
17
  filePath: string;
@@ -18,10 +27,15 @@
18
27
  </script>
19
28
 
20
29
  <div class={box ? "bg-white dark:bg-slate-800 rounded-md border border-slate-200/60 dark:border-slate-700/60 p-3" : ""}>
21
- <div class="flex items-center gap-3 mb-1">
22
- <Icon
23
- name={getFileIcon(displayFileName)}
24
- class="w-6 h-6 {iconColor || ''}"
30
+ <button
31
+ type="button"
32
+ class="flex items-center gap-3 mb-1 w-full text-left hover:opacity-80 transition-opacity cursor-pointer"
33
+ onclick={handleClick}
34
+ title="Reveal in Files panel"
35
+ >
36
+ <Icon
37
+ name={getFileIcon(displayFileName)}
38
+ class="w-6 h-6 {iconColor || ''}"
25
39
  />
26
40
  <div class="flex-1 min-w-0">
27
41
  <h3 class="font-medium text-slate-900 dark:text-slate-100 truncate">
@@ -31,7 +45,7 @@
31
45
  {formatPath(filePath)}
32
46
  </p>
33
47
  </div>
34
- </div>
48
+ </button>
35
49
 
36
50
  {#if badges.length > 0}
37
51
  <div class="flex gap-2 mt-3">
@@ -8,17 +8,14 @@
8
8
  <script lang="ts">
9
9
  import { sessionState } from '$frontend/stores/core/sessions.svelte';
10
10
  import { appState } from '$frontend/stores/core/app.svelte';
11
+ import { todoPanelState, saveTodoPanelState } from '$frontend/stores/ui/todo-panel.svelte';
11
12
  import Icon from '$frontend/components/common/display/Icon.svelte';
12
13
  import { fly } from 'svelte/transition';
13
14
  import type { TodoWriteToolInput } from '$shared/types/messaging';
14
15
 
15
- let isExpanded = $state(true);
16
- let isMinimized = $state(false);
17
-
18
- // Drag & snap state
19
- let posY = $state(80);
16
+ // Drag-only local state (posX is always transient, posY syncs to store on drop)
17
+ let posY = $state(todoPanelState.posY);
20
18
  let posX = $state(0);
21
- let snapSide = $state<'left' | 'right'>('right');
22
19
  let isDragging = $state(false);
23
20
 
24
21
  // Minimized button ref for measuring width at snap time
@@ -28,18 +25,18 @@
28
25
  let _sx = 0, _sy = 0, _mx = 0, _my = 0, _hasDragged = false;
29
26
 
30
27
  function getPanelWidth() {
31
- return isExpanded ? 330 : 230;
28
+ return todoPanelState.isExpanded ? 330 : 230;
32
29
  }
33
30
 
34
31
  // Always use `left` property so CSS can transition in both directions
35
32
  const panelDisplayLeft = $derived(
36
- isDragging ? posX : snapSide === 'right' ? window.innerWidth - getPanelWidth() - 16 : 16
33
+ isDragging ? posX : todoPanelState.snapSide === 'right' ? window.innerWidth - getPanelWidth() - 16 : 16
37
34
  );
38
35
 
39
36
  const minimizedDisplayLeft = $derived(
40
37
  isDragging
41
38
  ? posX
42
- : snapSide === 'right'
39
+ : todoPanelState.snapSide === 'right'
43
40
  ? window.innerWidth - (minimizedBtn?.offsetWidth ?? 90) - 16
44
41
  : 16
45
42
  );
@@ -69,7 +66,9 @@
69
66
  function endDrag(e: PointerEvent) {
70
67
  if (!isDragging) return;
71
68
  isDragging = false;
72
- snapSide = posX + getPanelWidth() / 2 < window.innerWidth / 2 ? 'left' : 'right';
69
+ todoPanelState.snapSide = posX + getPanelWidth() / 2 < window.innerWidth / 2 ? 'left' : 'right';
70
+ todoPanelState.posY = posY;
71
+ saveTodoPanelState();
73
72
  }
74
73
 
75
74
  // --- Minimized button drag (click = restore, drag = move) ---
@@ -103,7 +102,9 @@
103
102
  return;
104
103
  }
105
104
  const el = e.currentTarget as HTMLElement;
106
- snapSide = posX + el.offsetWidth / 2 < window.innerWidth / 2 ? 'left' : 'right';
105
+ todoPanelState.snapSide = posX + el.offsetWidth / 2 < window.innerWidth / 2 ? 'left' : 'right';
106
+ todoPanelState.posY = posY;
107
+ saveTodoPanelState();
107
108
  }
108
109
 
109
110
  // Extract the latest TodoWrite data from messages
@@ -151,19 +152,22 @@
151
152
  const shouldShow = $derived(latestTodos !== null && latestTodos.length > 0);
152
153
 
153
154
  function toggleExpand() {
154
- if (!isMinimized) {
155
- isExpanded = !isExpanded;
155
+ if (!todoPanelState.isMinimized) {
156
+ todoPanelState.isExpanded = !todoPanelState.isExpanded;
157
+ saveTodoPanelState();
156
158
  }
157
159
  }
158
160
 
159
161
  function minimize() {
160
- isMinimized = true;
161
- isExpanded = false;
162
+ todoPanelState.isMinimized = true;
163
+ todoPanelState.isExpanded = false;
164
+ saveTodoPanelState();
162
165
  }
163
166
 
164
167
  function restore() {
165
- isMinimized = false;
166
- isExpanded = true;
168
+ todoPanelState.isMinimized = false;
169
+ todoPanelState.isExpanded = true;
170
+ saveTodoPanelState();
167
171
  }
168
172
 
169
173
  function getStatusIcon(status: string) {
@@ -194,7 +198,7 @@
194
198
  </script>
195
199
 
196
200
  {#if shouldShow && !appState.isRestoring}
197
- {#if isMinimized}
201
+ {#if todoPanelState.isMinimized}
198
202
  <!-- Minimized state - small floating button, draggable -->
199
203
  <button
200
204
  bind:this={minimizedBtn}
@@ -210,7 +214,7 @@
210
214
  cursor: {isDragging ? 'grabbing' : 'grab'};
211
215
  transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease'};
212
216
  "
213
- transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 200 }}
217
+ transition:fly={{ x: todoPanelState.snapSide === 'right' ? 100 : -100, duration: 200 }}
214
218
  >
215
219
  <Icon name="lucide:list-todo" class="w-5 h-5" />
216
220
  <span class="text-sm font-medium">{progress.completed}/{progress.total}</span>
@@ -222,11 +226,11 @@
222
226
  style="
223
227
  top: {posY}px;
224
228
  left: {panelDisplayLeft}px;
225
- width: {isExpanded ? '330px' : '230px'};
226
- max-height: {isExpanded ? '600px' : '56px'};
229
+ width: {todoPanelState.isExpanded ? '330px' : '230px'};
230
+ max-height: {todoPanelState.isExpanded ? '600px' : '56px'};
227
231
  transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease, width 0.3s, max-height 0.3s'};
228
232
  "
229
- transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 300 }}
233
+ transition:fly={{ x: todoPanelState.snapSide === 'right' ? 100 : -100, duration: 300 }}
230
234
  >
231
235
  <!-- Header (drag handle) -->
232
236
  <div
@@ -244,7 +248,7 @@
244
248
  <span class="text-sm font-semibold text-slate-900 dark:text-slate-100">
245
249
  Task Progress
246
250
  </span>
247
- {#if !isExpanded}
251
+ {#if !todoPanelState.isExpanded}
248
252
  <span class="text-xs text-slate-600 dark:text-slate-400">
249
253
  {progress.completed}/{progress.total} tasks ({progress.percentage}%)
250
254
  </span>
@@ -256,10 +260,10 @@
256
260
  <button
257
261
  onclick={toggleExpand}
258
262
  class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
259
- title={isExpanded ? 'Collapse' : 'Expand'}
263
+ title={todoPanelState.isExpanded ? 'Collapse' : 'Expand'}
260
264
  >
261
265
  <Icon
262
- name={isExpanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
266
+ name={todoPanelState.isExpanded ? 'lucide:chevron-up' : 'lucide:chevron-down'}
263
267
  class="w-4 h-4 text-slate-600 dark:text-slate-400"
264
268
  />
265
269
  </button>
@@ -273,7 +277,7 @@
273
277
  </div>
274
278
  </div>
275
279
 
276
- {#if isExpanded}
280
+ {#if todoPanelState.isExpanded}
277
281
  <!-- Progress bar -->
278
282
  <div class="px-4 py-3 border-b border-slate-100 dark:border-slate-800">
279
283
  <div class="flex items-center justify-between mb-2">
@@ -0,0 +1,189 @@
1
+ <script lang="ts">
2
+ import Modal from '$frontend/components/common/overlay/Modal.svelte';
3
+ import Icon from '$frontend/components/common/display/Icon.svelte';
4
+ import DiffBlock from '$frontend/components/chat/tools/components/DiffBlock.svelte';
5
+ import type { RestoreConflict, ConflictResolution } from '$frontend/services/snapshot/snapshot.service';
6
+
7
+ let {
8
+ isOpen = $bindable(false),
9
+ conflicts,
10
+ onConfirm,
11
+ onClose
12
+ }: {
13
+ isOpen: boolean;
14
+ conflicts: RestoreConflict[];
15
+ onConfirm: (resolutions: ConflictResolution) => void;
16
+ onClose: () => void;
17
+ } = $props();
18
+
19
+ // Internal state
20
+ let conflictResolutions = $state<ConflictResolution>({});
21
+ let expandedDiffs = $state<Set<string>>(new Set());
22
+
23
+ // Reset internal state when conflicts change (modal opened with new data)
24
+ $effect(() => {
25
+ if (conflicts.length > 0) {
26
+ const resolutions: ConflictResolution = {};
27
+ for (const conflict of conflicts) {
28
+ resolutions[conflict.filepath] = 'keep';
29
+ }
30
+ conflictResolutions = resolutions;
31
+ expandedDiffs = new Set();
32
+ }
33
+ });
34
+
35
+ function handleClose() {
36
+ isOpen = false;
37
+ onClose();
38
+ }
39
+
40
+ function handleConfirm() {
41
+ isOpen = false;
42
+ onConfirm(conflictResolutions);
43
+ }
44
+
45
+ function toggleDiff(filepath: string) {
46
+ const next = new Set(expandedDiffs);
47
+ if (next.has(filepath)) {
48
+ next.delete(filepath);
49
+ } else {
50
+ next.add(filepath);
51
+ }
52
+ expandedDiffs = next;
53
+ }
54
+
55
+ function formatFilePath(filepath: string): string {
56
+ const parts = filepath.split('/');
57
+ if (parts.length <= 2) return filepath;
58
+ return '.../' + parts.slice(-2).join('/');
59
+ }
60
+
61
+ function formatTimestamp(iso: string): string {
62
+ try {
63
+ const date = new Date(iso);
64
+ return date.toLocaleString(undefined, {
65
+ month: 'short',
66
+ day: 'numeric',
67
+ hour: '2-digit',
68
+ minute: '2-digit'
69
+ });
70
+ } catch {
71
+ return iso;
72
+ }
73
+ }
74
+ </script>
75
+
76
+ <Modal
77
+ bind:isOpen
78
+ size="md"
79
+ onClose={handleClose}
80
+ >
81
+ {#snippet header()}
82
+ <div class="flex items-start gap-4 px-4 py-3 md:px-6 md:py-4">
83
+ <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700/50 rounded-xl p-3">
84
+ <Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
85
+ </div>
86
+ <div class="flex-1">
87
+ <h3 id="modal-title" class="text-lg font-semibold text-slate-900 dark:text-slate-100">
88
+ Restore Conflict Detected
89
+ </h3>
90
+ <p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
91
+ The following files were also modified in other sessions. Choose how to handle each file:
92
+ </p>
93
+ </div>
94
+ </div>
95
+ {/snippet}
96
+
97
+ {#snippet children()}
98
+ <div class="space-y-3">
99
+ {#each conflicts as conflict}
100
+ <div class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
101
+ <div class="flex items-center justify-between gap-2 mb-2">
102
+ <div class="flex items-center gap-2 min-w-0">
103
+ <Icon name="lucide:file-warning" class="w-4 h-4 text-amber-500 shrink-0" />
104
+ <span class="text-sm font-medium text-slate-800 dark:text-slate-200 truncate" title={conflict.filepath}>
105
+ {formatFilePath(conflict.filepath)}
106
+ </span>
107
+ </div>
108
+ {#if conflict.restoreContent && conflict.currentContent}
109
+ <button
110
+ class="shrink-0 flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-md transition-colors
111
+ {expandedDiffs.has(conflict.filepath)
112
+ ? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-600'
113
+ : 'bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-400 border border-slate-200 dark:border-slate-700 hover:bg-slate-100 dark:hover:bg-slate-700'
114
+ }"
115
+ onclick={() => toggleDiff(conflict.filepath)}
116
+ >
117
+ <Icon name="lucide:git-compare" class="w-3 h-3" />
118
+ {expandedDiffs.has(conflict.filepath) ? 'Hide Diff' : 'View Diff'}
119
+ </button>
120
+ {/if}
121
+ </div>
122
+ <p class="text-xs text-slate-500 dark:text-slate-400 mb-2">
123
+ Modified by another session on {formatTimestamp(conflict.modifiedAt)}
124
+ </p>
125
+
126
+ <!-- Diff View -->
127
+ {#if expandedDiffs.has(conflict.filepath) && conflict.restoreContent && conflict.currentContent}
128
+ <div class="mb-3">
129
+ <div class="flex items-center gap-3 mb-1.5 text-3xs text-slate-500 dark:text-slate-400">
130
+ <span class="flex items-center gap-1">
131
+ <span class="inline-block w-2 h-2 rounded-sm bg-red-400"></span>
132
+ Restore version
133
+ </span>
134
+ <span class="flex items-center gap-1">
135
+ <span class="inline-block w-2 h-2 rounded-sm bg-green-400"></span>
136
+ Current version
137
+ </span>
138
+ </div>
139
+ <DiffBlock
140
+ oldString={conflict.restoreContent}
141
+ newString={conflict.currentContent}
142
+ />
143
+ </div>
144
+ {/if}
145
+
146
+ <div class="flex gap-2">
147
+ <button
148
+ class="relative px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700"
149
+ onclick={() => { conflictResolutions[conflict.filepath] = 'restore'; }}
150
+ >
151
+ {#if conflictResolutions[conflict.filepath] === 'restore'}
152
+ <span class="absolute -top-1.5 -right-1.5 w-5 h-5 flex items-center justify-center rounded-full bg-green-500 dark:bg-green-600 border-2 border-white dark:border-slate-900">
153
+ <Icon name="lucide:check" class="w-3 h-3 text-white" />
154
+ </span>
155
+ {/if}
156
+ Restore
157
+ </button>
158
+ <button
159
+ class="relative px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700"
160
+ onclick={() => { conflictResolutions[conflict.filepath] = 'keep'; }}
161
+ >
162
+ {#if conflictResolutions[conflict.filepath] === 'keep'}
163
+ <span class="absolute -top-1.5 -right-1.5 w-5 h-5 flex items-center justify-center rounded-full bg-green-500 dark:bg-green-600 border-2 border-white dark:border-slate-900">
164
+ <Icon name="lucide:check" class="w-3 h-3 text-white" />
165
+ </span>
166
+ {/if}
167
+ Keep Current
168
+ </button>
169
+ </div>
170
+ </div>
171
+ {/each}
172
+ </div>
173
+ {/snippet}
174
+
175
+ {#snippet footer()}
176
+ <button
177
+ onclick={handleClose}
178
+ class="px-6 py-2.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 font-semibold"
179
+ >
180
+ Cancel
181
+ </button>
182
+ <button
183
+ onclick={handleConfirm}
184
+ class="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg transition-all duration-200 font-semibold"
185
+ >
186
+ Proceed with Restore
187
+ </button>
188
+ {/snippet}
189
+ </Modal>