@myrialabs/clopen 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/engine/adapters/claude/stream.ts +107 -0
- package/backend/engine/adapters/opencode/message-converter.ts +37 -2
- package/backend/engine/adapters/opencode/stream.ts +81 -1
- package/backend/engine/types.ts +17 -0
- package/backend/git/git-service.ts +2 -1
- package/backend/ws/git/commit-message.ts +108 -0
- package/backend/ws/git/index.ts +3 -1
- package/backend/ws/system/index.ts +7 -1
- package/backend/ws/system/operations.ts +28 -2
- package/backend/ws/user/crud.ts +6 -3
- package/frontend/App.svelte +3 -0
- package/frontend/components/auth/SetupPage.svelte +2 -2
- package/frontend/components/chat/input/ChatInput.svelte +1 -1
- package/frontend/components/chat/message/ChatMessage.svelte +64 -16
- package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
- package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
- package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
- package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
- package/frontend/components/common/feedback/UpdateBanner.svelte +17 -6
- package/frontend/components/common/media/MediaPreview.svelte +187 -0
- package/frontend/components/files/FileViewer.svelte +11 -143
- package/frontend/components/git/BranchManager.svelte +143 -155
- package/frontend/components/git/CommitForm.svelte +61 -11
- package/frontend/components/git/DiffViewer.svelte +50 -130
- package/frontend/components/git/FileChangeItem.svelte +22 -0
- package/frontend/components/settings/SettingsModal.svelte +1 -1
- package/frontend/components/settings/SettingsView.svelte +1 -1
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
- package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
- package/frontend/components/settings/git/GitSettings.svelte +392 -0
- package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
- package/frontend/components/settings/model/ModelSettings.svelte +172 -289
- package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
- package/frontend/components/workspace/PanelHeader.svelte +1 -3
- package/frontend/components/workspace/WorkspaceLayout.svelte +14 -2
- package/frontend/components/workspace/panels/FilesPanel.svelte +76 -1
- package/frontend/components/workspace/panels/GitPanel.svelte +84 -33
- package/frontend/main.ts +4 -0
- package/frontend/stores/core/files.svelte.ts +15 -1
- package/frontend/stores/features/settings.svelte.ts +13 -2
- package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
- package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
- package/frontend/stores/ui/update.svelte.ts +45 -4
- package/frontend/utils/file-type.ts +68 -0
- package/index.html +1 -0
- package/package.json +1 -1
- package/shared/constants/binary-extensions.ts +40 -0
- package/shared/types/git.ts +15 -0
- package/shared/types/messaging/tool.ts +1 -0
- package/shared/types/stores/settings.ts +12 -0
- package/shared/utils/file-type-detection.ts +9 -1
- package/static/manifest.json +16 -0
|
@@ -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"
|
|
@@ -219,7 +219,7 @@
|
|
|
219
219
|
<!-- Content Area -->
|
|
220
220
|
<main class="flex-1 flex flex-col min-w-0 overflow-hidden">
|
|
221
221
|
<div class="flex-1 overflow-y-auto p-4 md:p-5">
|
|
222
|
-
{#if activeSection === '
|
|
222
|
+
{#if activeSection === 'models'}
|
|
223
223
|
<div in:fly={{ x: 20, duration: 200 }}>
|
|
224
224
|
<ModelSettings />
|
|
225
225
|
</div>
|
|
@@ -383,9 +383,9 @@
|
|
|
383
383
|
|
|
384
384
|
<div class="space-y-6">
|
|
385
385
|
<!-- Header -->
|
|
386
|
-
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">
|
|
386
|
+
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Engines</h3>
|
|
387
387
|
<p class="text-sm text-slate-600 dark:text-slate-500 mb-4">
|
|
388
|
-
Manage
|
|
388
|
+
Manage engine installations and accounts
|
|
389
389
|
</p>
|
|
390
390
|
|
|
391
391
|
<!-- Claude Code Card -->
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { systemSettings, updateSystemSettings } from '$frontend/stores/features/settings.svelte';
|
|
3
|
-
import { updateState, checkForUpdate, runUpdate } from '$frontend/stores/ui/update.svelte';
|
|
3
|
+
import { updateState, checkForUpdate, runUpdate, showRestartModal } from '$frontend/stores/ui/update.svelte';
|
|
4
4
|
import Icon from '../../common/display/Icon.svelte';
|
|
5
5
|
|
|
6
6
|
function toggleAutoUpdate() {
|
|
@@ -88,10 +88,17 @@
|
|
|
88
88
|
</div>
|
|
89
89
|
{/if}
|
|
90
90
|
|
|
91
|
-
{#if updateState.updateSuccess}
|
|
91
|
+
{#if updateState.updateSuccess || updateState.pendingRestart}
|
|
92
92
|
<div class="mt-3 flex items-center gap-2 px-3 py-2 bg-emerald-500/10 border border-emerald-500/20 rounded-lg">
|
|
93
93
|
<Icon name="lucide:circle-check" class="w-4 h-4 text-emerald-500 shrink-0" />
|
|
94
|
-
<span class="text-xs text-emerald-600 dark:text-emerald-400">Updated
|
|
94
|
+
<span class="text-xs text-emerald-600 dark:text-emerald-400">Updated to v{updateState.latestVersion} — restart required</span>
|
|
95
|
+
<button
|
|
96
|
+
type="button"
|
|
97
|
+
onclick={() => showRestartModal()}
|
|
98
|
+
class="text-xs font-semibold text-emerald-600 dark:text-emerald-400 underline underline-offset-2 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors"
|
|
99
|
+
>
|
|
100
|
+
How to restart
|
|
101
|
+
</button>
|
|
95
102
|
</div>
|
|
96
103
|
{/if}
|
|
97
104
|
</div>
|