@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.
Files changed (53) hide show
  1. package/backend/engine/adapters/claude/stream.ts +107 -0
  2. package/backend/engine/adapters/opencode/message-converter.ts +37 -2
  3. package/backend/engine/adapters/opencode/stream.ts +81 -1
  4. package/backend/engine/types.ts +17 -0
  5. package/backend/git/git-service.ts +2 -1
  6. package/backend/ws/git/commit-message.ts +108 -0
  7. package/backend/ws/git/index.ts +3 -1
  8. package/backend/ws/system/index.ts +7 -1
  9. package/backend/ws/system/operations.ts +28 -2
  10. package/backend/ws/user/crud.ts +6 -3
  11. package/frontend/App.svelte +3 -0
  12. package/frontend/components/auth/SetupPage.svelte +2 -2
  13. package/frontend/components/chat/input/ChatInput.svelte +1 -1
  14. package/frontend/components/chat/message/ChatMessage.svelte +64 -16
  15. package/frontend/components/chat/tools/components/FileHeader.svelte +19 -5
  16. package/frontend/components/chat/widgets/FloatingTodoList.svelte +30 -26
  17. package/frontend/components/checkpoint/ConflictResolutionModal.svelte +189 -0
  18. package/frontend/components/checkpoint/TimelineModal.svelte +7 -162
  19. package/frontend/components/common/feedback/RestartRequiredModal.svelte +53 -0
  20. package/frontend/components/common/feedback/UpdateBanner.svelte +17 -6
  21. package/frontend/components/common/media/MediaPreview.svelte +187 -0
  22. package/frontend/components/files/FileViewer.svelte +11 -143
  23. package/frontend/components/git/BranchManager.svelte +143 -155
  24. package/frontend/components/git/CommitForm.svelte +61 -11
  25. package/frontend/components/git/DiffViewer.svelte +50 -130
  26. package/frontend/components/git/FileChangeItem.svelte +22 -0
  27. package/frontend/components/settings/SettingsModal.svelte +1 -1
  28. package/frontend/components/settings/SettingsView.svelte +1 -1
  29. package/frontend/components/settings/engines/AIEnginesSettings.svelte +2 -2
  30. package/frontend/components/settings/general/UpdateSettings.svelte +10 -3
  31. package/frontend/components/settings/git/GitSettings.svelte +392 -0
  32. package/frontend/components/settings/model/EngineModelPicker.svelte +275 -0
  33. package/frontend/components/settings/model/ModelSettings.svelte +172 -289
  34. package/frontend/components/workspace/DesktopNavigator.svelte +27 -1
  35. package/frontend/components/workspace/PanelHeader.svelte +1 -3
  36. package/frontend/components/workspace/WorkspaceLayout.svelte +14 -2
  37. package/frontend/components/workspace/panels/FilesPanel.svelte +76 -1
  38. package/frontend/components/workspace/panels/GitPanel.svelte +84 -33
  39. package/frontend/main.ts +4 -0
  40. package/frontend/stores/core/files.svelte.ts +15 -1
  41. package/frontend/stores/features/settings.svelte.ts +13 -2
  42. package/frontend/stores/ui/settings-modal.svelte.ts +9 -9
  43. package/frontend/stores/ui/todo-panel.svelte.ts +39 -0
  44. package/frontend/stores/ui/update.svelte.ts +45 -4
  45. package/frontend/utils/file-type.ts +68 -0
  46. package/index.html +1 -0
  47. package/package.json +1 -1
  48. package/shared/constants/binary-extensions.ts +40 -0
  49. package/shared/types/git.ts +15 -0
  50. package/shared/types/messaging/tool.ts +1 -0
  51. package/shared/types/stores/settings.ts +12 -0
  52. package/shared/utils/file-type-detection.ts +9 -1
  53. package/static/manifest.json +16 -0
@@ -1,7 +1,9 @@
1
1
  <script lang="ts">
2
- import { onMount, onDestroy, untrack } from 'svelte';
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 ws from '$frontend/utils/ws';
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
- // Binary file preview state
36
- let blobUrl = $state<string | null>(null);
37
- let pdfBlobUrl = $state<string | null>(null);
38
- let isBinaryLoading = $state(false);
39
-
40
- // File type detection helpers
41
- function isImageFile(fileName: string): boolean {
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
- const absolutePath = `${projectPath}/${filePath}`;
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
- // Load binary preview when activeDiff changes to a binary file
104
- $effect(() => {
105
- const currentDiff = activeDiff;
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 (like FileViewer) -->
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-5 h-5 shrink-0" />
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-3xs text-slate-500 dark:text-slate-400 truncate mt-0.5">
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-2 shrink-0">
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 isImageFile(fileName) || isSvgFile(fileName)}
324
- <!-- Image / SVG preview -->
325
- <div class="flex-1 flex items-center justify-center p-4 overflow-hidden bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2220%22%20height%3D%2220%22%3E%3Crect%20width%3D%2220%22%20height%3D%2220%22%20fill%3D%22%23f0f0f0%22%2F%3E%3Crect%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Crect%20x%3D%2210%22%20y%3D%2210%22%20width%3D%2210%22%20height%3D%2210%22%20fill%3D%22%23e0e0e0%22%2F%3E%3C%2Fsvg%3E')]">
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 === 'model'}
222
+ {#if activeSection === 'models'}
223
223
  <div in:fly={{ x: 20, duration: 200 }}>
224
224
  <ModelSettings />
225
225
  </div>
@@ -23,7 +23,7 @@
23
23
  <div class="flex-1 overflow-auto">
24
24
  <div class="space-y-6">
25
25
 
26
- <!-- Model Configuration -->
26
+ <!-- AI Model (Assistant + Commit Message) -->
27
27
  <ModelSettings />
28
28
 
29
29
  <!-- Appearance Configuration -->
@@ -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">AI Engine</h3>
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 AI engine installations and accounts
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 successfully — restart clopen to apply</span>
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>