@myrialabs/clopen 0.1.4 → 0.1.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/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/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/MonacoEditor.svelte +14 -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 +3 -4
- package/frontend/lib/components/settings/appearance/AppearanceSettings.svelte +59 -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/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/presence.svelte.ts +63 -1
- package/frontend/lib/stores/features/settings.svelte.ts +9 -1
- package/frontend/lib/stores/features/terminal.svelte.ts +6 -0
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -3
- package/package.json +1 -1
- package/shared/types/database/schema.ts +18 -0
- package/shared/types/stores/settings.ts +2 -0
|
@@ -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-[10px] 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>
|
|
@@ -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) {
|
|
@@ -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,8 @@
|
|
|
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
14
|
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
15
|
-
import { getSessionProcessState } from '$frontend/lib/stores/core/app.svelte';
|
|
16
15
|
import { debug } from '$shared/utils/logger';
|
|
17
16
|
|
|
18
17
|
interface Props {
|
|
@@ -474,7 +473,7 @@
|
|
|
474
473
|
<Icon name="lucide:message-square" class="text-violet-600 dark:text-violet-400 w-4 h-4" />
|
|
475
474
|
{#if streaming}
|
|
476
475
|
<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 {
|
|
476
|
+
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
477
|
></span>
|
|
479
478
|
{:else}
|
|
480
479
|
<span
|
|
@@ -512,7 +511,7 @@
|
|
|
512
511
|
{/if}
|
|
513
512
|
</div>
|
|
514
513
|
{#if streaming}
|
|
515
|
-
{#if
|
|
514
|
+
{#if isSessionWaitingInput(session.id, projectState.currentProject?.id)}
|
|
516
515
|
<p class="text-xs text-amber-500 dark:text-amber-400 mt-0.5 flex items-center gap-1.5">
|
|
517
516
|
<Icon name="lucide:message-circle-question-mark" class="w-3 h-3 shrink-0" />
|
|
518
517
|
Waiting for input...
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { themeStore, toggleDarkMode } from '$frontend/lib/stores/ui/theme.svelte';
|
|
3
|
+
import { settings, updateSettings, applyFontSize } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
4
|
import Icon from '../../common/Icon.svelte';
|
|
5
|
+
|
|
6
|
+
const FONT_SIZE_MIN = 10;
|
|
7
|
+
const FONT_SIZE_MAX = 20;
|
|
8
|
+
|
|
9
|
+
function handleFontSizeChange(e: Event) {
|
|
10
|
+
const value = Number((e.target as HTMLInputElement).value);
|
|
11
|
+
applyFontSize(value);
|
|
12
|
+
updateSettings({ fontSize: value });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fontSizePercent() {
|
|
16
|
+
return ((settings.fontSize - FONT_SIZE_MIN) / (FONT_SIZE_MAX - FONT_SIZE_MIN)) * 100;
|
|
17
|
+
}
|
|
4
18
|
</script>
|
|
5
19
|
|
|
6
20
|
<div class="py-1">
|
|
@@ -41,5 +55,50 @@
|
|
|
41
55
|
></span>
|
|
42
56
|
</label>
|
|
43
57
|
</div>
|
|
58
|
+
|
|
59
|
+
<!-- Font Size -->
|
|
60
|
+
<div
|
|
61
|
+
class="flex flex-col gap-3 p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl transition-all duration-150 hover:border-violet-500/20"
|
|
62
|
+
>
|
|
63
|
+
<div class="flex items-center gap-3.5">
|
|
64
|
+
<div
|
|
65
|
+
class="flex items-center justify-center w-10 h-10 bg-violet-500/10 dark:bg-violet-500/15 rounded-lg text-violet-600 dark:text-violet-400"
|
|
66
|
+
>
|
|
67
|
+
<Icon name="lucide:type" class="w-5 h-5" />
|
|
68
|
+
</div>
|
|
69
|
+
<div class="flex flex-col gap-0.5 flex-1">
|
|
70
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">Font Size</div>
|
|
71
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">Adjust the base font size of the application</div>
|
|
72
|
+
</div>
|
|
73
|
+
<div class="text-sm font-semibold text-violet-600 dark:text-violet-400 shrink-0 w-10 text-right">
|
|
74
|
+
{settings.fontSize}px
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<div class="flex items-center gap-2.5 px-0.5">
|
|
79
|
+
<span class="text-xs text-slate-500 dark:text-slate-500 shrink-0">A</span>
|
|
80
|
+
<div class="relative flex-1 h-1.5">
|
|
81
|
+
<div class="absolute inset-0 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
|
82
|
+
<div
|
|
83
|
+
class="absolute inset-y-0 left-0 bg-gradient-to-r from-violet-500 to-purple-500 rounded-full"
|
|
84
|
+
style="width: {fontSizePercent()}%"
|
|
85
|
+
></div>
|
|
86
|
+
<input
|
|
87
|
+
type="range"
|
|
88
|
+
min={FONT_SIZE_MIN}
|
|
89
|
+
max={FONT_SIZE_MAX}
|
|
90
|
+
step="1"
|
|
91
|
+
value={settings.fontSize}
|
|
92
|
+
oninput={handleFontSizeChange}
|
|
93
|
+
class="absolute inset-0 w-full opacity-0 cursor-pointer h-full"
|
|
94
|
+
/>
|
|
95
|
+
<div
|
|
96
|
+
class="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white border-2 border-violet-500 rounded-full shadow-sm pointer-events-none"
|
|
97
|
+
style="left: calc({fontSizePercent()}% - {fontSizePercent() / 100 * 16}px)"
|
|
98
|
+
></div>
|
|
99
|
+
</div>
|
|
100
|
+
<span class="text-base text-slate-500 dark:text-slate-500 shrink-0">A</span>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
44
103
|
</div>
|
|
45
104
|
</div>
|
|
@@ -284,16 +284,10 @@
|
|
|
284
284
|
/>
|
|
285
285
|
</div>
|
|
286
286
|
{:else}
|
|
287
|
-
<!-- No active session -->
|
|
287
|
+
<!-- No active session - will auto-connect -->
|
|
288
288
|
<div class="flex-1 flex items-center justify-center font-mono">
|
|
289
289
|
<div class="text-center text-slate-600 dark:text-slate-400">
|
|
290
290
|
<p>No terminal sessions available</p>
|
|
291
|
-
<button
|
|
292
|
-
class="mt-2 px-4 py-2 bg-violet-600 hover:bg-violet-700 text-white rounded-md transition-colors"
|
|
293
|
-
onclick={handleNewSession}
|
|
294
|
-
>
|
|
295
|
-
Create New Terminal
|
|
296
|
-
</button>
|
|
297
291
|
</div>
|
|
298
292
|
</div>
|
|
299
293
|
{/if}
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
8
8
|
import { openSettingsModal } from '$frontend/lib/stores/ui/settings-modal.svelte';
|
|
9
9
|
import { projectStatusService } from '$frontend/lib/services/project';
|
|
10
|
-
import { presenceState } from '$frontend/lib/stores/core/presence.svelte';
|
|
10
|
+
import { presenceState, getProjectStatusColor } from '$frontend/lib/stores/core/presence.svelte';
|
|
11
11
|
import type { Project } from '$shared/types/database/schema';
|
|
12
12
|
import { debug } from '$shared/utils/logger';
|
|
13
|
+
import { settings } from '$frontend/lib/stores/features/settings.svelte';
|
|
13
14
|
import FolderBrowser from '$frontend/lib/components/common/FolderBrowser.svelte';
|
|
14
15
|
import Dialog from '$frontend/lib/components/common/Dialog.svelte';
|
|
15
16
|
import ViewMenu from '$frontend/lib/components/workspace/ViewMenu.svelte';
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
const isCollapsed = $derived(workspaceState.navigatorCollapsed);
|
|
31
32
|
const currentProjectId = $derived(projectState.currentProject?.id);
|
|
32
33
|
const navigatorWidth = $derived(
|
|
33
|
-
workspaceState.navigatorCollapsed ? 48 : workspaceState.navigatorWidth
|
|
34
|
+
workspaceState.navigatorCollapsed ? 48 : Math.round(workspaceState.navigatorWidth * (settings.fontSize / 13))
|
|
34
35
|
);
|
|
35
36
|
|
|
36
37
|
const filteredProjects = $derived(() => {
|
|
@@ -110,18 +111,7 @@
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
//
|
|
114
|
-
// Shows real-time status for ALL projects, not just the active one.
|
|
115
|
-
// Uses backend-computed isWaitingInput so background sessions are accurate
|
|
116
|
-
// even when the frontend hasn't received their chat events.
|
|
117
|
-
function getStatusColor(projectId: string): string {
|
|
118
|
-
const status = presenceState.statuses.get(projectId);
|
|
119
|
-
if (!status?.streams) return 'bg-slate-500/30';
|
|
120
|
-
const activeStreams = status.streams.filter((s: any) => s.status === 'active');
|
|
121
|
-
if (activeStreams.length === 0) return 'bg-slate-500/30';
|
|
122
|
-
const hasWaitingInput = activeStreams.some((s: any) => s.isWaitingInput);
|
|
123
|
-
return hasWaitingInput ? 'bg-amber-500' : 'bg-emerald-500';
|
|
124
|
-
}
|
|
114
|
+
// Status color for project indicator — uses shared helper from presence store
|
|
125
115
|
|
|
126
116
|
// Close folder browser
|
|
127
117
|
function closeFolderBrowser() {
|
|
@@ -244,7 +234,7 @@
|
|
|
244
234
|
<div class="relative shrink-0">
|
|
245
235
|
<Icon name="lucide:folder" class="w-4 h-4" />
|
|
246
236
|
<span
|
|
247
|
-
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {
|
|
237
|
+
class="absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {getProjectStatusColor(project.id ?? '')}"
|
|
248
238
|
></span>
|
|
249
239
|
</div>
|
|
250
240
|
|
|
@@ -301,7 +291,7 @@
|
|
|
301
291
|
</footer>
|
|
302
292
|
{:else}
|
|
303
293
|
<!-- Collapsed State: Icon Buttons -->
|
|
304
|
-
<div class="flex
|
|
294
|
+
<div class="flex flex-col items-center pt-4 px-2 shrink-0">
|
|
305
295
|
<button
|
|
306
296
|
type="button"
|
|
307
297
|
class="flex items-center justify-center w-9 h-9 bg-transparent border-none rounded-lg text-slate-500 cursor-pointer transition-all duration-150 relative hover:bg-violet-500/10 hover:text-slate-900 dark:hover:text-slate-100"
|
|
@@ -312,13 +302,15 @@
|
|
|
312
302
|
</button>
|
|
313
303
|
|
|
314
304
|
<div class="w-6 h-px bg-violet-500/10 my-1"></div>
|
|
305
|
+
</div>
|
|
315
306
|
|
|
316
|
-
|
|
307
|
+
<div class="flex-1 flex flex-col items-center gap-2 px-2 pb-4 min-h-0 overflow-y-auto">
|
|
308
|
+
{#each existingProjects as project (project.id)}
|
|
317
309
|
{@const projectStatus = presenceState.statuses.get(project.id ?? '')}
|
|
318
310
|
{@const activeUserCount = (projectStatus?.activeUsers || []).length}
|
|
319
311
|
<button
|
|
320
312
|
type="button"
|
|
321
|
-
class="flex items-center justify-center w-9 h-9 border-none rounded-lg cursor-pointer transition-all duration-150 relative font-semibold text-sm
|
|
313
|
+
class="flex items-center justify-center w-9 h-9 shrink-0 border-none rounded-lg cursor-pointer transition-all duration-150 relative font-semibold text-sm
|
|
322
314
|
{currentProjectId === project.id
|
|
323
315
|
? 'bg-violet-500/10 dark:bg-violet-500/20 text-violet-700 dark:text-violet-300'
|
|
324
316
|
: 'bg-slate-200/50 dark:bg-slate-800/50 text-slate-600 dark:text-slate-400 hover:bg-violet-500/10 hover:text-slate-900 dark:hover:text-slate-100'}"
|
|
@@ -327,7 +319,7 @@
|
|
|
327
319
|
>
|
|
328
320
|
<span>{getProjectInitials(project.name)}</span>
|
|
329
321
|
<span
|
|
330
|
-
class="absolute bottom-1 right-1 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {
|
|
322
|
+
class="absolute bottom-1 right-1 w-2.5 h-2.5 rounded-full border-2 border-slate-50 dark:border-slate-900/95 {getProjectStatusColor(project.id ?? '')}"
|
|
331
323
|
></span>
|
|
332
324
|
{#if activeUserCount > 0}
|
|
333
325
|
<span
|