@myrialabs/clopen 0.2.4 → 0.2.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/chat/stream-manager.ts +136 -10
- package/backend/database/queries/session-queries.ts +9 -0
- package/backend/engine/adapters/claude/error-handler.ts +7 -2
- package/backend/engine/adapters/claude/stream.ts +21 -3
- package/backend/engine/adapters/opencode/message-converter.ts +37 -2
- package/backend/index.ts +25 -3
- package/backend/preview/browser/browser-preview-service.ts +16 -17
- package/backend/preview/browser/browser-video-capture.ts +199 -156
- package/backend/preview/browser/scripts/video-stream.ts +3 -5
- package/backend/snapshot/helpers.ts +15 -2
- package/backend/ws/snapshot/restore.ts +43 -2
- package/backend/ws/user/crud.ts +6 -3
- package/frontend/components/chat/input/ChatInput.svelte +6 -1
- package/frontend/components/chat/input/components/ChatInputActions.svelte +10 -0
- package/frontend/components/chat/message/MessageBubble.svelte +22 -1
- package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
- package/frontend/components/chat/tools/components/TerminalCommand.svelte +2 -2
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
- package/frontend/components/common/media/MediaPreview.svelte +187 -0
- package/frontend/components/files/FileViewer.svelte +23 -144
- package/frontend/components/git/DiffViewer.svelte +50 -130
- package/frontend/components/git/FileChangeItem.svelte +22 -0
- package/frontend/components/preview/browser/BrowserPreview.svelte +1 -0
- package/frontend/components/preview/browser/components/Canvas.svelte +110 -21
- package/frontend/components/preview/browser/components/Container.svelte +2 -1
- package/frontend/components/preview/browser/components/Toolbar.svelte +1 -8
- package/frontend/components/preview/browser/core/coordinator.svelte.ts +12 -4
- package/frontend/components/terminal/TerminalTabs.svelte +1 -2
- package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
- package/frontend/components/workspace/WorkspaceLayout.svelte +3 -1
- package/frontend/components/workspace/panels/FilesPanel.svelte +77 -1
- package/frontend/components/workspace/panels/GitPanel.svelte +72 -28
- package/frontend/services/chat/chat.service.ts +6 -1
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +13 -5
- package/frontend/stores/core/files.svelte.ts +15 -1
- package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
- package/frontend/utils/file-type.ts +68 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/shared/constants/binary-extensions.ts +40 -0
- package/shared/types/messaging/tool.ts +1 -0
- package/shared/utils/file-type-detection.ts +9 -1
- package/static/manifest.json +16 -0
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
import type { FileNode } from '$shared/types/filesystem';
|
|
3
3
|
import LoadingSpinner from '../common/feedback/LoadingSpinner.svelte';
|
|
4
4
|
import MonacoEditor from '../common/editor/MonacoEditor.svelte';
|
|
5
|
+
import MediaPreview from '../common/media/MediaPreview.svelte';
|
|
5
6
|
import { themeStore } from '$frontend/stores/ui/theme.svelte';
|
|
6
7
|
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
7
8
|
import { getFileIcon } from '$frontend/utils/file-icon-mappings';
|
|
8
9
|
import { getFolderIcon } from '$frontend/utils/folder-icon-mappings';
|
|
9
|
-
import {
|
|
10
|
+
import { isImageFile, isSvgFile, isPdfFile, isAudioFile, isVideoFile, isBinaryFile, isBinaryContent, isPreviewableFile, formatFileSize } from '$frontend/utils/file-type';
|
|
11
|
+
import { onMount } from 'svelte';
|
|
10
12
|
import type { IconName } from '$shared/types/ui/icons';
|
|
11
13
|
import type { editor } from 'monaco-editor';
|
|
12
14
|
import { debug } from '$shared/utils/logger';
|
|
13
|
-
import ws from '$frontend/utils/ws';
|
|
14
15
|
|
|
15
16
|
// Interface untuk MonacoEditor component
|
|
16
17
|
interface MonacoEditorComponent {
|
|
@@ -38,6 +39,8 @@
|
|
|
38
39
|
onToggleWordWrap?: () => void;
|
|
39
40
|
externallyChanged?: boolean;
|
|
40
41
|
onForceReload?: () => void;
|
|
42
|
+
isBinary?: boolean;
|
|
43
|
+
projectPath?: string;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
const {
|
|
@@ -53,9 +56,20 @@
|
|
|
53
56
|
wordWrap = false,
|
|
54
57
|
onToggleWordWrap,
|
|
55
58
|
externallyChanged = false,
|
|
56
|
-
onForceReload
|
|
59
|
+
onForceReload,
|
|
60
|
+
isBinary = false,
|
|
61
|
+
projectPath = ''
|
|
57
62
|
}: Props = $props();
|
|
58
63
|
|
|
64
|
+
// Relative path for display
|
|
65
|
+
const displayPath = $derived.by(() => {
|
|
66
|
+
if (!file) return '';
|
|
67
|
+
if (projectPath && file.path.startsWith(projectPath)) {
|
|
68
|
+
return file.path.slice(projectPath.length).replace(/^[/\\]/, '');
|
|
69
|
+
}
|
|
70
|
+
return file.path;
|
|
71
|
+
});
|
|
72
|
+
|
|
59
73
|
// Theme state
|
|
60
74
|
const isDark = $derived(themeStore.isDark);
|
|
61
75
|
const monacoTheme = $derived(isDark ? 'vs-dark' : 'vs-light');
|
|
@@ -76,15 +90,9 @@
|
|
|
76
90
|
// Line highlighting state
|
|
77
91
|
let currentDecorations: string[] = $state([]);
|
|
78
92
|
|
|
79
|
-
// Image/binary blob URL state
|
|
80
|
-
let blobUrl = $state<string | null>(null);
|
|
81
|
-
|
|
82
93
|
// SVG view mode
|
|
83
94
|
let svgViewMode = $state<'visual' | 'code'>('visual');
|
|
84
95
|
|
|
85
|
-
// PDF blob URL
|
|
86
|
-
let pdfBlobUrl = $state<string | null>(null);
|
|
87
|
-
|
|
88
96
|
// Keyboard shortcut for save
|
|
89
97
|
onMount(() => {
|
|
90
98
|
function handleKeyDown(e: KeyboardEvent) {
|
|
@@ -103,61 +111,6 @@
|
|
|
103
111
|
};
|
|
104
112
|
});
|
|
105
113
|
|
|
106
|
-
// Cleanup blob URLs on destroy
|
|
107
|
-
onDestroy(() => {
|
|
108
|
-
if (blobUrl) {
|
|
109
|
-
URL.revokeObjectURL(blobUrl);
|
|
110
|
-
}
|
|
111
|
-
if (pdfBlobUrl) {
|
|
112
|
-
URL.revokeObjectURL(pdfBlobUrl);
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
// Load binary content (images, PDF) via WebSocket when file changes
|
|
117
|
-
$effect(() => {
|
|
118
|
-
if (file && (isImageFile(file.name) || isPdfFile(file.name))) {
|
|
119
|
-
loadBinaryContent();
|
|
120
|
-
} else {
|
|
121
|
-
// Cleanup if not binary
|
|
122
|
-
if (blobUrl) {
|
|
123
|
-
URL.revokeObjectURL(blobUrl);
|
|
124
|
-
blobUrl = null;
|
|
125
|
-
}
|
|
126
|
-
if (pdfBlobUrl) {
|
|
127
|
-
URL.revokeObjectURL(pdfBlobUrl);
|
|
128
|
-
pdfBlobUrl = null;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
async function loadBinaryContent() {
|
|
134
|
-
if (!file) return;
|
|
135
|
-
|
|
136
|
-
try {
|
|
137
|
-
const response = await ws.http('files:read-content', { path: file.path });
|
|
138
|
-
|
|
139
|
-
if (response.content) {
|
|
140
|
-
// Decode base64 to binary
|
|
141
|
-
const binaryString = atob(response.content);
|
|
142
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
143
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
144
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
145
|
-
}
|
|
146
|
-
const blob = new Blob([bytes], { type: response.contentType || 'application/octet-stream' });
|
|
147
|
-
|
|
148
|
-
if (isPdfFile(file.name)) {
|
|
149
|
-
if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
|
|
150
|
-
pdfBlobUrl = URL.createObjectURL(blob);
|
|
151
|
-
} else {
|
|
152
|
-
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
153
|
-
blobUrl = URL.createObjectURL(blob);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
} catch (error) {
|
|
157
|
-
debug.error('file', 'Failed to load binary content:', error);
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
114
|
// Reference content for change detection (use savedContent if provided)
|
|
162
115
|
const referenceContent = $derived(savedContentProp !== undefined ? savedContentProp : content);
|
|
163
116
|
|
|
@@ -298,38 +251,6 @@
|
|
|
298
251
|
return getFileIcon(fileName);
|
|
299
252
|
}
|
|
300
253
|
|
|
301
|
-
function isImageFile(fileName: string): boolean {
|
|
302
|
-
const extension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
|
303
|
-
return ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp'].includes(extension);
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function isSvgFile(fileName: string): boolean {
|
|
307
|
-
return fileName.toLowerCase().endsWith('.svg');
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function isPdfFile(fileName: string): boolean {
|
|
311
|
-
return fileName.toLowerCase().endsWith('.pdf');
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function isBinaryFile(fileName: string): boolean {
|
|
315
|
-
const extension = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
|
316
|
-
return ['.doc', '.docx', '.xls', '.xlsx', '.zip', '.tar', '.gz', '.exe', '.dll',
|
|
317
|
-
'.ppt', '.pptx', '.7z', '.rar', '.bz2', '.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
318
|
-
'.mp3', '.mp4', '.wav', '.avi', '.mkv', '.flv', '.sqlite', '.db'].includes(extension);
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function isVisualFile(fileName: string): boolean {
|
|
322
|
-
return isImageFile(fileName) || isSvgFile(fileName) || isPdfFile(fileName);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
function formatFileSize(bytes: number): string {
|
|
326
|
-
if (bytes === 0) return '0 B';
|
|
327
|
-
const k = 1024;
|
|
328
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
329
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
330
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
331
|
-
}
|
|
332
|
-
|
|
333
254
|
function copyToClipboard() {
|
|
334
255
|
if (editableContent) {
|
|
335
256
|
navigator.clipboard.writeText(editableContent);
|
|
@@ -363,7 +284,7 @@
|
|
|
363
284
|
{file.name}
|
|
364
285
|
</h3>
|
|
365
286
|
<p class="text-xs text-slate-600 dark:text-slate-400 truncate mt-0.5">
|
|
366
|
-
<span class="hidden sm:inline">{
|
|
287
|
+
<span class="hidden sm:inline">{displayPath} • </span> {formatFileSize(file.size || 0)}
|
|
367
288
|
</p>
|
|
368
289
|
</div>
|
|
369
290
|
</div>
|
|
@@ -406,7 +327,7 @@
|
|
|
406
327
|
{/if}
|
|
407
328
|
|
|
408
329
|
<!-- Actions for editable files -->
|
|
409
|
-
{#if file && file.type === 'file' && !isImageFile(file.name) && !isBinaryFile(file.name) && !isPdfFile(file.name) && !(isSvgFile(file.name) && svgViewMode === 'visual')}
|
|
330
|
+
{#if file && file.type === 'file' && !isBinary && !isBinaryContent(content) && !isImageFile(file.name) && !isBinaryFile(file.name) && !isPdfFile(file.name) && !isAudioFile(file.name) && !isVideoFile(file.name) && !(isSvgFile(file.name) && svgViewMode === 'visual')}
|
|
410
331
|
<!-- Word Wrap toggle -->
|
|
411
332
|
{#if onToggleWordWrap}
|
|
412
333
|
<button
|
|
@@ -496,38 +417,9 @@
|
|
|
496
417
|
This is a directory. Select a file to view its content.
|
|
497
418
|
</p>
|
|
498
419
|
</div>
|
|
499
|
-
{:else if isImageFile(file.name)}
|
|
500
|
-
<!-- Image preview -->
|
|
501
|
-
<div class="flex items-center justify-center h-full p-4 overflow-hidden">
|
|
502
|
-
{#if blobUrl}
|
|
503
|
-
<img
|
|
504
|
-
src={blobUrl}
|
|
505
|
-
alt={file.name}
|
|
506
|
-
class="max-w-full max-h-full object-contain"
|
|
507
|
-
/>
|
|
508
|
-
{:else}
|
|
509
|
-
<LoadingSpinner size="lg" />
|
|
510
|
-
{/if}
|
|
511
|
-
</div>
|
|
512
420
|
{:else if isSvgFile(file.name)}
|
|
513
|
-
<!-- SVG: visual or code view -->
|
|
514
421
|
{#if svgViewMode === 'visual'}
|
|
515
|
-
<
|
|
516
|
-
{#if blobUrl}
|
|
517
|
-
<img
|
|
518
|
-
src={blobUrl}
|
|
519
|
-
alt={file.name}
|
|
520
|
-
class="max-w-full max-h-full object-contain"
|
|
521
|
-
/>
|
|
522
|
-
{:else if content}
|
|
523
|
-
<!-- Render SVG from content directly -->
|
|
524
|
-
<div class="max-w-full max-h-full flex items-center justify-center">
|
|
525
|
-
{@html content}
|
|
526
|
-
</div>
|
|
527
|
-
{:else}
|
|
528
|
-
<LoadingSpinner size="lg" />
|
|
529
|
-
{/if}
|
|
530
|
-
</div>
|
|
422
|
+
<MediaPreview fileName={file.name} filePath={file.path} svgContent={content} />
|
|
531
423
|
{:else}
|
|
532
424
|
<!-- SVG code view (editable) -->
|
|
533
425
|
<div class="h-full flex flex-col bg-slate-50 dark:bg-slate-950">
|
|
@@ -561,22 +453,9 @@
|
|
|
561
453
|
{/if}
|
|
562
454
|
</div>
|
|
563
455
|
{/if}
|
|
564
|
-
{:else if
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
{#if pdfBlobUrl}
|
|
568
|
-
<iframe
|
|
569
|
-
src={pdfBlobUrl}
|
|
570
|
-
title={file.name}
|
|
571
|
-
class="w-full h-full border-0"
|
|
572
|
-
></iframe>
|
|
573
|
-
{:else}
|
|
574
|
-
<div class="flex items-center justify-center h-full">
|
|
575
|
-
<LoadingSpinner size="lg" />
|
|
576
|
-
</div>
|
|
577
|
-
{/if}
|
|
578
|
-
</div>
|
|
579
|
-
{:else if isBinaryFile(file.name)}
|
|
456
|
+
{:else if isPreviewableFile(file.name)}
|
|
457
|
+
<MediaPreview fileName={file.name} filePath={file.path} />
|
|
458
|
+
{:else if isBinary || isBinaryFile(file.name) || isBinaryContent(content)}
|
|
580
459
|
<div class="flex flex-col items-center justify-center h-full p-8">
|
|
581
460
|
<Icon name="lucide:file-text" class="w-16 h-16 text-slate-400 mb-4" />
|
|
582
461
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-2">
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { onDestroy, untrack } from 'svelte';
|
|
3
3
|
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
4
|
+
import MediaPreview from '$frontend/components/common/media/MediaPreview.svelte';
|
|
4
5
|
import { getFileIcon } from '$frontend/utils/file-icon-mappings';
|
|
6
|
+
import { isPreviewableFile } from '$frontend/utils/file-type';
|
|
5
7
|
import { getGitStatusBadgeLabel, getGitStatusBadgeColor } from '$frontend/utils/git-status';
|
|
6
8
|
import { themeStore } from '$frontend/stores/ui/theme.svelte';
|
|
7
9
|
import { projectState } from '$frontend/stores/core/projects.svelte';
|
|
@@ -11,7 +13,8 @@
|
|
|
11
13
|
import type { GitFileDiff } from '$shared/types/git';
|
|
12
14
|
import type { IconName } from '$shared/types/ui/icons';
|
|
13
15
|
import { debug } from '$shared/utils/logger';
|
|
14
|
-
import
|
|
16
|
+
import { requestRevealFile } from '$frontend/stores/core/files.svelte';
|
|
17
|
+
import { getVisiblePanels, workspaceState } from '$frontend/stores/ui/workspace.svelte';
|
|
15
18
|
|
|
16
19
|
interface Props {
|
|
17
20
|
diff: GitFileDiff | null;
|
|
@@ -32,91 +35,21 @@
|
|
|
32
35
|
let monacoInstance: typeof import('monaco-editor') | null = null;
|
|
33
36
|
const isDark = $derived(themeStore.isDark);
|
|
34
37
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const ext = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
|
|
43
|
-
return ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp'].includes(ext);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function isSvgFile(fileName: string): boolean {
|
|
47
|
-
return fileName.toLowerCase().endsWith('.svg');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function isPdfFile(fileName: string): boolean {
|
|
51
|
-
return fileName.toLowerCase().endsWith('.pdf');
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function isPreviewableBinary(fileName: string): boolean {
|
|
55
|
-
return isImageFile(fileName) || isSvgFile(fileName) || isPdfFile(fileName);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Load binary content for preview
|
|
59
|
-
async function loadBinaryPreview(filePath: string) {
|
|
38
|
+
// Derive absolute path for binary preview
|
|
39
|
+
const binaryPreviewPath = $derived.by(() => {
|
|
40
|
+
if (!activeDiff?.isBinary || activeDiff.status === 'D') return null;
|
|
41
|
+
const filePath = activeDiff.newPath || activeDiff.oldPath;
|
|
42
|
+
if (!filePath) return null;
|
|
43
|
+
const fileName = getFileName(filePath);
|
|
44
|
+
if (!isPreviewableFile(fileName)) return null;
|
|
60
45
|
const projectPath = projectState.currentProject?.path;
|
|
61
|
-
if (!projectPath) return;
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
isBinaryLoading = true;
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
const response = await ws.http('files:read-content', { path: absolutePath });
|
|
68
|
-
|
|
69
|
-
if (response.content) {
|
|
70
|
-
const binaryString = atob(response.content);
|
|
71
|
-
const bytes = new Uint8Array(binaryString.length);
|
|
72
|
-
for (let i = 0; i < binaryString.length; i++) {
|
|
73
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
74
|
-
}
|
|
75
|
-
const blob = new Blob([bytes], { type: response.contentType || 'application/octet-stream' });
|
|
76
|
-
|
|
77
|
-
if (isPdfFile(filePath)) {
|
|
78
|
-
if (pdfBlobUrl) URL.revokeObjectURL(pdfBlobUrl);
|
|
79
|
-
pdfBlobUrl = URL.createObjectURL(blob);
|
|
80
|
-
} else {
|
|
81
|
-
if (blobUrl) URL.revokeObjectURL(blobUrl);
|
|
82
|
-
blobUrl = URL.createObjectURL(blob);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
} catch (err) {
|
|
86
|
-
debug.error('git', 'Failed to load binary preview:', err);
|
|
87
|
-
} finally {
|
|
88
|
-
isBinaryLoading = false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function cleanupBlobUrls() {
|
|
93
|
-
if (blobUrl) {
|
|
94
|
-
URL.revokeObjectURL(blobUrl);
|
|
95
|
-
blobUrl = null;
|
|
96
|
-
}
|
|
97
|
-
if (pdfBlobUrl) {
|
|
98
|
-
URL.revokeObjectURL(pdfBlobUrl);
|
|
99
|
-
pdfBlobUrl = null;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
46
|
+
if (!projectPath) return null;
|
|
47
|
+
return `${projectPath}/${filePath}`;
|
|
48
|
+
});
|
|
102
49
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (currentDiff?.isBinary) {
|
|
107
|
-
const filePath = currentDiff.newPath || currentDiff.oldPath;
|
|
108
|
-
if (filePath && isPreviewableBinary(getFileName(filePath))) {
|
|
109
|
-
// Status 'D' means file is deleted, can't preview
|
|
110
|
-
if (currentDiff.status !== 'D') {
|
|
111
|
-
untrack(() => {
|
|
112
|
-
cleanupBlobUrls();
|
|
113
|
-
loadBinaryPreview(filePath);
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} else {
|
|
118
|
-
untrack(() => cleanupBlobUrls());
|
|
119
|
-
}
|
|
50
|
+
const binaryFileName = $derived.by(() => {
|
|
51
|
+
if (!activeDiff) return '';
|
|
52
|
+
return getFileName(activeDiff.newPath || activeDiff.oldPath);
|
|
120
53
|
});
|
|
121
54
|
|
|
122
55
|
// Build full content from hunks
|
|
@@ -162,6 +95,19 @@
|
|
|
162
95
|
return path.split(/[\\/]/).pop() || path;
|
|
163
96
|
}
|
|
164
97
|
|
|
98
|
+
function openInFilesPanel() {
|
|
99
|
+
if (!activeDiff) return;
|
|
100
|
+
const visiblePanels = getVisiblePanels(workspaceState.layout);
|
|
101
|
+
if (!visiblePanels.includes('files')) return;
|
|
102
|
+
const basePath = projectState.currentProject?.path;
|
|
103
|
+
if (!basePath) return;
|
|
104
|
+
const relativePath = activeDiff.newPath || activeDiff.oldPath;
|
|
105
|
+
const separator = basePath.includes('\\') ? '\\' : '/';
|
|
106
|
+
requestRevealFile(`${basePath}${separator}${relativePath}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isFilesPanelVisible = $derived(getVisiblePanels(workspaceState.layout).includes('files'));
|
|
110
|
+
|
|
165
111
|
async function initDiffEditor() {
|
|
166
112
|
if (!containerRef || !activeDiff) return;
|
|
167
113
|
|
|
@@ -273,7 +219,6 @@
|
|
|
273
219
|
diffEditorInstance.dispose();
|
|
274
220
|
diffEditorInstance = null;
|
|
275
221
|
}
|
|
276
|
-
cleanupBlobUrls();
|
|
277
222
|
});
|
|
278
223
|
</script>
|
|
279
224
|
|
|
@@ -288,20 +233,30 @@
|
|
|
288
233
|
<span>Select a file to view diff</span>
|
|
289
234
|
</div>
|
|
290
235
|
{:else}
|
|
291
|
-
<!-- File header
|
|
236
|
+
<!-- File header -->
|
|
292
237
|
<div class="flex-shrink-0 flex items-center justify-between px-4 py-2.5 border-b border-slate-200 dark:border-slate-700">
|
|
293
|
-
<div class="flex items-center gap-2 min-w-0 flex-1">
|
|
294
|
-
<Icon name={getFileIcon(getFileName(activeDiff.newPath || activeDiff.oldPath)) as IconName} class="w-
|
|
238
|
+
<div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
|
|
239
|
+
<Icon name={getFileIcon(getFileName(activeDiff.newPath || activeDiff.oldPath)) as IconName} class="w-7 h-7 shrink-0" />
|
|
295
240
|
<div class="min-w-0 flex-1">
|
|
296
|
-
<h3 class="text-xs font-bold text-slate-900 dark:text-slate-100 truncate">
|
|
241
|
+
<h3 class="text-xs sm:text-sm font-bold text-slate-900 dark:text-slate-100 truncate">
|
|
297
242
|
{getFileName(activeDiff.newPath || activeDiff.oldPath)}
|
|
298
243
|
</h3>
|
|
299
|
-
<p class="text-
|
|
244
|
+
<p class="text-xs text-slate-600 dark:text-slate-400 truncate mt-0.5">
|
|
300
245
|
{activeDiff.newPath || activeDiff.oldPath}
|
|
301
246
|
</p>
|
|
302
247
|
</div>
|
|
303
248
|
</div>
|
|
304
|
-
<div class="flex items-center gap-
|
|
249
|
+
<div class="flex items-center gap-1.5 sm:gap-1 flex-shrink-0">
|
|
250
|
+
{#if isFilesPanelVisible && activeDiff.status !== 'D'}
|
|
251
|
+
<button
|
|
252
|
+
type="button"
|
|
253
|
+
class="flex p-2 text-slate-600 dark:text-slate-400 hover:text-violet-600 dark:hover:text-violet-400 hover:bg-violet-50 dark:hover:bg-violet-900/30 rounded-lg transition-all duration-200 cursor-pointer"
|
|
254
|
+
onclick={openInFilesPanel}
|
|
255
|
+
title="Open in Files panel"
|
|
256
|
+
>
|
|
257
|
+
<Icon name="lucide:file-symlink" class="w-4 h-4" />
|
|
258
|
+
</button>
|
|
259
|
+
{/if}
|
|
305
260
|
<span class="text-3xs font-bold px-1.5 py-0.5 rounded {getGitStatusBadgeColor(activeDiff.status)}">
|
|
306
261
|
{getGitStatusBadgeLabel(activeDiff.status)}
|
|
307
262
|
</span>
|
|
@@ -309,7 +264,6 @@
|
|
|
309
264
|
</div>
|
|
310
265
|
|
|
311
266
|
{#if activeDiff.isBinary}
|
|
312
|
-
{@const fileName = getFileName(activeDiff.newPath || activeDiff.oldPath)}
|
|
313
267
|
{@const isDeleted = activeDiff.status === 'D'}
|
|
314
268
|
{#if isDeleted}
|
|
315
269
|
<!-- Deleted binary file -->
|
|
@@ -320,43 +274,9 @@
|
|
|
320
274
|
This binary file has been deleted and cannot be previewed.
|
|
321
275
|
</p>
|
|
322
276
|
</div>
|
|
323
|
-
{:else if
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
{#if isBinaryLoading}
|
|
327
|
-
<div class="w-5 h-5 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
|
|
328
|
-
{:else if blobUrl}
|
|
329
|
-
<img
|
|
330
|
-
src={blobUrl}
|
|
331
|
-
alt={fileName}
|
|
332
|
-
class="max-w-full max-h-full object-contain"
|
|
333
|
-
/>
|
|
334
|
-
{:else}
|
|
335
|
-
<div class="flex flex-col items-center gap-2 text-slate-500 text-xs">
|
|
336
|
-
<Icon name="lucide:image-off" class="w-8 h-8 opacity-40" />
|
|
337
|
-
<span>Failed to load preview</span>
|
|
338
|
-
</div>
|
|
339
|
-
{/if}
|
|
340
|
-
</div>
|
|
341
|
-
{:else if isPdfFile(fileName)}
|
|
342
|
-
<!-- PDF preview -->
|
|
343
|
-
<div class="flex-1 h-full w-full">
|
|
344
|
-
{#if isBinaryLoading}
|
|
345
|
-
<div class="flex items-center justify-center h-full">
|
|
346
|
-
<div class="w-5 h-5 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
|
|
347
|
-
</div>
|
|
348
|
-
{:else if pdfBlobUrl}
|
|
349
|
-
<iframe
|
|
350
|
-
src={pdfBlobUrl}
|
|
351
|
-
title={fileName}
|
|
352
|
-
class="w-full h-full border-0"
|
|
353
|
-
></iframe>
|
|
354
|
-
{:else}
|
|
355
|
-
<div class="flex flex-col items-center justify-center h-full gap-2 text-slate-500 text-xs">
|
|
356
|
-
<Icon name="lucide:file-x" class="w-8 h-8 opacity-40" />
|
|
357
|
-
<span>Failed to load PDF preview</span>
|
|
358
|
-
</div>
|
|
359
|
-
{/if}
|
|
277
|
+
{:else if binaryPreviewPath}
|
|
278
|
+
<div class="flex-1 overflow-hidden">
|
|
279
|
+
<MediaPreview fileName={binaryFileName} filePath={binaryPreviewPath} />
|
|
360
280
|
</div>
|
|
361
281
|
{:else}
|
|
362
282
|
<!-- Generic binary file -->
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
import { getGitStatusLabel, getGitStatusColor } from '$frontend/utils/git-status';
|
|
5
5
|
import type { GitFileChange } from '$shared/types/git';
|
|
6
6
|
import type { IconName } from '$shared/types/ui/icons';
|
|
7
|
+
import { requestRevealFile } from '$frontend/stores/core/files.svelte';
|
|
8
|
+
import { getVisiblePanels, workspaceState } from '$frontend/stores/ui/workspace.svelte';
|
|
9
|
+
import { projectState } from '$frontend/stores/core/projects.svelte';
|
|
7
10
|
|
|
8
11
|
interface Props {
|
|
9
12
|
file: GitFileChange;
|
|
@@ -28,6 +31,15 @@
|
|
|
28
31
|
return parts.join('/');
|
|
29
32
|
});
|
|
30
33
|
const fileIcon = $derived(getFileIcon(fileName) as IconName);
|
|
34
|
+
const isFilesPanelVisible = $derived(getVisiblePanels(workspaceState.layout).includes('files'));
|
|
35
|
+
|
|
36
|
+
function openInFilesPanel(e: MouseEvent) {
|
|
37
|
+
e.stopPropagation();
|
|
38
|
+
const basePath = projectState.currentProject?.path;
|
|
39
|
+
if (!basePath) return;
|
|
40
|
+
const separator = basePath.includes('\\') ? '\\' : '/';
|
|
41
|
+
requestRevealFile(`${basePath}${separator}${file.path}`);
|
|
42
|
+
}
|
|
31
43
|
</script>
|
|
32
44
|
|
|
33
45
|
<div
|
|
@@ -57,6 +69,16 @@
|
|
|
57
69
|
|
|
58
70
|
<!-- Actions - always visible -->
|
|
59
71
|
<div class="flex items-center gap-0.5 shrink-0">
|
|
72
|
+
{#if isFilesPanelVisible && statusCode !== 'D'}
|
|
73
|
+
<button
|
|
74
|
+
type="button"
|
|
75
|
+
class="flex items-center justify-center w-6 h-6 rounded-md text-slate-400 hover:bg-violet-500/10 hover:text-violet-500 transition-colors bg-transparent border-none cursor-pointer"
|
|
76
|
+
onclick={openInFilesPanel}
|
|
77
|
+
title="Open in Files panel"
|
|
78
|
+
>
|
|
79
|
+
<Icon name="lucide:file-symlink" class="w-3.5 h-3.5" />
|
|
80
|
+
</button>
|
|
81
|
+
{/if}
|
|
60
82
|
{#if section === 'staged'}
|
|
61
83
|
<button
|
|
62
84
|
type="button"
|
|
@@ -160,6 +160,7 @@
|
|
|
160
160
|
if (currentProjectId && currentProjectId !== previousProjectId && previousProjectId !== '') {
|
|
161
161
|
debug.log('preview', `🔄 Project changed: ${previousProjectId} → ${currentProjectId}`);
|
|
162
162
|
previousProjectId = currentProjectId;
|
|
163
|
+
sessionsRecovered = false; // Reset for new project (enables empty tab creation if needed)
|
|
163
164
|
coordinator.switchProject();
|
|
164
165
|
}
|
|
165
166
|
});
|