@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.
- package/backend/engine/adapters/claude/stream.ts +107 -0
- package/backend/engine/adapters/opencode/message-converter.ts +37 -2
- package/backend/engine/adapters/opencode/stream.ts +81 -1
- package/backend/engine/types.ts +17 -0
- package/backend/git/git-service.ts +2 -1
- package/backend/ws/git/commit-message.ts +108 -0
- package/backend/ws/git/index.ts +3 -1
- package/backend/ws/system/index.ts +7 -1
- package/backend/ws/system/operations.ts +28 -2
- package/backend/ws/user/crud.ts +6 -3
- package/frontend/App.svelte +3 -0
- package/frontend/components/auth/SetupPage.svelte +2 -2
- package/frontend/components/chat/input/ChatInput.svelte +1 -1
- package/frontend/components/chat/message/ChatMessage.svelte +64 -16
- package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
- package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
- package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
- package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
- package/frontend/components/common/feedback/UpdateBanner.svelte +17 -6
- package/frontend/components/common/media/MediaPreview.svelte +187 -0
- package/frontend/components/files/FileViewer.svelte +11 -143
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/git/DiffViewer.svelte +50 -130
- package/frontend/components/git/FileChangeItem.svelte +22 -0
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/SettingsView.svelte +1 -1
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
- package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
- package/frontend/components/settings/git/GitSettings.svelte +392 -0
- package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
- package/frontend/components/settings/model/ModelSettings.svelte +172 -289
- package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
- package/frontend/components/workspace/PanelHeader.svelte +1 -3
- package/frontend/components/workspace/WorkspaceLayout.svelte +14 -2
- package/frontend/components/workspace/panels/FilesPanel.svelte +76 -1
- package/frontend/components/workspace/panels/GitPanel.svelte +84 -33
- package/frontend/main.ts +4 -0
- package/frontend/stores/core/files.svelte.ts +15 -1
- package/frontend/stores/features/settings.svelte.ts +13 -2
- package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
- package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
- package/frontend/stores/ui/update.svelte.ts +45 -4
- package/frontend/utils/file-type.ts +68 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/shared/constants/binary-extensions.ts +40 -0
- package/shared/types/git.ts +15 -0
- package/shared/types/messaging/tool.ts +1 -0
- package/shared/types/stores/settings.ts +12 -0
- package/shared/utils/file-type-detection.ts +9 -1
- 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
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
295
|
-
|
|
320
|
+
sessionState.currentSession.id,
|
|
321
|
+
resolutions
|
|
322
|
+
);
|
|
296
323
|
|
|
297
|
-
|
|
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
|
-
<
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
16
|
-
let
|
|
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>
|