@myrialabs/clopen 0.2.2 → 0.2.4
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/.dockerignore +5 -0
- package/.env.example +2 -5
- package/CONTRIBUTING.md +4 -0
- package/README.md +4 -2
- package/backend/database/queries/message-queries.ts +42 -0
- package/backend/database/utils/connection.ts +5 -5
- package/backend/engine/adapters/claude/environment.ts +3 -4
- package/backend/engine/adapters/claude/stream.ts +107 -0
- package/backend/engine/adapters/opencode/server.ts +7 -1
- package/backend/engine/adapters/opencode/stream.ts +81 -1
- package/backend/engine/types.ts +17 -0
- package/backend/git/git-executor.ts +2 -1
- package/backend/git/git-service.ts +2 -1
- package/backend/index.ts +10 -10
- package/backend/snapshot/blob-store.ts +2 -2
- package/backend/utils/env.ts +13 -15
- package/backend/utils/index.ts +4 -1
- package/backend/utils/paths.ts +11 -0
- package/backend/utils/port-utils.ts +19 -6
- package/backend/ws/git/commit-message.ts +108 -0
- package/backend/ws/git/index.ts +3 -1
- package/backend/ws/messages/crud.ts +52 -0
- package/backend/ws/system/index.ts +7 -1
- package/backend/ws/system/operations.ts +28 -2
- package/bin/clopen.ts +15 -15
- package/docker-compose.yml +31 -0
- package/frontend/App.svelte +3 -0
- package/frontend/components/auth/SetupPage.svelte +45 -13
- package/frontend/components/chat/input/ChatInput.svelte +1 -1
- package/frontend/components/chat/message/ChatMessage.svelte +64 -16
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +124 -10
- 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 +19 -8
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/history/HistoryModal.svelte +30 -78
- package/frontend/components/history/HistoryView.svelte +45 -92
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/SettingsView.svelte +1 -1
- package/frontend/components/settings/appearance/AppearanceSettings.svelte +2 -2
- 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/PanelHeader.svelte +1 -3
- package/frontend/components/workspace/WorkspaceLayout.svelte +11 -1
- package/frontend/components/workspace/panels/FilesPanel.svelte +41 -3
- package/frontend/components/workspace/panels/GitPanel.svelte +53 -8
- package/frontend/main.ts +4 -0
- package/frontend/stores/features/auth.svelte.ts +28 -0
- package/frontend/stores/features/settings.svelte.ts +13 -2
- package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
- package/frontend/stores/ui/update.svelte.ts +51 -4
- package/package.json +2 -2
- package/scripts/dev.ts +3 -2
- package/scripts/start.ts +24 -0
- package/shared/types/git.ts +15 -0
- package/shared/types/stores/settings.ts +12 -0
- package/vite.config.ts +2 -2
|
@@ -9,12 +9,103 @@
|
|
|
9
9
|
import { sessionState } from '$frontend/stores/core/sessions.svelte';
|
|
10
10
|
import { appState } from '$frontend/stores/core/app.svelte';
|
|
11
11
|
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
12
|
-
import {
|
|
12
|
+
import { fly } from 'svelte/transition';
|
|
13
13
|
import type { TodoWriteToolInput } from '$shared/types/messaging';
|
|
14
14
|
|
|
15
15
|
let isExpanded = $state(true);
|
|
16
16
|
let isMinimized = $state(false);
|
|
17
17
|
|
|
18
|
+
// Drag & snap state
|
|
19
|
+
let posY = $state(80);
|
|
20
|
+
let posX = $state(0);
|
|
21
|
+
let snapSide = $state<'left' | 'right'>('right');
|
|
22
|
+
let isDragging = $state(false);
|
|
23
|
+
|
|
24
|
+
// Minimized button ref for measuring width at snap time
|
|
25
|
+
let minimizedBtn = $state<HTMLButtonElement | null>(null);
|
|
26
|
+
|
|
27
|
+
// Non-reactive drag tracking
|
|
28
|
+
let _sx = 0, _sy = 0, _mx = 0, _my = 0, _hasDragged = false;
|
|
29
|
+
|
|
30
|
+
function getPanelWidth() {
|
|
31
|
+
return isExpanded ? 330 : 230;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Always use `left` property so CSS can transition in both directions
|
|
35
|
+
const panelDisplayLeft = $derived(
|
|
36
|
+
isDragging ? posX : snapSide === 'right' ? window.innerWidth - getPanelWidth() - 16 : 16
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const minimizedDisplayLeft = $derived(
|
|
40
|
+
isDragging
|
|
41
|
+
? posX
|
|
42
|
+
: snapSide === 'right'
|
|
43
|
+
? window.innerWidth - (minimizedBtn?.offsetWidth ?? 90) - 16
|
|
44
|
+
: 16
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// --- Main panel drag (from header) ---
|
|
48
|
+
function startDrag(e: PointerEvent) {
|
|
49
|
+
if ((e.target as HTMLElement).closest('button')) return;
|
|
50
|
+
isDragging = true;
|
|
51
|
+
// Use actual rendered position for accuracy
|
|
52
|
+
const panel = (e.currentTarget as HTMLElement).parentElement!;
|
|
53
|
+
const rect = panel.getBoundingClientRect();
|
|
54
|
+
_sx = rect.left;
|
|
55
|
+
_sy = rect.top;
|
|
56
|
+
_mx = e.clientX;
|
|
57
|
+
_my = e.clientY;
|
|
58
|
+
posX = _sx;
|
|
59
|
+
posY = _sy;
|
|
60
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onDrag(e: PointerEvent) {
|
|
64
|
+
if (!isDragging) return;
|
|
65
|
+
posX = _sx + e.clientX - _mx;
|
|
66
|
+
posY = Math.max(0, Math.min(window.innerHeight - 56, _sy + e.clientY - _my));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function endDrag(e: PointerEvent) {
|
|
70
|
+
if (!isDragging) return;
|
|
71
|
+
isDragging = false;
|
|
72
|
+
snapSide = posX + getPanelWidth() / 2 < window.innerWidth / 2 ? 'left' : 'right';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// --- Minimized button drag (click = restore, drag = move) ---
|
|
76
|
+
function startMinimizedDrag(e: PointerEvent) {
|
|
77
|
+
isDragging = true;
|
|
78
|
+
_hasDragged = false;
|
|
79
|
+
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
80
|
+
_sx = rect.left;
|
|
81
|
+
_sy = rect.top;
|
|
82
|
+
_mx = e.clientX;
|
|
83
|
+
_my = e.clientY;
|
|
84
|
+
posX = _sx;
|
|
85
|
+
posY = _sy;
|
|
86
|
+
(e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function onMinimizedDrag(e: PointerEvent) {
|
|
90
|
+
if (!isDragging) return;
|
|
91
|
+
const dx = e.clientX - _mx;
|
|
92
|
+
const dy = e.clientY - _my;
|
|
93
|
+
if (Math.abs(dx) > 5 || Math.abs(dy) > 5) _hasDragged = true;
|
|
94
|
+
posX = _sx + dx;
|
|
95
|
+
posY = Math.max(0, Math.min(window.innerHeight - 56, _sy + dy));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function endMinimizedDrag(e: PointerEvent) {
|
|
99
|
+
if (!isDragging) return;
|
|
100
|
+
isDragging = false;
|
|
101
|
+
if (!_hasDragged) {
|
|
102
|
+
restore();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const el = e.currentTarget as HTMLElement;
|
|
106
|
+
snapSide = posX + el.offsetWidth / 2 < window.innerWidth / 2 ? 'left' : 'right';
|
|
107
|
+
}
|
|
108
|
+
|
|
18
109
|
// Extract the latest TodoWrite data from messages
|
|
19
110
|
const latestTodos = $derived.by(() => {
|
|
20
111
|
if (!sessionState.currentSession || sessionState.messages.length === 0) {
|
|
@@ -104,11 +195,22 @@
|
|
|
104
195
|
|
|
105
196
|
{#if shouldShow && !appState.isRestoring}
|
|
106
197
|
{#if isMinimized}
|
|
107
|
-
<!-- Minimized state - small floating button -->
|
|
198
|
+
<!-- Minimized state - small floating button, draggable -->
|
|
108
199
|
<button
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
200
|
+
bind:this={minimizedBtn}
|
|
201
|
+
onpointerdown={startMinimizedDrag}
|
|
202
|
+
onpointermove={onMinimizedDrag}
|
|
203
|
+
onpointerup={endMinimizedDrag}
|
|
204
|
+
onpointercancel={endMinimizedDrag}
|
|
205
|
+
class="fixed z-30 bg-violet-600 hover:bg-violet-700 dark:bg-violet-500 dark:hover:bg-violet-600 text-white rounded-full p-3 shadow-lg flex items-center gap-2"
|
|
206
|
+
style="
|
|
207
|
+
top: {posY}px;
|
|
208
|
+
left: {minimizedDisplayLeft}px;
|
|
209
|
+
touch-action: none;
|
|
210
|
+
cursor: {isDragging ? 'grabbing' : 'grab'};
|
|
211
|
+
transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease'};
|
|
212
|
+
"
|
|
213
|
+
transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 200 }}
|
|
112
214
|
>
|
|
113
215
|
<Icon name="lucide:list-todo" class="w-5 h-5" />
|
|
114
216
|
<span class="text-sm font-medium">{progress.completed}/{progress.total}</span>
|
|
@@ -116,13 +218,25 @@
|
|
|
116
218
|
{:else}
|
|
117
219
|
<!-- Floating panel -->
|
|
118
220
|
<div
|
|
119
|
-
class="fixed
|
|
120
|
-
style="
|
|
121
|
-
|
|
221
|
+
class="fixed z-30 bg-white dark:bg-slate-900 rounded-xl shadow-lg border border-slate-200 dark:border-slate-700 overflow-hidden"
|
|
222
|
+
style="
|
|
223
|
+
top: {posY}px;
|
|
224
|
+
left: {panelDisplayLeft}px;
|
|
225
|
+
width: {isExpanded ? '330px' : '230px'};
|
|
226
|
+
max-height: {isExpanded ? '600px' : '56px'};
|
|
227
|
+
transition: {isDragging ? 'none' : 'left 0.25s ease, top 0.15s ease, width 0.3s, max-height 0.3s'};
|
|
228
|
+
"
|
|
229
|
+
transition:fly={{ x: snapSide === 'right' ? 100 : -100, duration: 300 }}
|
|
122
230
|
>
|
|
123
|
-
<!-- Header -->
|
|
231
|
+
<!-- Header (drag handle) -->
|
|
124
232
|
<div
|
|
125
233
|
class="flex items-center justify-between px-4 py-3 bg-gradient-to-r from-violet-50 to-violet-50 dark:from-slate-800 dark:to-slate-800 border-b border-slate-200 dark:border-slate-700"
|
|
234
|
+
style="touch-action: none; cursor: {isDragging ? 'grabbing' : 'grab'};"
|
|
235
|
+
onpointerdown={startDrag}
|
|
236
|
+
onpointermove={onDrag}
|
|
237
|
+
onpointerup={endDrag}
|
|
238
|
+
onpointercancel={endDrag}
|
|
239
|
+
role="none"
|
|
126
240
|
>
|
|
127
241
|
<div class="flex items-center gap-3">
|
|
128
242
|
<Icon name="lucide:list-todo" class="w-5 h-5 text-violet-600 dark:text-violet-400" />
|
|
@@ -246,4 +360,4 @@
|
|
|
246
360
|
:global(.dark) div::-webkit-scrollbar-thumb:hover {
|
|
247
361
|
background: rgb(71 85 105);
|
|
248
362
|
}
|
|
249
|
-
</style>
|
|
363
|
+
</style>
|
|
@@ -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>
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import Modal from '$frontend/components/common/overlay/Modal.svelte';
|
|
3
3
|
import Dialog from '$frontend/components/common/overlay/Dialog.svelte';
|
|
4
4
|
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
5
|
-
import
|
|
5
|
+
import ConflictResolutionModal from './ConflictResolutionModal.svelte';
|
|
6
6
|
import TimelineGraph from './timeline/TimelineGraph.svelte';
|
|
7
7
|
import { sessionState, loadMessagesForSession } from '$frontend/stores/core/sessions.svelte';
|
|
8
8
|
import { appState } from '$frontend/stores/core/app.svelte';
|
|
@@ -38,9 +38,7 @@
|
|
|
38
38
|
// Conflict resolution modal state
|
|
39
39
|
let showConflictModal = $state(false);
|
|
40
40
|
let conflictList = $state<RestoreConflict[]>([]);
|
|
41
|
-
let conflictResolutions = $state<ConflictResolution>({});
|
|
42
41
|
let conflictCheckingNode = $state<GraphNode | null>(null);
|
|
43
|
-
let expandedDiffs = $state<Set<string>>(new Set());
|
|
44
42
|
|
|
45
43
|
// Graph visualization state
|
|
46
44
|
let graphNodes = $state<GraphNode[]>([]);
|
|
@@ -207,12 +205,6 @@
|
|
|
207
205
|
if (conflictCheck.hasConflicts) {
|
|
208
206
|
// Show conflict resolution modal
|
|
209
207
|
conflictList = conflictCheck.conflicts;
|
|
210
|
-
conflictResolutions = {};
|
|
211
|
-
expandedDiffs = new Set();
|
|
212
|
-
// Default all to 'keep' (safer default)
|
|
213
|
-
for (const conflict of conflictCheck.conflicts) {
|
|
214
|
-
conflictResolutions[conflict.filepath] = 'keep';
|
|
215
|
-
}
|
|
216
208
|
conflictCheckingNode = node;
|
|
217
209
|
showConflictModal = true;
|
|
218
210
|
} else {
|
|
@@ -229,14 +221,13 @@
|
|
|
229
221
|
}
|
|
230
222
|
|
|
231
223
|
// Execute restore after conflict resolution
|
|
232
|
-
async function confirmConflictRestore() {
|
|
224
|
+
async function confirmConflictRestore(resolutions: ConflictResolution) {
|
|
233
225
|
if (!conflictCheckingNode) return;
|
|
234
226
|
|
|
235
|
-
showConflictModal = false;
|
|
236
227
|
const node = conflictCheckingNode;
|
|
237
228
|
conflictCheckingNode = null;
|
|
238
229
|
|
|
239
|
-
await executeRestore(node,
|
|
230
|
+
await executeRestore(node, resolutions);
|
|
240
231
|
}
|
|
241
232
|
|
|
242
233
|
// Execute restore after simple confirmation
|
|
@@ -335,35 +326,6 @@
|
|
|
335
326
|
return text.substring(0, maxLength) + '...';
|
|
336
327
|
}
|
|
337
328
|
|
|
338
|
-
function toggleDiff(filepath: string) {
|
|
339
|
-
const next = new Set(expandedDiffs);
|
|
340
|
-
if (next.has(filepath)) {
|
|
341
|
-
next.delete(filepath);
|
|
342
|
-
} else {
|
|
343
|
-
next.add(filepath);
|
|
344
|
-
}
|
|
345
|
-
expandedDiffs = next;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function formatFilePath(filepath: string): string {
|
|
349
|
-
const parts = filepath.split('/');
|
|
350
|
-
if (parts.length <= 2) return filepath;
|
|
351
|
-
return '.../' + parts.slice(-2).join('/');
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function formatTimestamp(iso: string): string {
|
|
355
|
-
try {
|
|
356
|
-
const date = new Date(iso);
|
|
357
|
-
return date.toLocaleString(undefined, {
|
|
358
|
-
month: 'short',
|
|
359
|
-
day: 'numeric',
|
|
360
|
-
hour: '2-digit',
|
|
361
|
-
minute: '2-digit'
|
|
362
|
-
});
|
|
363
|
-
} catch {
|
|
364
|
-
return iso;
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
329
|
</script>
|
|
368
330
|
|
|
369
331
|
<Modal bind:isOpen={isOpen} bind:contentRef={scrollContainer} size="lg" onClose={onClose}>
|
|
@@ -472,129 +434,12 @@
|
|
|
472
434
|
/>
|
|
473
435
|
|
|
474
436
|
<!-- Conflict Resolution Modal -->
|
|
475
|
-
<
|
|
437
|
+
<ConflictResolutionModal
|
|
476
438
|
bind:isOpen={showConflictModal}
|
|
477
|
-
|
|
439
|
+
conflicts={conflictList}
|
|
440
|
+
onConfirm={confirmConflictRestore}
|
|
478
441
|
onClose={() => {
|
|
479
|
-
showConflictModal = false;
|
|
480
442
|
conflictCheckingNode = null;
|
|
481
443
|
conflictList = [];
|
|
482
|
-
conflictResolutions = {};
|
|
483
|
-
expandedDiffs = new Set();
|
|
484
444
|
}}
|
|
485
|
-
|
|
486
|
-
{#snippet header()}
|
|
487
|
-
<div class="flex items-start gap-4 px-4 py-3 md:px-6 md:py-4">
|
|
488
|
-
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700/50 rounded-xl p-3">
|
|
489
|
-
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
490
|
-
</div>
|
|
491
|
-
<div class="flex-1">
|
|
492
|
-
<h3 id="modal-title" class="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
493
|
-
Restore Conflict Detected
|
|
494
|
-
</h3>
|
|
495
|
-
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
|
496
|
-
The following files were also modified in other sessions. Choose how to handle each file:
|
|
497
|
-
</p>
|
|
498
|
-
</div>
|
|
499
|
-
</div>
|
|
500
|
-
{/snippet}
|
|
501
|
-
|
|
502
|
-
{#snippet children()}
|
|
503
|
-
<div class="space-y-3">
|
|
504
|
-
{#each conflictList as conflict}
|
|
505
|
-
<div class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
|
506
|
-
<div class="flex items-center justify-between gap-2 mb-2">
|
|
507
|
-
<div class="flex items-center gap-2 min-w-0">
|
|
508
|
-
<Icon name="lucide:file-warning" class="w-4 h-4 text-amber-500 shrink-0" />
|
|
509
|
-
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 truncate" title={conflict.filepath}>
|
|
510
|
-
{formatFilePath(conflict.filepath)}
|
|
511
|
-
</span>
|
|
512
|
-
</div>
|
|
513
|
-
{#if conflict.restoreContent && conflict.currentContent}
|
|
514
|
-
<button
|
|
515
|
-
class="shrink-0 flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-md transition-colors
|
|
516
|
-
{expandedDiffs.has(conflict.filepath)
|
|
517
|
-
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-600'
|
|
518
|
-
: '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'
|
|
519
|
-
}"
|
|
520
|
-
onclick={() => toggleDiff(conflict.filepath)}
|
|
521
|
-
>
|
|
522
|
-
<Icon name="lucide:git-compare" class="w-3 h-3" />
|
|
523
|
-
{expandedDiffs.has(conflict.filepath) ? 'Hide Diff' : 'View Diff'}
|
|
524
|
-
</button>
|
|
525
|
-
{/if}
|
|
526
|
-
</div>
|
|
527
|
-
<p class="text-xs text-slate-500 dark:text-slate-400 mb-2">
|
|
528
|
-
Modified by another session on {formatTimestamp(conflict.modifiedAt)}
|
|
529
|
-
</p>
|
|
530
|
-
|
|
531
|
-
<!-- Diff View -->
|
|
532
|
-
{#if expandedDiffs.has(conflict.filepath) && conflict.restoreContent && conflict.currentContent}
|
|
533
|
-
<div class="mb-3">
|
|
534
|
-
<div class="flex items-center gap-3 mb-1.5 text-3xs text-slate-500 dark:text-slate-400">
|
|
535
|
-
<span class="flex items-center gap-1">
|
|
536
|
-
<span class="inline-block w-2 h-2 rounded-sm bg-red-400"></span>
|
|
537
|
-
Restore version
|
|
538
|
-
</span>
|
|
539
|
-
<span class="flex items-center gap-1">
|
|
540
|
-
<span class="inline-block w-2 h-2 rounded-sm bg-green-400"></span>
|
|
541
|
-
Current version
|
|
542
|
-
</span>
|
|
543
|
-
</div>
|
|
544
|
-
<DiffBlock
|
|
545
|
-
oldString={conflict.restoreContent}
|
|
546
|
-
newString={conflict.currentContent}
|
|
547
|
-
/>
|
|
548
|
-
</div>
|
|
549
|
-
{/if}
|
|
550
|
-
|
|
551
|
-
<div class="flex gap-2">
|
|
552
|
-
<button
|
|
553
|
-
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"
|
|
554
|
-
onclick={() => { conflictResolutions[conflict.filepath] = 'restore'; }}
|
|
555
|
-
>
|
|
556
|
-
{#if conflictResolutions[conflict.filepath] === 'restore'}
|
|
557
|
-
<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">
|
|
558
|
-
<Icon name="lucide:check" class="w-3 h-3 text-white" />
|
|
559
|
-
</span>
|
|
560
|
-
{/if}
|
|
561
|
-
Restore
|
|
562
|
-
</button>
|
|
563
|
-
<button
|
|
564
|
-
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"
|
|
565
|
-
onclick={() => { conflictResolutions[conflict.filepath] = 'keep'; }}
|
|
566
|
-
>
|
|
567
|
-
{#if conflictResolutions[conflict.filepath] === 'keep'}
|
|
568
|
-
<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">
|
|
569
|
-
<Icon name="lucide:check" class="w-3 h-3 text-white" />
|
|
570
|
-
</span>
|
|
571
|
-
{/if}
|
|
572
|
-
Keep Current
|
|
573
|
-
</button>
|
|
574
|
-
</div>
|
|
575
|
-
</div>
|
|
576
|
-
{/each}
|
|
577
|
-
</div>
|
|
578
|
-
{/snippet}
|
|
579
|
-
|
|
580
|
-
{#snippet footer()}
|
|
581
|
-
<button
|
|
582
|
-
onclick={() => {
|
|
583
|
-
showConflictModal = false;
|
|
584
|
-
conflictCheckingNode = null;
|
|
585
|
-
conflictList = [];
|
|
586
|
-
conflictResolutions = {};
|
|
587
|
-
expandedDiffs = new Set();
|
|
588
|
-
}}
|
|
589
|
-
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"
|
|
590
|
-
>
|
|
591
|
-
Cancel
|
|
592
|
-
</button>
|
|
593
|
-
<button
|
|
594
|
-
onclick={confirmConflictRestore}
|
|
595
|
-
class="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg transition-all duration-200 font-semibold"
|
|
596
|
-
>
|
|
597
|
-
Proceed with Restore
|
|
598
|
-
</button>
|
|
599
|
-
{/snippet}
|
|
600
|
-
</Modal>
|
|
445
|
+
/>
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Dialog from '../overlay/Dialog.svelte';
|
|
3
|
+
import Icon from '../display/Icon.svelte';
|
|
4
|
+
import { updateState, hideRestartModal } from '$frontend/stores/ui/update.svelte';
|
|
5
|
+
|
|
6
|
+
function handleClose() {
|
|
7
|
+
hideRestartModal();
|
|
8
|
+
}
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<Dialog
|
|
12
|
+
bind:isOpen={updateState.showRestartModal}
|
|
13
|
+
onClose={handleClose}
|
|
14
|
+
title="Updated to v{updateState.latestVersion}"
|
|
15
|
+
type="success"
|
|
16
|
+
confirmText="Got it"
|
|
17
|
+
showCancel={false}
|
|
18
|
+
>
|
|
19
|
+
<div class="flex items-start space-x-4">
|
|
20
|
+
<div class="bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-700/50 rounded-xl p-3">
|
|
21
|
+
<Icon name="lucide:circle-check" class="w-6 h-6 text-green-600 dark:text-green-400" />
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="flex-1 space-y-3">
|
|
25
|
+
<h3 class="text-lg font-semibold text-green-900 dark:text-green-100">
|
|
26
|
+
Updated to v{updateState.latestVersion}
|
|
27
|
+
</h3>
|
|
28
|
+
|
|
29
|
+
<p class="text-sm text-slate-600 dark:text-slate-400">
|
|
30
|
+
To apply the update, restart the server:
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<ol class="text-sm text-slate-700 dark:text-slate-300 space-y-2.5 list-none pl-0">
|
|
34
|
+
<li class="flex items-start gap-2.5">
|
|
35
|
+
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 text-xs font-bold shrink-0 mt-0.5">1</span>
|
|
36
|
+
<span>Go to the terminal where you ran <code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-800 rounded text-xs font-mono">clopen</code> <span class="text-slate-500 dark:text-slate-500">(not the terminal inside Clopen)</span></span>
|
|
37
|
+
</li>
|
|
38
|
+
<li class="flex items-start gap-2.5">
|
|
39
|
+
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 text-xs font-bold shrink-0 mt-0.5">2</span>
|
|
40
|
+
<span>Press <kbd class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-800 border border-slate-300 dark:border-slate-600 rounded text-xs font-mono">Ctrl+C</kbd> to stop the server</span>
|
|
41
|
+
</li>
|
|
42
|
+
<li class="flex items-start gap-2.5">
|
|
43
|
+
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 text-xs font-bold shrink-0 mt-0.5">3</span>
|
|
44
|
+
<span>Run <code class="px-1.5 py-0.5 bg-slate-100 dark:bg-slate-800 rounded text-xs font-mono">clopen</code> again</span>
|
|
45
|
+
</li>
|
|
46
|
+
<li class="flex items-start gap-2.5">
|
|
47
|
+
<span class="flex items-center justify-center w-5 h-5 rounded-full bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400 text-xs font-bold shrink-0 mt-0.5">4</span>
|
|
48
|
+
<span>Refresh this browser tab</span>
|
|
49
|
+
</li>
|
|
50
|
+
</ol>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</Dialog>
|