@myrialabs/clopen 0.1.7 → 0.1.9
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/database/migrations/023_create_user_unread_sessions_table.ts +32 -0
- package/backend/lib/database/migrations/index.ts +7 -0
- package/backend/lib/database/queries/session-queries.ts +37 -0
- package/backend/lib/git/git-service.ts +1 -0
- package/backend/ws/sessions/crud.ts +34 -2
- package/backend/ws/user/crud.ts +8 -4
- package/bun.lock +34 -12
- package/frontend/lib/components/common/MonacoEditor.svelte +6 -6
- package/frontend/lib/components/common/xterm/XTerm.svelte +27 -108
- package/frontend/lib/components/common/xterm/terminal-config.ts +2 -2
- package/frontend/lib/components/common/xterm/types.ts +1 -0
- package/frontend/lib/components/common/xterm/xterm-service.ts +69 -20
- package/frontend/lib/components/files/FileTree.svelte +4 -6
- package/frontend/lib/components/files/FileViewer.svelte +45 -101
- package/frontend/lib/components/git/CommitForm.svelte +1 -1
- package/frontend/lib/components/git/GitLog.svelte +141 -101
- package/frontend/lib/components/preview/browser/components/Toolbar.svelte +81 -72
- package/frontend/lib/components/settings/SettingsModal.svelte +1 -8
- package/frontend/lib/components/settings/engines/AIEnginesSettings.svelte +3 -3
- package/frontend/lib/components/terminal/Terminal.svelte +1 -1
- package/frontend/lib/components/terminal/TerminalTabs.svelte +28 -26
- package/frontend/lib/components/workspace/PanelHeader.svelte +639 -623
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +3 -2
- package/frontend/lib/components/workspace/panels/GitPanel.svelte +34 -92
- package/frontend/lib/stores/core/app.svelte.ts +46 -0
- package/frontend/lib/stores/core/sessions.svelte.ts +24 -3
- package/frontend/lib/stores/ui/workspace.svelte.ts +14 -14
- package/package.json +8 -6
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { browser } from '$frontend/lib/app-environment';
|
|
9
|
-
import type { Terminal } from 'xterm';
|
|
9
|
+
import type { Terminal } from '@xterm/xterm';
|
|
10
10
|
import type { FitAddon } from '@xterm/addon-fit';
|
|
11
11
|
import type { WebLinksAddon } from '@xterm/addon-web-links';
|
|
12
|
+
import type { ClipboardAddon } from '@xterm/addon-clipboard';
|
|
13
|
+
import type { Unicode11Addon } from '@xterm/addon-unicode11';
|
|
14
|
+
import type { LigaturesAddon } from '@xterm/addon-ligatures';
|
|
12
15
|
import type { TerminalLine } from '$shared/types/terminal';
|
|
13
16
|
import { terminalConfig } from './terminal-config';
|
|
14
17
|
import { debug } from '$shared/utils/logger';
|
|
@@ -18,12 +21,16 @@ export class XTermService {
|
|
|
18
21
|
public terminal: Terminal | null = null;
|
|
19
22
|
public fitAddon: FitAddon | null = null;
|
|
20
23
|
public webLinksAddon: WebLinksAddon | null = null;
|
|
24
|
+
public clipboardAddon: ClipboardAddon | null = null;
|
|
25
|
+
private unicode11Addon: Unicode11Addon | null = null;
|
|
26
|
+
private ligaturesAddon: LigaturesAddon | null = null;
|
|
21
27
|
public isInitialized = false;
|
|
22
28
|
public isReady = false;
|
|
23
29
|
|
|
24
30
|
private sessionId: string | null = null;
|
|
25
31
|
private resizeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
26
32
|
private inputDisposable: any = null;
|
|
33
|
+
private lastSentDims: { cols: number; rows: number } | null = null;
|
|
27
34
|
|
|
28
35
|
constructor() {
|
|
29
36
|
// Service is stateless by design
|
|
@@ -49,27 +56,39 @@ export class XTermService {
|
|
|
49
56
|
this.terminal = null;
|
|
50
57
|
this.fitAddon = null;
|
|
51
58
|
this.webLinksAddon = null;
|
|
59
|
+
this.clipboardAddon = null;
|
|
60
|
+
this.unicode11Addon = null;
|
|
61
|
+
this.ligaturesAddon = null;
|
|
52
62
|
}
|
|
53
63
|
|
|
54
64
|
try {
|
|
55
65
|
debug.log('terminal', '🚀 Initializing XTerm...');
|
|
56
66
|
|
|
57
67
|
// Dynamic import xterm classes
|
|
58
|
-
const [{ Terminal }, { FitAddon }, { WebLinksAddon }] = await Promise.all([
|
|
59
|
-
import('xterm'),
|
|
68
|
+
const [{ Terminal }, { FitAddon }, { WebLinksAddon }, { ClipboardAddon }, { Unicode11Addon }] = await Promise.all([
|
|
69
|
+
import('@xterm/xterm'),
|
|
60
70
|
import('@xterm/addon-fit'),
|
|
61
|
-
import('@xterm/addon-web-links')
|
|
71
|
+
import('@xterm/addon-web-links'),
|
|
72
|
+
import('@xterm/addon-clipboard'),
|
|
73
|
+
import('@xterm/addon-unicode11')
|
|
62
74
|
]);
|
|
63
75
|
|
|
64
76
|
// Create terminal instance
|
|
65
77
|
this.terminal = new Terminal(terminalConfig);
|
|
66
78
|
|
|
67
|
-
// Create and load addons
|
|
79
|
+
// Create and load core addons
|
|
68
80
|
this.fitAddon = new FitAddon();
|
|
69
81
|
this.webLinksAddon = new WebLinksAddon();
|
|
82
|
+
this.clipboardAddon = new ClipboardAddon();
|
|
83
|
+
this.unicode11Addon = new Unicode11Addon();
|
|
70
84
|
|
|
71
85
|
this.terminal.loadAddon(this.fitAddon);
|
|
72
86
|
this.terminal.loadAddon(this.webLinksAddon);
|
|
87
|
+
this.terminal.loadAddon(this.clipboardAddon);
|
|
88
|
+
this.terminal.loadAddon(this.unicode11Addon);
|
|
89
|
+
|
|
90
|
+
// Enable Unicode 11 for better character width support
|
|
91
|
+
this.terminal.unicode.activeVersion = '11';
|
|
73
92
|
|
|
74
93
|
// Open terminal in container
|
|
75
94
|
this.terminal.open(container);
|
|
@@ -82,6 +101,16 @@ export class XTermService {
|
|
|
82
101
|
debug.log('terminal', '⚠️ Initial fit failed (container may have zero dimensions), will retry on resize');
|
|
83
102
|
}
|
|
84
103
|
|
|
104
|
+
// Try ligatures addon for font ligature rendering (non-critical)
|
|
105
|
+
try {
|
|
106
|
+
const { LigaturesAddon } = await import('@xterm/addon-ligatures');
|
|
107
|
+
this.ligaturesAddon = new LigaturesAddon();
|
|
108
|
+
this.terminal.loadAddon(this.ligaturesAddon);
|
|
109
|
+
debug.log('terminal', '🔤 Font ligatures enabled');
|
|
110
|
+
} catch {
|
|
111
|
+
debug.log('terminal', '⚠️ Ligatures addon not available');
|
|
112
|
+
}
|
|
113
|
+
|
|
85
114
|
this.isInitialized = true;
|
|
86
115
|
this.isReady = true;
|
|
87
116
|
|
|
@@ -94,6 +123,9 @@ export class XTermService {
|
|
|
94
123
|
}
|
|
95
124
|
this.fitAddon = null;
|
|
96
125
|
this.webLinksAddon = null;
|
|
126
|
+
this.clipboardAddon = null;
|
|
127
|
+
this.unicode11Addon = null;
|
|
128
|
+
this.ligaturesAddon = null;
|
|
97
129
|
debug.error('terminal', '❌ Failed to initialize XTerm:', error);
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -180,11 +212,12 @@ export class XTermService {
|
|
|
180
212
|
}
|
|
181
213
|
|
|
182
214
|
/**
|
|
183
|
-
* Update terminal font size and refit to container
|
|
215
|
+
* Update terminal font size, line height, and refit to container
|
|
184
216
|
*/
|
|
185
|
-
updateFontSize(size: number, sessionId?: string): void {
|
|
217
|
+
updateFontSize(size: number, lineHeight: number, sessionId?: string): void {
|
|
186
218
|
if (!this.terminal) return;
|
|
187
219
|
this.terminal.options.fontSize = size;
|
|
220
|
+
this.terminal.options.lineHeight = lineHeight / size;
|
|
188
221
|
this.fit(sessionId);
|
|
189
222
|
}
|
|
190
223
|
|
|
@@ -262,6 +295,12 @@ export class XTermService {
|
|
|
262
295
|
const dims = this.fitAddon.proposeDimensions();
|
|
263
296
|
|
|
264
297
|
if (dims && sessionId) {
|
|
298
|
+
// Skip if dimensions haven't changed
|
|
299
|
+
if (this.lastSentDims && this.lastSentDims.cols === dims.cols && this.lastSentDims.rows === dims.rows) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
this.lastSentDims = { cols: dims.cols, rows: dims.rows };
|
|
303
|
+
|
|
265
304
|
// Notify backend of new terminal size via WebSocket HTTP
|
|
266
305
|
debug.log('terminal', `🔧 Syncing terminal size: ${dims.cols}x${dims.rows}`);
|
|
267
306
|
ws.http('terminal:resize', {
|
|
@@ -293,20 +332,11 @@ export class XTermService {
|
|
|
293
332
|
scrollToBottomIfNearEnd(): void {
|
|
294
333
|
if (!this.terminal) return;
|
|
295
334
|
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
this.scrollToBottom();
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const scrollTop = viewport._viewportElement.scrollTop;
|
|
303
|
-
const scrollHeight = viewport._viewportElement.scrollHeight;
|
|
304
|
-
const clientHeight = viewport._viewportElement.clientHeight;
|
|
335
|
+
const buffer = this.terminal.buffer.active;
|
|
336
|
+
const isNearBottom = buffer.viewportY >= buffer.baseY - 3;
|
|
305
337
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
if (scrollHeight - scrollTop - clientHeight < threshold) {
|
|
309
|
-
this.scrollToBottom();
|
|
338
|
+
if (isNearBottom) {
|
|
339
|
+
this.terminal.scrollToBottom();
|
|
310
340
|
}
|
|
311
341
|
}
|
|
312
342
|
|
|
@@ -361,6 +391,21 @@ export class XTermService {
|
|
|
361
391
|
};
|
|
362
392
|
}
|
|
363
393
|
|
|
394
|
+
/**
|
|
395
|
+
* Paste text by sending to PTY via WebSocket
|
|
396
|
+
*/
|
|
397
|
+
pasteText(text: string): void {
|
|
398
|
+
if (!this.sessionId || !text) return;
|
|
399
|
+
try {
|
|
400
|
+
ws.emit('terminal:input', {
|
|
401
|
+
sessionId: this.sessionId,
|
|
402
|
+
data: text
|
|
403
|
+
});
|
|
404
|
+
} catch (error) {
|
|
405
|
+
debug.error('terminal', '❌ Error pasting text:', error);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
364
409
|
/**
|
|
365
410
|
* Cleanup terminal resources
|
|
366
411
|
*/
|
|
@@ -382,6 +427,10 @@ export class XTermService {
|
|
|
382
427
|
|
|
383
428
|
this.fitAddon = null;
|
|
384
429
|
this.webLinksAddon = null;
|
|
430
|
+
this.clipboardAddon = null;
|
|
431
|
+
this.unicode11Addon = null;
|
|
432
|
+
this.ligaturesAddon = null;
|
|
433
|
+
this.lastSentDims = null;
|
|
385
434
|
this.isInitialized = false;
|
|
386
435
|
this.isReady = false;
|
|
387
436
|
}
|
|
@@ -420,9 +420,9 @@
|
|
|
420
420
|
|
|
421
421
|
<div class="relative flex flex-col h-full overflow-hidden">
|
|
422
422
|
<!-- Modern Header -->
|
|
423
|
-
<div class="px-5 py-
|
|
423
|
+
<div class="px-5 py-2.5 border-b border-slate-200 dark:border-slate-700">
|
|
424
424
|
<div class="flex items-start justify-between gap-2">
|
|
425
|
-
<div class="flex-1 min-w-0">
|
|
425
|
+
<div class="flex-1 min-w-0" title={projectState.currentProject?.path}>
|
|
426
426
|
<h3 class="text-sm font-bold text-slate-900 dark:text-slate-100">
|
|
427
427
|
{projectState.currentProject?.name}
|
|
428
428
|
</h3>
|
|
@@ -466,9 +466,8 @@
|
|
|
466
466
|
<div class="relative flex border-b border-slate-200 dark:border-slate-700">
|
|
467
467
|
<button
|
|
468
468
|
onclick={() => { searchVisible = false; }}
|
|
469
|
-
class="relative flex-1 flex items-center justify-center
|
|
469
|
+
class="relative flex-1 flex items-center justify-center px-3 py-2 text-xs font-medium transition-colors {!searchVisible ? 'text-violet-600 dark:text-violet-400' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
470
470
|
>
|
|
471
|
-
<Icon name="lucide:folder" class="w-3.5 h-3.5" />
|
|
472
471
|
Explorer
|
|
473
472
|
{#if !searchVisible}
|
|
474
473
|
<span class="absolute bottom-0 inset-x-0 h-px bg-violet-600 dark:bg-violet-400"></span>
|
|
@@ -476,9 +475,8 @@
|
|
|
476
475
|
</button>
|
|
477
476
|
<button
|
|
478
477
|
onclick={switchToSearch}
|
|
479
|
-
class="relative flex-1 flex items-center justify-center
|
|
478
|
+
class="relative flex-1 flex items-center justify-center px-3 py-2 text-xs font-medium transition-colors {searchVisible ? 'text-violet-600 dark:text-violet-400' : 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
480
479
|
>
|
|
481
|
-
<Icon name="lucide:search" class="w-3.5 h-3.5" />
|
|
482
480
|
Search
|
|
483
481
|
{#if searchVisible}
|
|
484
482
|
<span class="absolute bottom-0 inset-x-0 h-px bg-violet-600 dark:bg-violet-400"></span>
|
|
@@ -85,33 +85,8 @@
|
|
|
85
85
|
// PDF blob URL
|
|
86
86
|
let pdfBlobUrl = $state<string | null>(null);
|
|
87
87
|
|
|
88
|
-
//
|
|
89
|
-
let containerRef = $state<HTMLDivElement | null>(null);
|
|
90
|
-
let editorHeight = $state('0px'); // Default height
|
|
91
|
-
|
|
92
|
-
// Update editor height based on container
|
|
93
|
-
function updateEditorHeight() {
|
|
94
|
-
if (containerRef) {
|
|
95
|
-
const rect = containerRef.getBoundingClientRect();
|
|
96
|
-
// Get actual available height
|
|
97
|
-
const availableHeight = rect.height;
|
|
98
|
-
if (availableHeight > 0) {
|
|
99
|
-
const adjustedHeight = availableHeight;
|
|
100
|
-
editorHeight = `${Math.max(200, adjustedHeight)}px`;
|
|
101
|
-
} else {
|
|
102
|
-
const viewportHeight = window.innerHeight;
|
|
103
|
-
const estimatedHeight = viewportHeight - 200;
|
|
104
|
-
editorHeight = `${Math.max(400, estimatedHeight)}px`;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Update height on mount and resize
|
|
88
|
+
// Keyboard shortcut for save
|
|
110
89
|
onMount(() => {
|
|
111
|
-
setTimeout(updateEditorHeight, 100);
|
|
112
|
-
|
|
113
|
-
window.addEventListener('resize', updateEditorHeight);
|
|
114
|
-
|
|
115
90
|
function handleKeyDown(e: KeyboardEvent) {
|
|
116
91
|
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
|
|
117
92
|
e.preventDefault();
|
|
@@ -123,21 +98,7 @@
|
|
|
123
98
|
|
|
124
99
|
window.addEventListener('keydown', handleKeyDown);
|
|
125
100
|
|
|
126
|
-
if (containerRef && typeof ResizeObserver !== 'undefined') {
|
|
127
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
128
|
-
updateEditorHeight();
|
|
129
|
-
});
|
|
130
|
-
resizeObserver.observe(containerRef);
|
|
131
|
-
|
|
132
|
-
return () => {
|
|
133
|
-
resizeObserver.disconnect();
|
|
134
|
-
window.removeEventListener('resize', updateEditorHeight);
|
|
135
|
-
window.removeEventListener('keydown', handleKeyDown);
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
101
|
return () => {
|
|
140
|
-
window.removeEventListener('resize', updateEditorHeight);
|
|
141
102
|
window.removeEventListener('keydown', handleKeyDown);
|
|
142
103
|
};
|
|
143
104
|
});
|
|
@@ -152,14 +113,6 @@
|
|
|
152
113
|
}
|
|
153
114
|
});
|
|
154
115
|
|
|
155
|
-
// Update height when container ref changes
|
|
156
|
-
$effect(() => {
|
|
157
|
-
if (containerRef) {
|
|
158
|
-
updateEditorHeight();
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
|
|
163
116
|
// Load binary content (images, PDF) via WebSocket when file changes
|
|
164
117
|
$effect(() => {
|
|
165
118
|
if (file && (isImageFile(file.name) || isPdfFile(file.name))) {
|
|
@@ -577,38 +530,35 @@
|
|
|
577
530
|
</div>
|
|
578
531
|
{:else}
|
|
579
532
|
<!-- SVG code view (editable) -->
|
|
580
|
-
<div class="h-full flex flex-col
|
|
581
|
-
<div class="flex-1
|
|
582
|
-
<div class="
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
/>
|
|
599
|
-
{/key}
|
|
600
|
-
</div>
|
|
601
|
-
|
|
602
|
-
{#if hasChanges}
|
|
603
|
-
<div class="flex-shrink-0 p-4 bg-amber-50 dark:bg-amber-900/30 border-t border-amber-200 dark:border-amber-800">
|
|
604
|
-
<div class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
|
605
|
-
<Icon name="lucide:circle-alert" class="w-3 h-3" />
|
|
606
|
-
Unsaved changes
|
|
607
|
-
</div>
|
|
608
|
-
</div>
|
|
609
|
-
{/if}
|
|
533
|
+
<div class="h-full flex flex-col bg-slate-50 dark:bg-slate-950">
|
|
534
|
+
<div class="flex-1 relative overflow-hidden">
|
|
535
|
+
<div class="absolute inset-0">
|
|
536
|
+
{#key themeKey}
|
|
537
|
+
<MonacoEditor
|
|
538
|
+
bind:this={monacoEditorRef}
|
|
539
|
+
bind:value={editableContent}
|
|
540
|
+
language="xml"
|
|
541
|
+
readonly={false}
|
|
542
|
+
onChange={handleContentChange}
|
|
543
|
+
options={{
|
|
544
|
+
minimap: { enabled: false },
|
|
545
|
+
wordWrap: 'off',
|
|
546
|
+
renderWhitespace: 'none',
|
|
547
|
+
mouseWheelZoom: false
|
|
548
|
+
}}
|
|
549
|
+
/>
|
|
550
|
+
{/key}
|
|
610
551
|
</div>
|
|
611
552
|
</div>
|
|
553
|
+
|
|
554
|
+
{#if hasChanges}
|
|
555
|
+
<div class="flex-shrink-0 p-4 bg-amber-50 dark:bg-amber-900/30 border-t border-amber-200 dark:border-amber-800">
|
|
556
|
+
<div class="text-xs text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
|
557
|
+
<Icon name="lucide:circle-alert" class="w-3 h-3" />
|
|
558
|
+
Unsaved changes
|
|
559
|
+
</div>
|
|
560
|
+
</div>
|
|
561
|
+
{/if}
|
|
612
562
|
</div>
|
|
613
563
|
{/if}
|
|
614
564
|
{:else if isPdfFile(file.name)}
|
|
@@ -644,29 +594,23 @@
|
|
|
644
594
|
</div>
|
|
645
595
|
{:else}
|
|
646
596
|
<!-- Code content (always in edit mode) -->
|
|
647
|
-
<div class="h-full
|
|
648
|
-
<div class="
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
}}
|
|
665
|
-
/>
|
|
666
|
-
{/key}
|
|
667
|
-
</div>
|
|
668
|
-
|
|
669
|
-
</div>
|
|
597
|
+
<div class="h-full relative bg-slate-50 dark:bg-slate-950">
|
|
598
|
+
<div class="absolute inset-0">
|
|
599
|
+
{#key themeKey}
|
|
600
|
+
<MonacoEditor
|
|
601
|
+
bind:this={monacoEditorRef}
|
|
602
|
+
bind:value={editableContent}
|
|
603
|
+
language={getDetectedLanguage()}
|
|
604
|
+
readonly={false}
|
|
605
|
+
onChange={handleContentChange}
|
|
606
|
+
options={{
|
|
607
|
+
minimap: { enabled: false },
|
|
608
|
+
wordWrap: wordWrap ? 'on' : 'off',
|
|
609
|
+
renderWhitespace: 'none',
|
|
610
|
+
mouseWheelZoom: false
|
|
611
|
+
}}
|
|
612
|
+
/>
|
|
613
|
+
{/key}
|
|
670
614
|
</div>
|
|
671
615
|
</div>
|
|
672
616
|
{/if}
|