@myrialabs/clopen 0.1.4 → 0.1.6
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/lib/chat/stream-manager.ts +8 -0
- package/backend/lib/database/migrations/022_add_snapshot_changes_column.ts +35 -0
- package/backend/lib/database/migrations/index.ts +7 -0
- package/backend/lib/database/queries/snapshot-queries.ts +7 -4
- package/backend/lib/files/file-watcher.ts +34 -0
- package/backend/lib/project/status-manager.ts +6 -4
- package/backend/lib/snapshot/snapshot-service.ts +471 -316
- package/backend/lib/terminal/pty-session-manager.ts +1 -32
- package/backend/ws/chat/stream.ts +45 -2
- package/backend/ws/snapshot/restore.ts +77 -67
- package/backend/ws/system/operations.ts +95 -0
- package/frontend/App.svelte +24 -7
- package/frontend/lib/components/chat/ChatInterface.svelte +14 -14
- package/frontend/lib/components/chat/input/ChatInput.svelte +2 -2
- package/frontend/lib/components/chat/input/components/ChatInputActions.svelte +1 -1
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +8 -3
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +12 -2
- package/frontend/lib/components/chat/tools/AskUserQuestionTool.svelte +3 -8
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +222 -30
- package/frontend/lib/components/common/ConnectionBanner.svelte +55 -0
- package/frontend/lib/components/common/MonacoEditor.svelte +14 -0
- package/frontend/lib/components/common/UpdateBanner.svelte +88 -0
- package/frontend/lib/components/common/xterm/XTerm.svelte +9 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +9 -0
- package/frontend/lib/components/git/DiffViewer.svelte +16 -2
- package/frontend/lib/components/history/HistoryModal.svelte +8 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +2 -2
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +123 -0
- package/frontend/lib/components/terminal/Terminal.svelte +1 -7
- package/frontend/lib/components/workspace/DesktopNavigator.svelte +11 -19
- package/frontend/lib/components/workspace/MobileNavigator.svelte +4 -15
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +1 -1
- package/frontend/lib/components/workspace/panels/FilesPanel.svelte +3 -2
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +3 -2
- package/frontend/lib/services/notification/global-stream-monitor.ts +56 -16
- package/frontend/lib/services/snapshot/snapshot.service.ts +71 -32
- package/frontend/lib/stores/core/app.svelte.ts +47 -0
- package/frontend/lib/stores/core/presence.svelte.ts +80 -1
- package/frontend/lib/stores/core/projects.svelte.ts +10 -2
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -2
- package/frontend/lib/stores/features/settings.svelte.ts +10 -1
- package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
- package/frontend/lib/stores/ui/connection.svelte.ts +40 -0
- package/frontend/lib/stores/ui/update.svelte.ts +124 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
- package/frontend/lib/utils/ws.ts +5 -1
- package/index.html +1 -1
- package/package.json +1 -1
- package/shared/types/database/schema.ts +18 -0
- package/shared/types/stores/settings.ts +4 -0
- package/shared/utils/ws-client.ts +16 -2
- package/vite.config.ts +1 -0
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
import type { AskUserQuestionToolInput } from '$shared/types/messaging';
|
|
3
3
|
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
4
4
|
import ws from '$frontend/lib/utils/ws';
|
|
5
|
-
import { soundNotification, pushNotification } from '$frontend/lib/services/notification';
|
|
6
5
|
import { currentSessionId } from '$frontend/lib/stores/core/sessions.svelte';
|
|
7
6
|
import { appState, updateSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
8
7
|
import { debug } from '$shared/utils/logger';
|
|
@@ -99,13 +98,9 @@
|
|
|
99
98
|
otherActive = initialOther;
|
|
100
99
|
});
|
|
101
100
|
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
soundNotification.play().catch(() => {});
|
|
106
|
-
pushNotification.sendChatComplete('Claude is asking you a question. Please respond.').catch(() => {});
|
|
107
|
-
}
|
|
108
|
-
});
|
|
101
|
+
// Sound/push notifications for AskUserQuestion are handled globally by
|
|
102
|
+
// GlobalStreamMonitor (via chat:waiting-input event) — works cross-session,
|
|
103
|
+
// plays once per tool_use, and does not replay when returning to session.
|
|
109
104
|
|
|
110
105
|
function toggleSelection(questionIdx: number, label: string, isMultiSelect: boolean) {
|
|
111
106
|
const current = selections[questionIdx] || new Set();
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import Modal from '$frontend/lib/components/common/Modal.svelte';
|
|
3
3
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
4
4
|
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
5
|
+
import DiffBlock from '$frontend/lib/components/chat/tools/components/DiffBlock.svelte';
|
|
5
6
|
import TimelineGraph from './timeline/TimelineGraph.svelte';
|
|
6
7
|
import { sessionState, loadMessagesForSession } from '$frontend/lib/stores/core/sessions.svelte';
|
|
7
8
|
import { appState } from '$frontend/lib/stores/core/app.svelte';
|
|
@@ -10,6 +11,7 @@
|
|
|
10
11
|
import { buildGraph } from './timeline/graph-builder';
|
|
11
12
|
import { startAnimation } from './timeline/animation';
|
|
12
13
|
import { snapshotService } from '$frontend/lib/services/snapshot/snapshot.service';
|
|
14
|
+
import type { RestoreConflict, ConflictResolution } from '$frontend/lib/services/snapshot/snapshot.service';
|
|
13
15
|
import type { TimelineResponse, GraphNode, GraphEdge, VersionGroup, AnimationState } from './timeline/types';
|
|
14
16
|
|
|
15
17
|
let {
|
|
@@ -32,6 +34,13 @@
|
|
|
32
34
|
let showConfirmDialog = $state(false);
|
|
33
35
|
let pendingNode = $state<GraphNode | null>(null);
|
|
34
36
|
|
|
37
|
+
// Conflict resolution modal state
|
|
38
|
+
let showConflictModal = $state(false);
|
|
39
|
+
let conflictList = $state<RestoreConflict[]>([]);
|
|
40
|
+
let conflictResolutions = $state<ConflictResolution>({});
|
|
41
|
+
let conflictCheckingNode = $state<GraphNode | null>(null);
|
|
42
|
+
let expandedDiffs = $state<Set<string>>(new Set());
|
|
43
|
+
|
|
35
44
|
// Graph visualization state
|
|
36
45
|
let graphNodes = $state<GraphNode[]>([]);
|
|
37
46
|
let graphEdges = $state<GraphEdge[]>([]);
|
|
@@ -58,7 +67,7 @@
|
|
|
58
67
|
let previousMessageCount = $state(0);
|
|
59
68
|
let isInitialLoad = $state(true);
|
|
60
69
|
|
|
61
|
-
// Track if modal was opened
|
|
70
|
+
// Track if modal was opened
|
|
62
71
|
let wasOpen = $state(false);
|
|
63
72
|
|
|
64
73
|
// Get the session ID to use (override or current)
|
|
@@ -66,7 +75,6 @@
|
|
|
66
75
|
|
|
67
76
|
// Load timeline data when modal opens
|
|
68
77
|
$effect(() => {
|
|
69
|
-
// Detect modal opening (transition from closed to open)
|
|
70
78
|
if (isOpen && !wasOpen && sessionId) {
|
|
71
79
|
isContentReady = false;
|
|
72
80
|
hasScrolledToBottom = false;
|
|
@@ -149,7 +157,6 @@
|
|
|
149
157
|
function rebuildGraph(captureOldState = false) {
|
|
150
158
|
if (!timelineData) return;
|
|
151
159
|
|
|
152
|
-
// Capture old state for animation (FLIP technique)
|
|
153
160
|
if (captureOldState && !animationState.isAnimating) {
|
|
154
161
|
animationState.oldNodePositions = new Map(
|
|
155
162
|
graphNodes.map(n => [n.id, { x: n.x, y: n.y }])
|
|
@@ -168,20 +175,64 @@
|
|
|
168
175
|
svgHeight = graphData.svgHeight;
|
|
169
176
|
}
|
|
170
177
|
|
|
171
|
-
// Handle node click - show
|
|
172
|
-
function handleNodeClick(node: GraphNode) {
|
|
178
|
+
// Handle node click - check conflicts first, then show appropriate dialog
|
|
179
|
+
async function handleNodeClick(node: GraphNode) {
|
|
173
180
|
if (readonly) return;
|
|
174
181
|
if (processingAction || node.isCurrent || animationState.isAnimating || appState.isLoading) return;
|
|
175
182
|
|
|
176
|
-
|
|
177
|
-
|
|
183
|
+
const currentSessionId = sessionState.currentSession?.id;
|
|
184
|
+
if (!currentSessionId) return;
|
|
185
|
+
|
|
186
|
+
// Check for conflicts before showing confirmation
|
|
187
|
+
try {
|
|
188
|
+
const conflictCheck = await snapshotService.checkConflicts(
|
|
189
|
+
node.checkpoint.messageId,
|
|
190
|
+
currentSessionId
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
if (conflictCheck.hasConflicts) {
|
|
194
|
+
// Show conflict resolution modal
|
|
195
|
+
conflictList = conflictCheck.conflicts;
|
|
196
|
+
conflictResolutions = {};
|
|
197
|
+
expandedDiffs = new Set();
|
|
198
|
+
// Default all to 'keep' (safer default)
|
|
199
|
+
for (const conflict of conflictCheck.conflicts) {
|
|
200
|
+
conflictResolutions[conflict.filepath] = 'keep';
|
|
201
|
+
}
|
|
202
|
+
conflictCheckingNode = node;
|
|
203
|
+
showConflictModal = true;
|
|
204
|
+
} else {
|
|
205
|
+
// No conflicts - show simple confirmation dialog
|
|
206
|
+
pendingNode = node;
|
|
207
|
+
showConfirmDialog = true;
|
|
208
|
+
}
|
|
209
|
+
} catch (error) {
|
|
210
|
+
debug.error('snapshot', 'Error checking conflicts:', error);
|
|
211
|
+
// Fallback to simple confirmation on error
|
|
212
|
+
pendingNode = node;
|
|
213
|
+
showConfirmDialog = true;
|
|
214
|
+
}
|
|
178
215
|
}
|
|
179
216
|
|
|
180
|
-
//
|
|
217
|
+
// Execute restore after conflict resolution
|
|
218
|
+
async function confirmConflictRestore() {
|
|
219
|
+
if (!conflictCheckingNode) return;
|
|
220
|
+
|
|
221
|
+
showConflictModal = false;
|
|
222
|
+
const node = conflictCheckingNode;
|
|
223
|
+
conflictCheckingNode = null;
|
|
224
|
+
|
|
225
|
+
await executeRestore(node, conflictResolutions);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Execute restore after simple confirmation
|
|
181
229
|
async function confirmRestore() {
|
|
182
230
|
if (!pendingNode) return;
|
|
231
|
+
await executeRestore(pendingNode);
|
|
232
|
+
}
|
|
183
233
|
|
|
184
|
-
|
|
234
|
+
// Shared restore execution logic
|
|
235
|
+
async function executeRestore(node: GraphNode, resolutions?: ConflictResolution) {
|
|
185
236
|
processingAction = true;
|
|
186
237
|
|
|
187
238
|
// Capture current state for animation
|
|
@@ -202,10 +253,10 @@
|
|
|
202
253
|
throw new Error('No active session');
|
|
203
254
|
}
|
|
204
255
|
|
|
205
|
-
// Unified restore - works for all nodes
|
|
206
256
|
await snapshotService.restore(
|
|
207
257
|
node.checkpoint.messageId,
|
|
208
|
-
sessionState.currentSession.id
|
|
258
|
+
sessionState.currentSession.id,
|
|
259
|
+
resolutions
|
|
209
260
|
);
|
|
210
261
|
|
|
211
262
|
// Reload messages
|
|
@@ -214,7 +265,6 @@
|
|
|
214
265
|
// Reload timeline
|
|
215
266
|
const newTimelineData = await snapshotService.getTimeline(sessionState.currentSession.id);
|
|
216
267
|
|
|
217
|
-
// Check if structure changed
|
|
218
268
|
if (JSON.stringify(newTimelineData.nodes) !== JSON.stringify(timelineData?.nodes)) {
|
|
219
269
|
timelineData = newTimelineData;
|
|
220
270
|
rebuildGraph();
|
|
@@ -270,6 +320,36 @@
|
|
|
270
320
|
if (text.length <= maxLength) return text;
|
|
271
321
|
return text.substring(0, maxLength) + '...';
|
|
272
322
|
}
|
|
323
|
+
|
|
324
|
+
function toggleDiff(filepath: string) {
|
|
325
|
+
const next = new Set(expandedDiffs);
|
|
326
|
+
if (next.has(filepath)) {
|
|
327
|
+
next.delete(filepath);
|
|
328
|
+
} else {
|
|
329
|
+
next.add(filepath);
|
|
330
|
+
}
|
|
331
|
+
expandedDiffs = next;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function formatFilePath(filepath: string): string {
|
|
335
|
+
const parts = filepath.split('/');
|
|
336
|
+
if (parts.length <= 2) return filepath;
|
|
337
|
+
return '.../' + parts.slice(-2).join('/');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function formatTimestamp(iso: string): string {
|
|
341
|
+
try {
|
|
342
|
+
const date = new Date(iso);
|
|
343
|
+
return date.toLocaleString(undefined, {
|
|
344
|
+
month: 'short',
|
|
345
|
+
day: 'numeric',
|
|
346
|
+
hour: '2-digit',
|
|
347
|
+
minute: '2-digit'
|
|
348
|
+
});
|
|
349
|
+
} catch {
|
|
350
|
+
return iso;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
273
353
|
</script>
|
|
274
354
|
|
|
275
355
|
<Modal bind:isOpen={isOpen} bind:contentRef={scrollContainer} size="lg" onClose={onClose}>
|
|
@@ -335,22 +415,6 @@
|
|
|
335
415
|
isDisabled={appState.isLoading}
|
|
336
416
|
onNodeClick={handleNodeClick}
|
|
337
417
|
/>
|
|
338
|
-
|
|
339
|
-
<!-- Legend -->
|
|
340
|
-
<!-- <div class="mt-4 p-4 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700 flex flex-wrap items-center gap-6 text-xs">
|
|
341
|
-
<div class="flex items-center gap-2">
|
|
342
|
-
<div class="w-3 h-3 rounded-full bg-green-500 ring-2 ring-green-300 dark:ring-green-700"></div>
|
|
343
|
-
<span class="text-slate-600 dark:text-slate-400">Current Position</span>
|
|
344
|
-
</div>
|
|
345
|
-
<div class="flex items-center gap-2">
|
|
346
|
-
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
|
|
347
|
-
<span class="text-slate-600 dark:text-slate-400">Active Path</span>
|
|
348
|
-
</div>
|
|
349
|
-
<div class="flex items-center gap-2">
|
|
350
|
-
<div class="w-3 h-3 rounded-full bg-slate-300 dark:bg-slate-600 opacity-60"></div>
|
|
351
|
-
<span class="text-slate-600 dark:text-slate-400">Orphaned</span>
|
|
352
|
-
</div>
|
|
353
|
-
</div> -->
|
|
354
418
|
</div>
|
|
355
419
|
{:else if timelineData && graphNodes.length === 0}
|
|
356
420
|
<div class="text-center py-12">
|
|
@@ -375,7 +439,7 @@
|
|
|
375
439
|
{/snippet}
|
|
376
440
|
</Modal>
|
|
377
441
|
|
|
378
|
-
<!-- Confirmation Dialog -->
|
|
442
|
+
<!-- Simple Confirmation Dialog (no conflicts) -->
|
|
379
443
|
<Dialog
|
|
380
444
|
bind:isOpen={showConfirmDialog}
|
|
381
445
|
type="warning"
|
|
@@ -383,7 +447,7 @@
|
|
|
383
447
|
message={pendingNode
|
|
384
448
|
? `Are you sure you want to restore to this checkpoint?
|
|
385
449
|
"${getTruncatedMessage(pendingNode.checkpoint.messageText)}"
|
|
386
|
-
This will restore your
|
|
450
|
+
This will restore your files to this point within this session.`
|
|
387
451
|
: ''}
|
|
388
452
|
confirmText="Restore"
|
|
389
453
|
cancelText="Cancel"
|
|
@@ -394,3 +458,131 @@ This will restore your conversation and files to this point.`
|
|
|
394
458
|
pendingNode = null;
|
|
395
459
|
}}
|
|
396
460
|
/>
|
|
461
|
+
|
|
462
|
+
<!-- Conflict Resolution Modal -->
|
|
463
|
+
<Modal
|
|
464
|
+
bind:isOpen={showConflictModal}
|
|
465
|
+
size="md"
|
|
466
|
+
onClose={() => {
|
|
467
|
+
showConflictModal = false;
|
|
468
|
+
conflictCheckingNode = null;
|
|
469
|
+
conflictList = [];
|
|
470
|
+
conflictResolutions = {};
|
|
471
|
+
expandedDiffs = new Set();
|
|
472
|
+
}}
|
|
473
|
+
>
|
|
474
|
+
{#snippet header()}
|
|
475
|
+
<div class="flex items-start gap-4 px-4 py-3 md:px-6 md:py-4">
|
|
476
|
+
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-700/50 rounded-xl p-3">
|
|
477
|
+
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
478
|
+
</div>
|
|
479
|
+
<div class="flex-1">
|
|
480
|
+
<h3 id="modal-title" class="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
481
|
+
Restore Conflict Detected
|
|
482
|
+
</h3>
|
|
483
|
+
<p class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
|
484
|
+
The following files were also modified in other sessions. Choose how to handle each file:
|
|
485
|
+
</p>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
488
|
+
{/snippet}
|
|
489
|
+
|
|
490
|
+
{#snippet children()}
|
|
491
|
+
<div class="space-y-3">
|
|
492
|
+
{#each conflictList as conflict}
|
|
493
|
+
<div class="border border-slate-200 dark:border-slate-700 rounded-lg p-3">
|
|
494
|
+
<div class="flex items-center justify-between gap-2 mb-2">
|
|
495
|
+
<div class="flex items-center gap-2 min-w-0">
|
|
496
|
+
<Icon name="lucide:file-warning" class="w-4 h-4 text-amber-500 shrink-0" />
|
|
497
|
+
<span class="text-sm font-medium text-slate-800 dark:text-slate-200 truncate" title={conflict.filepath}>
|
|
498
|
+
{formatFilePath(conflict.filepath)}
|
|
499
|
+
</span>
|
|
500
|
+
</div>
|
|
501
|
+
{#if conflict.restoreContent && conflict.currentContent}
|
|
502
|
+
<button
|
|
503
|
+
class="shrink-0 flex items-center gap-1 px-2 py-1 text-xs font-medium rounded-md transition-colors
|
|
504
|
+
{expandedDiffs.has(conflict.filepath)
|
|
505
|
+
? 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 border border-blue-300 dark:border-blue-600'
|
|
506
|
+
: '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'
|
|
507
|
+
}"
|
|
508
|
+
onclick={() => toggleDiff(conflict.filepath)}
|
|
509
|
+
>
|
|
510
|
+
<Icon name="lucide:git-compare" class="w-3 h-3" />
|
|
511
|
+
{expandedDiffs.has(conflict.filepath) ? 'Hide Diff' : 'View Diff'}
|
|
512
|
+
</button>
|
|
513
|
+
{/if}
|
|
514
|
+
</div>
|
|
515
|
+
<p class="text-xs text-slate-500 dark:text-slate-400 mb-2">
|
|
516
|
+
Modified by another session on {formatTimestamp(conflict.modifiedAt)}
|
|
517
|
+
</p>
|
|
518
|
+
|
|
519
|
+
<!-- Diff View -->
|
|
520
|
+
{#if expandedDiffs.has(conflict.filepath) && conflict.restoreContent && conflict.currentContent}
|
|
521
|
+
<div class="mb-3">
|
|
522
|
+
<div class="flex items-center gap-3 mb-1.5 text-3xs text-slate-500 dark:text-slate-400">
|
|
523
|
+
<span class="flex items-center gap-1">
|
|
524
|
+
<span class="inline-block w-2 h-2 rounded-sm bg-red-400"></span>
|
|
525
|
+
Restore version
|
|
526
|
+
</span>
|
|
527
|
+
<span class="flex items-center gap-1">
|
|
528
|
+
<span class="inline-block w-2 h-2 rounded-sm bg-green-400"></span>
|
|
529
|
+
Current version
|
|
530
|
+
</span>
|
|
531
|
+
</div>
|
|
532
|
+
<DiffBlock
|
|
533
|
+
oldString={conflict.restoreContent}
|
|
534
|
+
newString={conflict.currentContent}
|
|
535
|
+
/>
|
|
536
|
+
</div>
|
|
537
|
+
{/if}
|
|
538
|
+
|
|
539
|
+
<div class="flex gap-2">
|
|
540
|
+
<button
|
|
541
|
+
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"
|
|
542
|
+
onclick={() => { conflictResolutions[conflict.filepath] = 'restore'; }}
|
|
543
|
+
>
|
|
544
|
+
{#if conflictResolutions[conflict.filepath] === 'restore'}
|
|
545
|
+
<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">
|
|
546
|
+
<Icon name="lucide:check" class="w-3 h-3 text-white" />
|
|
547
|
+
</span>
|
|
548
|
+
{/if}
|
|
549
|
+
Restore
|
|
550
|
+
</button>
|
|
551
|
+
<button
|
|
552
|
+
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"
|
|
553
|
+
onclick={() => { conflictResolutions[conflict.filepath] = 'keep'; }}
|
|
554
|
+
>
|
|
555
|
+
{#if conflictResolutions[conflict.filepath] === 'keep'}
|
|
556
|
+
<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">
|
|
557
|
+
<Icon name="lucide:check" class="w-3 h-3 text-white" />
|
|
558
|
+
</span>
|
|
559
|
+
{/if}
|
|
560
|
+
Keep Current
|
|
561
|
+
</button>
|
|
562
|
+
</div>
|
|
563
|
+
</div>
|
|
564
|
+
{/each}
|
|
565
|
+
</div>
|
|
566
|
+
{/snippet}
|
|
567
|
+
|
|
568
|
+
{#snippet footer()}
|
|
569
|
+
<button
|
|
570
|
+
onclick={() => {
|
|
571
|
+
showConflictModal = false;
|
|
572
|
+
conflictCheckingNode = null;
|
|
573
|
+
conflictList = [];
|
|
574
|
+
conflictResolutions = {};
|
|
575
|
+
expandedDiffs = new Set();
|
|
576
|
+
}}
|
|
577
|
+
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"
|
|
578
|
+
>
|
|
579
|
+
Cancel
|
|
580
|
+
</button>
|
|
581
|
+
<button
|
|
582
|
+
onclick={confirmConflictRestore}
|
|
583
|
+
class="px-6 py-2.5 bg-amber-600 hover:bg-amber-700 text-white rounded-lg transition-all duration-200 font-semibold"
|
|
584
|
+
>
|
|
585
|
+
Proceed with Restore
|
|
586
|
+
</button>
|
|
587
|
+
{/snippet}
|
|
588
|
+
</Modal>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { connectionState } from '$frontend/lib/stores/ui/connection.svelte';
|
|
3
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
4
|
+
import ws from '$frontend/lib/utils/ws';
|
|
5
|
+
import { slide } from 'svelte/transition';
|
|
6
|
+
|
|
7
|
+
const showBanner = $derived(
|
|
8
|
+
connectionState.status !== 'connected' || connectionState.justReconnected
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
const isReconnecting = $derived(connectionState.status === 'reconnecting');
|
|
12
|
+
const isDisconnected = $derived(connectionState.status === 'disconnected');
|
|
13
|
+
const isReconnected = $derived(connectionState.justReconnected && connectionState.status === 'connected');
|
|
14
|
+
|
|
15
|
+
function handleReconnect() {
|
|
16
|
+
ws.reconnect();
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if showBanner}
|
|
21
|
+
<div
|
|
22
|
+
transition:slide={{ duration: 300 }}
|
|
23
|
+
class="flex items-center justify-center gap-2 px-4 py-1.5 text-sm font-medium
|
|
24
|
+
{isReconnected
|
|
25
|
+
? 'bg-emerald-600 text-white'
|
|
26
|
+
: isDisconnected
|
|
27
|
+
? 'bg-red-600 text-white'
|
|
28
|
+
: 'bg-amber-600 text-white'}"
|
|
29
|
+
role="status"
|
|
30
|
+
aria-live="polite"
|
|
31
|
+
>
|
|
32
|
+
{#if isReconnected}
|
|
33
|
+
<Icon name="lucide:wifi" class="w-4 h-4" />
|
|
34
|
+
<span>Reconnected</span>
|
|
35
|
+
{:else if isReconnecting}
|
|
36
|
+
<Icon name="lucide:loader-circle" class="w-4 h-4 animate-spin" />
|
|
37
|
+
<span>Reconnecting{#if connectionState.reconnectAttempts > 1} (attempt {connectionState.reconnectAttempts}){/if}...</span>
|
|
38
|
+
<button
|
|
39
|
+
onclick={handleReconnect}
|
|
40
|
+
class="ml-1 px-2 py-0.5 text-xs font-semibold rounded bg-white/20 hover:bg-white/30 transition-colors"
|
|
41
|
+
>
|
|
42
|
+
Reconnect now
|
|
43
|
+
</button>
|
|
44
|
+
{:else}
|
|
45
|
+
<Icon name="lucide:wifi-off" class="w-4 h-4" />
|
|
46
|
+
<span>Connection lost</span>
|
|
47
|
+
<button
|
|
48
|
+
onclick={handleReconnect}
|
|
49
|
+
class="ml-1 px-2 py-0.5 text-xs font-semibold rounded bg-white/20 hover:bg-white/30 transition-colors"
|
|
50
|
+
>
|
|
51
|
+
Reconnect now
|
|
52
|
+
</button>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import loader from '@monaco-editor/loader';
|
|
4
4
|
import type { editor } from 'monaco-editor';
|
|
5
5
|
import { themeStore } from '$frontend/lib/stores/ui/theme.svelte';
|
|
6
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
6
7
|
import { debug } from '$shared/utils/logger';
|
|
7
8
|
|
|
8
9
|
interface Props {
|
|
@@ -375,6 +376,8 @@
|
|
|
375
376
|
|
|
376
377
|
const createEditorOptions: (value: string, lang: string, theme: string) => editor.IStandaloneEditorConstructionOptions = (value, lang, theme) => ({
|
|
377
378
|
...EDITOR_CONFIG,
|
|
379
|
+
fontSize: Math.round(settings.fontSize * 0.9),
|
|
380
|
+
lineHeight: Math.round(settings.fontSize * 0.9 * 1.5),
|
|
378
381
|
value,
|
|
379
382
|
language: lang,
|
|
380
383
|
theme,
|
|
@@ -472,6 +475,17 @@
|
|
|
472
475
|
}
|
|
473
476
|
});
|
|
474
477
|
|
|
478
|
+
// Update font size when setting changes
|
|
479
|
+
$effect(() => {
|
|
480
|
+
const size = settings.fontSize;
|
|
481
|
+
if (monacoEditor && isInitialized) {
|
|
482
|
+
monacoEditor.updateOptions({
|
|
483
|
+
fontSize: Math.round(size * 0.9),
|
|
484
|
+
lineHeight: Math.round(size * 0.9 * 1.5)
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
|
|
475
489
|
// Cleanup function
|
|
476
490
|
function cleanup() {
|
|
477
491
|
if (resizeObserver) {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { updateState, runUpdate, dismissUpdate, checkForUpdate } from '$frontend/lib/stores/ui/update.svelte';
|
|
3
|
+
import { settings, updateSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
4
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
5
|
+
import { slide } from 'svelte/transition';
|
|
6
|
+
|
|
7
|
+
const showBanner = $derived(
|
|
8
|
+
!updateState.dismissed && (
|
|
9
|
+
updateState.updateAvailable ||
|
|
10
|
+
updateState.updating ||
|
|
11
|
+
updateState.updateSuccess ||
|
|
12
|
+
updateState.error
|
|
13
|
+
)
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
function handleUpdate() {
|
|
17
|
+
runUpdate();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function handleDismiss() {
|
|
21
|
+
dismissUpdate();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toggleAutoUpdate() {
|
|
25
|
+
updateSettings({ autoUpdate: !settings.autoUpdate });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function handleRetry() {
|
|
29
|
+
checkForUpdate();
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
{#if showBanner}
|
|
34
|
+
<div
|
|
35
|
+
transition:slide={{ duration: 300 }}
|
|
36
|
+
class="flex items-center justify-center gap-2 px-4 py-1.5 text-sm font-medium
|
|
37
|
+
{updateState.updateSuccess
|
|
38
|
+
? 'bg-emerald-600 text-white'
|
|
39
|
+
: updateState.error
|
|
40
|
+
? 'bg-red-600 text-white'
|
|
41
|
+
: updateState.updating
|
|
42
|
+
? 'bg-amber-600 text-white'
|
|
43
|
+
: 'bg-violet-600 text-white'}"
|
|
44
|
+
role="status"
|
|
45
|
+
aria-live="polite"
|
|
46
|
+
>
|
|
47
|
+
{#if updateState.updateSuccess}
|
|
48
|
+
<Icon name="lucide:package-check" class="w-4 h-4" />
|
|
49
|
+
<span>Updated to v{updateState.latestVersion} — restart clopen to apply</span>
|
|
50
|
+
{:else if updateState.error}
|
|
51
|
+
<Icon name="lucide:package-x" class="w-4 h-4" />
|
|
52
|
+
<span>Update failed</span>
|
|
53
|
+
<button
|
|
54
|
+
onclick={handleRetry}
|
|
55
|
+
class="ml-1 px-2 py-0.5 text-xs font-semibold rounded bg-white/20 hover:bg-white/30 transition-colors"
|
|
56
|
+
>
|
|
57
|
+
Retry
|
|
58
|
+
</button>
|
|
59
|
+
<button
|
|
60
|
+
onclick={handleDismiss}
|
|
61
|
+
class="ml-1 px-1.5 py-0.5 text-xs rounded bg-white/10 hover:bg-white/20 transition-colors"
|
|
62
|
+
>
|
|
63
|
+
<Icon name="lucide:x" class="w-3 h-3" />
|
|
64
|
+
</button>
|
|
65
|
+
{:else if updateState.updating}
|
|
66
|
+
<Icon name="lucide:loader-circle" class="w-4 h-4 animate-spin" />
|
|
67
|
+
<span>Updating to v{updateState.latestVersion}...</span>
|
|
68
|
+
{:else}
|
|
69
|
+
<Icon name="lucide:package" class="w-4 h-4" />
|
|
70
|
+
<span>
|
|
71
|
+
v{updateState.latestVersion} available
|
|
72
|
+
<span class="opacity-70">(current: v{updateState.currentVersion})</span>
|
|
73
|
+
</span>
|
|
74
|
+
<button
|
|
75
|
+
onclick={handleUpdate}
|
|
76
|
+
class="ml-1 px-2 py-0.5 text-xs font-semibold rounded bg-white/20 hover:bg-white/30 transition-colors"
|
|
77
|
+
>
|
|
78
|
+
Update now
|
|
79
|
+
</button>
|
|
80
|
+
<button
|
|
81
|
+
onclick={handleDismiss}
|
|
82
|
+
class="ml-1 px-1.5 py-0.5 text-xs rounded bg-white/10 hover:bg-white/20 transition-colors"
|
|
83
|
+
>
|
|
84
|
+
<Icon name="lucide:x" class="w-3 h-3" />
|
|
85
|
+
</button>
|
|
86
|
+
{/if}
|
|
87
|
+
</div>
|
|
88
|
+
{/if}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { terminalStore } from '$frontend/lib/stores/features/terminal.svelte';
|
|
12
12
|
import { backgroundTerminalService } from '$frontend/lib/services/terminal/background';
|
|
13
13
|
import { terminalService } from '$frontend/lib/services/terminal';
|
|
14
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
14
15
|
|
|
15
16
|
// Import CSS directly - Vite will handle it properly
|
|
16
17
|
import 'xterm/css/xterm.css';
|
|
@@ -399,6 +400,14 @@
|
|
|
399
400
|
lastExecutingState = sessionExecuting;
|
|
400
401
|
});
|
|
401
402
|
|
|
403
|
+
// Reactively update terminal font size when setting changes
|
|
404
|
+
$effect(() => {
|
|
405
|
+
const size = settings.fontSize;
|
|
406
|
+
if (isInitialized) {
|
|
407
|
+
xtermService.updateFontSize(size, session?.id);
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
|
|
402
411
|
// Save terminal buffer before switching away
|
|
403
412
|
function saveCurrentBuffer(sessionId: string) {
|
|
404
413
|
// No longer saving buffer since we re-render from session lines
|
|
@@ -179,6 +179,15 @@ export class XTermService {
|
|
|
179
179
|
// No-op in interactive mode
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* Update terminal font size and refit to container
|
|
184
|
+
*/
|
|
185
|
+
updateFontSize(size: number, sessionId?: string): void {
|
|
186
|
+
if (!this.terminal) return;
|
|
187
|
+
this.terminal.options.fontSize = size;
|
|
188
|
+
this.fit(sessionId);
|
|
189
|
+
}
|
|
190
|
+
|
|
182
191
|
/**
|
|
183
192
|
* Show prompt (compatibility method - not needed in interactive mode)
|
|
184
193
|
*/
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { getGitStatusBadgeLabel, getGitStatusBadgeColor } from '$frontend/lib/utils/git-status';
|
|
6
6
|
import { themeStore } from '$frontend/lib/stores/ui/theme.svelte';
|
|
7
7
|
import { projectState } from '$frontend/lib/stores/core/projects.svelte';
|
|
8
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
8
9
|
import loader from '@monaco-editor/loader';
|
|
9
10
|
import type { editor } from 'monaco-editor';
|
|
10
11
|
import type { GitFileDiff } from '$shared/types/git';
|
|
@@ -216,10 +217,11 @@
|
|
|
216
217
|
theme: isDark ? 'diff-dark' : 'diff-light',
|
|
217
218
|
readOnly: true,
|
|
218
219
|
renderSideBySide: true,
|
|
220
|
+
renderSideBySideInlineBreakpoint: Math.round(600 * (settings.fontSize / 13)),
|
|
219
221
|
minimap: { enabled: false },
|
|
220
222
|
scrollBeyondLastLine: false,
|
|
221
|
-
fontSize:
|
|
222
|
-
lineHeight:
|
|
223
|
+
fontSize: Math.round(settings.fontSize * 0.9),
|
|
224
|
+
lineHeight: Math.round(settings.fontSize * 0.9 * 1.5),
|
|
223
225
|
renderOverviewRuler: false,
|
|
224
226
|
enableSplitViewResizing: true,
|
|
225
227
|
automaticLayout: true,
|
|
@@ -246,6 +248,18 @@
|
|
|
246
248
|
}
|
|
247
249
|
});
|
|
248
250
|
|
|
251
|
+
// Update font size when setting changes
|
|
252
|
+
$effect(() => {
|
|
253
|
+
const size = settings.fontSize;
|
|
254
|
+
if (diffEditorInstance) {
|
|
255
|
+
diffEditorInstance.updateOptions({
|
|
256
|
+
fontSize: Math.round(size * 0.9),
|
|
257
|
+
lineHeight: Math.round(size * 0.9 * 1.5),
|
|
258
|
+
renderSideBySideInlineBreakpoint: Math.round(600 * (size / 13))
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
249
263
|
// Reinitialize when diff or selected file changes
|
|
250
264
|
$effect(() => {
|
|
251
265
|
if (activeDiff && containerRef) {
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
import AvatarBubble from '$frontend/lib/components/common/AvatarBubble.svelte';
|
|
11
11
|
import Modal from '$frontend/lib/components/common/Modal.svelte';
|
|
12
12
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
13
|
-
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
13
|
+
import { presenceState, isSessionWaitingInput } from '$frontend/lib/stores/core/presence.svelte';
|
|
14
|
+
import { isSessionUnread } from '$frontend/lib/stores/core/app.svelte';
|
|
14
15
|
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
15
|
-
import { getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
16
16
|
import { debug } from '$shared/utils/logger';
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
@@ -474,7 +474,11 @@
|
|
|
474
474
|
<Icon name="lucide:message-square" class="text-violet-600 dark:text-violet-400 w-4 h-4" />
|
|
475
475
|
{#if streaming}
|
|
476
476
|
<span
|
|
477
|
-
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {
|
|
477
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 {isSessionWaitingInput(session.id, projectState.currentProject?.id) ? 'bg-amber-500' : 'bg-emerald-500'}"
|
|
478
|
+
></span>
|
|
479
|
+
{:else if isSessionUnread(session.id)}
|
|
480
|
+
<span
|
|
481
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white dark:border-slate-900 bg-blue-500"
|
|
478
482
|
></span>
|
|
479
483
|
{:else}
|
|
480
484
|
<span
|
|
@@ -512,7 +516,7 @@
|
|
|
512
516
|
{/if}
|
|
513
517
|
</div>
|
|
514
518
|
{#if streaming}
|
|
515
|
-
{#if
|
|
519
|
+
{#if isSessionWaitingInput(session.id, projectState.currentProject?.id)}
|
|
516
520
|
<p class="text-xs text-amber-500 dark:text-amber-400 mt-0.5 flex items-center gap-1.5">
|
|
517
521
|
<Icon name="lucide:message-circle-question-mark" class="w-3 h-3 shrink-0" />
|
|
518
522
|
Waiting for input...
|