@myrialabs/clopen 0.2.12 → 0.2.14
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 +3 -0
- package/backend/engine/adapters/claude/stream.ts +2 -1
- package/backend/engine/types.ts +9 -0
- package/backend/mcp/config.ts +32 -6
- package/backend/snapshot/snapshot-service.ts +9 -7
- package/backend/terminal/stream-manager.ts +106 -155
- package/backend/ws/projects/crud.ts +3 -3
- package/backend/ws/snapshot/timeline.ts +6 -2
- package/backend/ws/terminal/persistence.ts +19 -33
- package/backend/ws/terminal/session.ts +37 -19
- package/bin/clopen.ts +376 -99
- package/bun.lock +6 -0
- package/frontend/components/chat/input/ChatInput.svelte +8 -0
- package/frontend/components/chat/input/components/LoadingIndicator.svelte +2 -2
- package/frontend/components/chat/input/composables/use-animations.svelte.ts +127 -111
- package/frontend/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -1
- package/frontend/components/chat/widgets/FloatingTodoList.svelte +2 -2
- package/frontend/components/checkpoint/TimelineModal.svelte +3 -1
- package/frontend/components/common/overlay/Dialog.svelte +2 -2
- package/frontend/components/git/ChangesSection.svelte +104 -13
- package/frontend/components/preview/browser/BrowserPreview.svelte +7 -0
- package/frontend/components/preview/browser/components/Canvas.svelte +8 -0
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/components/settings/general/AuthModeSettings.svelte +2 -2
- package/frontend/components/terminal/Terminal.svelte +5 -1
- package/frontend/components/tunnel/TunnelInactive.svelte +4 -4
- package/frontend/services/chat/chat.service.ts +52 -11
- package/frontend/services/terminal/project.service.ts +4 -60
- package/frontend/services/terminal/terminal.service.ts +18 -27
- package/frontend/stores/core/sessions.svelte.ts +6 -0
- package/frontend/stores/ui/settings-modal.svelte.ts +1 -1
- package/frontend/stores/ui/theme.svelte.ts +11 -11
- package/frontend/stores/ui/workspace.svelte.ts +1 -1
- package/index.html +2 -2
- package/package.json +4 -2
- package/shared/utils/anonymous-user.ts +4 -4
|
@@ -178,6 +178,14 @@
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// Resize textarea when placeholder text changes (typewriter animation) while empty
|
|
182
|
+
$effect(() => {
|
|
183
|
+
chatPlaceholder; // track placeholder changes
|
|
184
|
+
if (!messageText || !messageText.trim()) {
|
|
185
|
+
adjustTextareaHeight();
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
181
189
|
// Sync appState.isLoading from presence data (single source of truth for all users)
|
|
182
190
|
// Also fetch partial text and reconnect to stream for late-joining users / refresh
|
|
183
191
|
let lastCatchupProjectId: string | undefined;
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
{#if appState.isLoading}
|
|
15
15
|
<div
|
|
16
|
-
class="absolute z-20 {isWelcomeState ? '-top-16' : '-top-14'} left-0 right-0 flex justify-center pointer-events-none"
|
|
16
|
+
class="absolute z-20 h-9 {isWelcomeState ? '-top-16' : '-top-14'} left-0 right-0 flex justify-center pointer-events-none"
|
|
17
17
|
transition:fly={{ y: 100, duration: 300 }}
|
|
18
18
|
>
|
|
19
19
|
{#if appState.isWaitingInput}
|
|
20
20
|
<!-- Waiting for user input state -->
|
|
21
|
-
<div class="flex items-center gap-2.5 px-4 py-2 bg-amber-50 dark:bg-amber-
|
|
21
|
+
<div class="flex items-center gap-2.5 px-4 py-2 bg-amber-50 dark:bg-amber-950 rounded-full border border-amber-200 dark:border-amber-900 shadow-sm">
|
|
22
22
|
<Icon name="lucide:message-circle-question-mark" class="w-4 h-4 text-amber-600 dark:text-amber-400" />
|
|
23
23
|
<span class="text-sm font-medium text-amber-700 dark:text-amber-300">
|
|
24
24
|
Waiting for your input...
|
|
@@ -3,6 +3,9 @@ import { onDestroy } from 'svelte';
|
|
|
3
3
|
/**
|
|
4
4
|
* Composable for managing placeholder and loading text animations
|
|
5
5
|
* Combines placeholder typewriter effect and loading text rotation
|
|
6
|
+
*
|
|
7
|
+
* Both animations use a `destroyed` flag to prevent interval callbacks
|
|
8
|
+
* from mutating state after the owning component is torn down (HMR / navigation).
|
|
6
9
|
*/
|
|
7
10
|
|
|
8
11
|
// ============================================================================
|
|
@@ -12,83 +15,77 @@ import { onDestroy } from 'svelte';
|
|
|
12
15
|
export function usePlaceholderAnimation(placeholderTexts: string[]) {
|
|
13
16
|
let currentPlaceholderIndex = $state(0);
|
|
14
17
|
let placeholderText = $state('');
|
|
15
|
-
let
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
let destroyed = false;
|
|
19
|
+
|
|
20
|
+
// Track every active timer so stopPlaceholderAnimation can clear them all
|
|
21
|
+
let typewriterInterval: number | null = null;
|
|
22
|
+
let rotationInterval: number | null = null;
|
|
23
|
+
let deleteTimeout: number | null = null;
|
|
24
|
+
let deleteInterval: number | null = null;
|
|
18
25
|
|
|
19
|
-
// Typewriter effect for placeholder
|
|
20
26
|
function typewritePlaceholder(text: string) {
|
|
21
|
-
if (
|
|
22
|
-
|
|
23
|
-
}
|
|
27
|
+
if (typewriterInterval) clearInterval(typewriterInterval);
|
|
28
|
+
typewriterInterval = null;
|
|
24
29
|
|
|
25
|
-
let
|
|
30
|
+
let idx = 0;
|
|
26
31
|
placeholderText = '';
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
33
|
+
typewriterInterval = window.setInterval(() => {
|
|
34
|
+
if (destroyed) { clearInterval(typewriterInterval!); typewriterInterval = null; return; }
|
|
35
|
+
if (idx < text.length) {
|
|
36
|
+
placeholderText = text.substring(0, idx + 1);
|
|
37
|
+
idx++;
|
|
32
38
|
} else {
|
|
33
|
-
clearInterval(
|
|
34
|
-
|
|
39
|
+
clearInterval(typewriterInterval!);
|
|
40
|
+
typewriterInterval = null;
|
|
35
41
|
}
|
|
36
|
-
}, 20);
|
|
42
|
+
}, 20);
|
|
37
43
|
}
|
|
38
44
|
|
|
39
|
-
// Update placeholder with typewriter effect
|
|
40
45
|
function updatePlaceholder() {
|
|
41
46
|
const fullText = placeholderTexts[currentPlaceholderIndex];
|
|
42
47
|
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
|
|
46
|
-
|
|
48
|
+
if (deleteTimeout) clearTimeout(deleteTimeout);
|
|
49
|
+
if (deleteInterval) clearInterval(deleteInterval);
|
|
50
|
+
deleteTimeout = null;
|
|
51
|
+
deleteInterval = null;
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
deleteTimeout = window.setTimeout(() => {
|
|
54
|
+
if (destroyed) return;
|
|
55
|
+
deleteTimeout = null;
|
|
56
|
+
|
|
57
|
+
deleteInterval = window.setInterval(() => {
|
|
58
|
+
if (destroyed) { clearInterval(deleteInterval!); deleteInterval = null; return; }
|
|
52
59
|
if (placeholderText.length > 0) {
|
|
53
60
|
placeholderText = placeholderText.substring(0, placeholderText.length - 1);
|
|
54
61
|
} else {
|
|
55
|
-
clearInterval(deleteInterval);
|
|
56
|
-
|
|
62
|
+
clearInterval(deleteInterval!);
|
|
63
|
+
deleteInterval = null;
|
|
57
64
|
typewritePlaceholder(fullText);
|
|
58
65
|
}
|
|
59
|
-
}, 15);
|
|
60
|
-
}, 2000);
|
|
66
|
+
}, 15);
|
|
67
|
+
}, 2000);
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
function startPlaceholderAnimation() {
|
|
64
|
-
// Clear any existing intervals first
|
|
65
71
|
stopPlaceholderAnimation();
|
|
72
|
+
destroyed = false;
|
|
66
73
|
|
|
67
74
|
currentPlaceholderIndex = Math.floor(Math.random() * placeholderTexts.length);
|
|
68
|
-
|
|
69
|
-
const initialText = placeholderTexts[currentPlaceholderIndex];
|
|
70
|
-
typewritePlaceholder(initialText);
|
|
75
|
+
typewritePlaceholder(placeholderTexts[currentPlaceholderIndex]);
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
rotationInterval = window.setInterval(() => {
|
|
78
|
+
if (destroyed) { clearInterval(rotationInterval!); rotationInterval = null; return; }
|
|
74
79
|
currentPlaceholderIndex = (currentPlaceholderIndex + 1) % placeholderTexts.length;
|
|
75
80
|
updatePlaceholder();
|
|
76
|
-
}, 7000);
|
|
81
|
+
}, 7000);
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
function stopPlaceholderAnimation() {
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
if (placeholderRotationInterval) {
|
|
85
|
-
clearInterval(placeholderRotationInterval);
|
|
86
|
-
placeholderRotationInterval = null;
|
|
87
|
-
}
|
|
88
|
-
if (placeholderDeleteTimeout) {
|
|
89
|
-
clearTimeout(placeholderDeleteTimeout);
|
|
90
|
-
placeholderDeleteTimeout = null;
|
|
91
|
-
}
|
|
85
|
+
if (typewriterInterval) { clearInterval(typewriterInterval); typewriterInterval = null; }
|
|
86
|
+
if (rotationInterval) { clearInterval(rotationInterval); rotationInterval = null; }
|
|
87
|
+
if (deleteTimeout) { clearTimeout(deleteTimeout); deleteTimeout = null; }
|
|
88
|
+
if (deleteInterval) { clearInterval(deleteInterval); deleteInterval = null; }
|
|
92
89
|
}
|
|
93
90
|
|
|
94
91
|
function setStaticPlaceholder(text: string) {
|
|
@@ -96,15 +93,13 @@ export function usePlaceholderAnimation(placeholderTexts: string[]) {
|
|
|
96
93
|
placeholderText = text;
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
// Cleanup on destroy
|
|
100
96
|
onDestroy(() => {
|
|
97
|
+
destroyed = true;
|
|
101
98
|
stopPlaceholderAnimation();
|
|
102
99
|
});
|
|
103
100
|
|
|
104
101
|
return {
|
|
105
|
-
get placeholderText() {
|
|
106
|
-
return placeholderText;
|
|
107
|
-
},
|
|
102
|
+
get placeholderText() { return placeholderText; },
|
|
108
103
|
startAnimation: startPlaceholderAnimation,
|
|
109
104
|
stopAnimation: stopPlaceholderAnimation,
|
|
110
105
|
setStaticPlaceholder
|
|
@@ -116,85 +111,106 @@ export function usePlaceholderAnimation(placeholderTexts: string[]) {
|
|
|
116
111
|
// ============================================================================
|
|
117
112
|
|
|
118
113
|
export function useLoadingTextAnimation(loadingTexts: string[]) {
|
|
119
|
-
let
|
|
120
|
-
let
|
|
121
|
-
let
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
let visibleLoadingText = $state('');
|
|
115
|
+
let currentFullText = '';
|
|
116
|
+
let destroyed = false;
|
|
117
|
+
|
|
118
|
+
let typewriterInterval: number | null = null;
|
|
119
|
+
let rotationInterval: number | null = null;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Typewriter: type characters one-by-one.
|
|
123
|
+
* Calls `onDone` when the full text has been typed.
|
|
124
|
+
*/
|
|
125
|
+
function typeText(text: string, onDone?: () => void) {
|
|
126
|
+
if (typewriterInterval) clearInterval(typewriterInterval);
|
|
127
|
+
typewriterInterval = null;
|
|
128
|
+
|
|
129
|
+
let idx = 0;
|
|
130
|
+
visibleLoadingText = '';
|
|
131
|
+
|
|
132
|
+
typewriterInterval = window.setInterval(() => {
|
|
133
|
+
if (destroyed) { clearInterval(typewriterInterval!); typewriterInterval = null; return; }
|
|
134
|
+
if (idx < text.length) {
|
|
135
|
+
visibleLoadingText = text.substring(0, idx + 1);
|
|
136
|
+
idx++;
|
|
137
|
+
} else {
|
|
138
|
+
clearInterval(typewriterInterval!);
|
|
139
|
+
typewriterInterval = null;
|
|
140
|
+
onDone?.();
|
|
141
|
+
}
|
|
142
|
+
}, 40);
|
|
143
|
+
}
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Backspace: delete characters one-by-one from the current visible text.
|
|
147
|
+
* Calls `onDone` when the text is fully erased.
|
|
148
|
+
*/
|
|
149
|
+
function deleteText(onDone?: () => void) {
|
|
150
|
+
if (typewriterInterval) clearInterval(typewriterInterval);
|
|
151
|
+
typewriterInterval = null;
|
|
152
|
+
|
|
153
|
+
const snapshot = visibleLoadingText;
|
|
154
|
+
let len = snapshot.length;
|
|
155
|
+
|
|
156
|
+
typewriterInterval = window.setInterval(() => {
|
|
157
|
+
if (destroyed) { clearInterval(typewriterInterval!); typewriterInterval = null; return; }
|
|
158
|
+
if (len > 0) {
|
|
159
|
+
len--;
|
|
160
|
+
visibleLoadingText = snapshot.substring(0, len);
|
|
145
161
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
typeIndex++;
|
|
150
|
-
} else {
|
|
151
|
-
// Finished typing
|
|
152
|
-
clearInterval(typewriterIntervalId!);
|
|
153
|
-
typewriterIntervalId = null;
|
|
154
|
-
}
|
|
162
|
+
clearInterval(typewriterInterval!);
|
|
163
|
+
typewriterInterval = null;
|
|
164
|
+
onDone?.();
|
|
155
165
|
}
|
|
156
|
-
},
|
|
166
|
+
}, 40);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function pickNextText(): string {
|
|
170
|
+
let next = currentFullText;
|
|
171
|
+
while (next === currentFullText && loadingTexts.length > 1) {
|
|
172
|
+
next = loadingTexts[Math.floor(Math.random() * loadingTexts.length)];
|
|
173
|
+
}
|
|
174
|
+
return next;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Delete current text, then type new text */
|
|
178
|
+
function transitionTo(newText: string) {
|
|
179
|
+
currentFullText = newText;
|
|
180
|
+
deleteText(() => {
|
|
181
|
+
if (destroyed) return;
|
|
182
|
+
typeText(newText);
|
|
183
|
+
});
|
|
157
184
|
}
|
|
158
185
|
|
|
159
186
|
function startLoadingAnimation() {
|
|
160
|
-
// Clear any existing intervals first to prevent duplication
|
|
161
187
|
stopLoadingAnimation();
|
|
188
|
+
destroyed = false;
|
|
162
189
|
|
|
163
|
-
//
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
currentLoadingText = newText;
|
|
173
|
-
animateTextTransition(newText);
|
|
190
|
+
// Type the initial text character-by-character
|
|
191
|
+
currentFullText = loadingTexts[Math.floor(Math.random() * loadingTexts.length)];
|
|
192
|
+
typeText(currentFullText);
|
|
193
|
+
|
|
194
|
+
// Rotate to a new random text periodically
|
|
195
|
+
rotationInterval = window.setInterval(() => {
|
|
196
|
+
if (destroyed) { clearInterval(rotationInterval!); rotationInterval = null; return; }
|
|
197
|
+
transitionTo(pickNextText());
|
|
174
198
|
}, 15000);
|
|
175
199
|
}
|
|
176
200
|
|
|
177
201
|
function stopLoadingAnimation() {
|
|
178
|
-
|
|
179
|
-
if (
|
|
180
|
-
|
|
181
|
-
loadingTextIntervalId = null;
|
|
182
|
-
}
|
|
183
|
-
if (typewriterIntervalId) {
|
|
184
|
-
window.clearInterval(typewriterIntervalId);
|
|
185
|
-
typewriterIntervalId = null;
|
|
186
|
-
}
|
|
202
|
+
if (typewriterInterval) { clearInterval(typewriterInterval); typewriterInterval = null; }
|
|
203
|
+
if (rotationInterval) { clearInterval(rotationInterval); rotationInterval = null; }
|
|
204
|
+
visibleLoadingText = '';
|
|
187
205
|
}
|
|
188
206
|
|
|
189
|
-
// Cleanup on destroy
|
|
190
207
|
onDestroy(() => {
|
|
208
|
+
destroyed = true;
|
|
191
209
|
stopLoadingAnimation();
|
|
192
210
|
});
|
|
193
211
|
|
|
194
212
|
return {
|
|
195
|
-
get visibleLoadingText() {
|
|
196
|
-
return visibleLoadingText;
|
|
197
|
-
},
|
|
213
|
+
get visibleLoadingText() { return visibleLoadingText; },
|
|
198
214
|
startAnimation: startLoadingAnimation,
|
|
199
215
|
stopAnimation: stopLoadingAnimation
|
|
200
216
|
};
|
|
@@ -12,8 +12,18 @@ export function useTextareaResize() {
|
|
|
12
12
|
// Reset height to auto to get accurate scrollHeight
|
|
13
13
|
textareaElement.style.height = 'auto';
|
|
14
14
|
|
|
15
|
-
// If content is empty
|
|
15
|
+
// If content is empty, measure placeholder height instead
|
|
16
16
|
if (!messageText || !messageText.trim()) {
|
|
17
|
+
const placeholder = textareaElement.placeholder;
|
|
18
|
+
if (placeholder) {
|
|
19
|
+
// Temporarily set value to placeholder to measure wrapped height
|
|
20
|
+
// (native placeholder doesn't affect scrollHeight)
|
|
21
|
+
textareaElement.value = placeholder;
|
|
22
|
+
const scrollHeight = textareaElement.scrollHeight;
|
|
23
|
+
textareaElement.value = '';
|
|
24
|
+
const newHeight = Math.min(scrollHeight, MAX_HEIGHT_PX);
|
|
25
|
+
textareaElement.style.height = newHeight + 'px';
|
|
26
|
+
}
|
|
17
27
|
return;
|
|
18
28
|
}
|
|
19
29
|
|
|
@@ -259,7 +259,7 @@
|
|
|
259
259
|
<div class="flex items-center gap-1">
|
|
260
260
|
<button
|
|
261
261
|
onclick={toggleExpand}
|
|
262
|
-
class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
262
|
+
class="flex p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
263
263
|
title={todoPanelState.isExpanded ? 'Collapse' : 'Expand'}
|
|
264
264
|
>
|
|
265
265
|
<Icon
|
|
@@ -269,7 +269,7 @@
|
|
|
269
269
|
</button>
|
|
270
270
|
<button
|
|
271
271
|
onclick={minimize}
|
|
272
|
-
class="p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
272
|
+
class="flex p-1.5 hover:bg-slate-100 dark:hover:bg-slate-700 rounded-lg transition-colors"
|
|
273
273
|
title="Minimize"
|
|
274
274
|
>
|
|
275
275
|
<Icon name="lucide:minus" class="w-4 h-4 text-slate-600 dark:text-slate-400" />
|
|
@@ -247,9 +247,11 @@
|
|
|
247
247
|
const previousCurrentId = timelineData?.currentHeadId;
|
|
248
248
|
if (timelineData) {
|
|
249
249
|
timelineData.currentHeadId = node.id;
|
|
250
|
+
const isInitialRestore = !!node.checkpoint.isInitial;
|
|
250
251
|
graphNodes = graphNodes.map(n => ({
|
|
251
252
|
...n,
|
|
252
|
-
isCurrent: n.id === node.id
|
|
253
|
+
isCurrent: n.id === node.id,
|
|
254
|
+
isOrphaned: isInitialRestore ? n.id !== node.id : n.isOrphaned
|
|
253
255
|
}));
|
|
254
256
|
}
|
|
255
257
|
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
</div>
|
|
202
202
|
{/if}
|
|
203
203
|
|
|
204
|
-
<div class="flex-1 space-y-
|
|
204
|
+
<div class="flex-1 space-y-1">
|
|
205
205
|
<h3 id="dialog-title" class="text-lg font-semibold {colors.text}">
|
|
206
206
|
{title}
|
|
207
207
|
</h3>
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
</div>
|
|
228
228
|
{/if}
|
|
229
229
|
|
|
230
|
-
{#if !children}
|
|
230
|
+
{#if !children || onConfirm}
|
|
231
231
|
<div class="flex justify-end gap-3 pt-2">
|
|
232
232
|
{#if showCancel}
|
|
233
233
|
<button
|
|
@@ -27,12 +27,78 @@
|
|
|
27
27
|
onStageAll, onUnstageAll, onDiscardAll,
|
|
28
28
|
onViewDiff, onResolve
|
|
29
29
|
}: Props = $props();
|
|
30
|
+
|
|
31
|
+
// Virtual scroll — only render visible items when list is large
|
|
32
|
+
const ITEM_HEIGHT = 32;
|
|
33
|
+
const BUFFER = 10;
|
|
34
|
+
const VIRTUALIZE_THRESHOLD = 200;
|
|
35
|
+
|
|
36
|
+
let scrollEl = $state<HTMLDivElement>();
|
|
37
|
+
let headerEl = $state<HTMLDivElement>();
|
|
38
|
+
let scrollTop = $state(0);
|
|
39
|
+
let containerHeight = $state(384);
|
|
40
|
+
let panelHeight = $state(0);
|
|
41
|
+
|
|
42
|
+
const shouldVirtualize = $derived(files.length > VIRTUALIZE_THRESHOLD);
|
|
43
|
+
const visibleStart = $derived(
|
|
44
|
+
shouldVirtualize ? Math.max(0, Math.floor(scrollTop / ITEM_HEIGHT) - BUFFER) : 0
|
|
45
|
+
);
|
|
46
|
+
const visibleEnd = $derived(
|
|
47
|
+
shouldVirtualize
|
|
48
|
+
? Math.min(files.length, Math.ceil((scrollTop + containerHeight) / ITEM_HEIGHT) + BUFFER)
|
|
49
|
+
: files.length
|
|
50
|
+
);
|
|
51
|
+
const visibleFiles = $derived(files.slice(visibleStart, visibleEnd));
|
|
52
|
+
const topPad = $derived(visibleStart * ITEM_HEIGHT);
|
|
53
|
+
const bottomPad = $derived(Math.max(0, (files.length - visibleEnd) * ITEM_HEIGHT));
|
|
54
|
+
|
|
55
|
+
// Dynamic max-height: fill panel minus section header — no gap
|
|
56
|
+
const headerH = $derived(headerEl?.offsetHeight ?? 48);
|
|
57
|
+
const scrollMaxH = $derived(panelHeight > 0 ? Math.max(128, panelHeight - headerH) : 384);
|
|
58
|
+
|
|
59
|
+
// Track nearest scrollable ancestor size via ResizeObserver
|
|
60
|
+
$effect(() => {
|
|
61
|
+
const el = scrollEl;
|
|
62
|
+
if (!el) return;
|
|
63
|
+
|
|
64
|
+
let parent = el.parentElement;
|
|
65
|
+
while (parent && parent !== document.body) {
|
|
66
|
+
const ov = getComputedStyle(parent).overflowY;
|
|
67
|
+
if (ov === 'auto' || ov === 'scroll') break;
|
|
68
|
+
parent = parent.parentElement;
|
|
69
|
+
}
|
|
70
|
+
if (!parent || parent === document.body) return;
|
|
71
|
+
|
|
72
|
+
const obs = new ResizeObserver(() => {
|
|
73
|
+
panelHeight = parent!.clientHeight;
|
|
74
|
+
containerHeight = el.clientHeight || 384;
|
|
75
|
+
});
|
|
76
|
+
obs.observe(parent);
|
|
77
|
+
panelHeight = parent.clientHeight;
|
|
78
|
+
containerHeight = el.clientHeight || 384;
|
|
79
|
+
|
|
80
|
+
return () => obs.disconnect();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Reset scroll position when section is expanded (container remounts)
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (!isCollapsed) {
|
|
86
|
+
scrollTop = 0;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function onScroll(e: Event) {
|
|
91
|
+
const el = e.currentTarget as HTMLDivElement;
|
|
92
|
+
scrollTop = el.scrollTop;
|
|
93
|
+
containerHeight = el.clientHeight;
|
|
94
|
+
}
|
|
30
95
|
</script>
|
|
31
96
|
|
|
32
97
|
{#if files.length > 0}
|
|
33
98
|
<div class="mb-1">
|
|
34
99
|
<!-- Section header -->
|
|
35
100
|
<div
|
|
101
|
+
bind:this={headerEl}
|
|
36
102
|
onclick={() => isCollapsed = !isCollapsed}
|
|
37
103
|
class="group flex items-center gap-2 py-3 px-2 cursor-pointer select-none hover:bg-slate-100 dark:hover:bg-slate-800/40 rounded-md transition-colors">
|
|
38
104
|
<div
|
|
@@ -87,19 +153,44 @@
|
|
|
87
153
|
|
|
88
154
|
<!-- Files list -->
|
|
89
155
|
{#if !isCollapsed}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
156
|
+
{#if shouldVirtualize}
|
|
157
|
+
<div
|
|
158
|
+
class="ml-2 overflow-y-auto"
|
|
159
|
+
style="max-height: {scrollMaxH}px"
|
|
160
|
+
bind:this={scrollEl}
|
|
161
|
+
onscroll={onScroll}
|
|
162
|
+
>
|
|
163
|
+
<div style="padding-top: {topPad}px; padding-bottom: {bottomPad}px;">
|
|
164
|
+
{#each visibleFiles as file (file.path)}
|
|
165
|
+
<div style="height: {ITEM_HEIGHT}px" class="overflow-hidden">
|
|
166
|
+
<FileChangeItem
|
|
167
|
+
{file}
|
|
168
|
+
{section}
|
|
169
|
+
{onStage}
|
|
170
|
+
{onUnstage}
|
|
171
|
+
{onDiscard}
|
|
172
|
+
{onViewDiff}
|
|
173
|
+
{onResolve}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
{/each}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
{:else}
|
|
180
|
+
<div class="ml-2">
|
|
181
|
+
{#each files as file (file.path)}
|
|
182
|
+
<FileChangeItem
|
|
183
|
+
{file}
|
|
184
|
+
{section}
|
|
185
|
+
{onStage}
|
|
186
|
+
{onUnstage}
|
|
187
|
+
{onDiscard}
|
|
188
|
+
{onViewDiff}
|
|
189
|
+
{onResolve}
|
|
190
|
+
/>
|
|
191
|
+
{/each}
|
|
192
|
+
</div>
|
|
193
|
+
{/if}
|
|
103
194
|
{/if}
|
|
104
195
|
</div>
|
|
105
196
|
{/if}
|
|
@@ -299,6 +299,13 @@
|
|
|
299
299
|
}
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
+
// Sync isNavigating back to active tab (Canvas resets this on first frame after navigation)
|
|
303
|
+
$effect(() => {
|
|
304
|
+
if (activeTabId && activeTab && activeTab.isNavigating !== isNavigating) {
|
|
305
|
+
tabManager.updateTab(activeTabId, { isNavigating });
|
|
306
|
+
}
|
|
307
|
+
});
|
|
308
|
+
|
|
302
309
|
// Watch scale changes and send to backend
|
|
303
310
|
let lastSentScale = 1;
|
|
304
311
|
$effect(() => {
|
|
@@ -573,6 +573,14 @@
|
|
|
573
573
|
isReconnecting = false;
|
|
574
574
|
}, 300);
|
|
575
575
|
}
|
|
576
|
+
|
|
577
|
+
// Reset navigation state when first frame arrives after navigation.
|
|
578
|
+
// The preview:browser-navigation event that normally resets this can be
|
|
579
|
+
// missed during stream reconnect (listeners are removed/re-registered),
|
|
580
|
+
// so use the first rendered frame as definitive signal that navigation completed.
|
|
581
|
+
if (isNavigating) {
|
|
582
|
+
isNavigating = false;
|
|
583
|
+
}
|
|
576
584
|
});
|
|
577
585
|
|
|
578
586
|
// Setup cursor change handler
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
let openCodeCommandCopiedTimer: ReturnType<typeof setTimeout> | null = null;
|
|
74
74
|
|
|
75
75
|
// Debug PTY (xterm.js)
|
|
76
|
-
const showDebug = $state(
|
|
76
|
+
const showDebug = $state(false);
|
|
77
77
|
let debugTermContainer = $state<HTMLDivElement>();
|
|
78
78
|
let debugTerminal: Terminal | null = null;
|
|
79
79
|
let debugFitAddon: FitAddon | null = null;
|
|
@@ -188,7 +188,7 @@
|
|
|
188
188
|
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
189
189
|
</div>
|
|
190
190
|
|
|
191
|
-
<div class="flex-1 space-y-
|
|
191
|
+
<div class="flex-1 space-y-1">
|
|
192
192
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
|
193
193
|
Change Authentication Mode
|
|
194
194
|
</h3>
|
|
@@ -197,7 +197,7 @@
|
|
|
197
197
|
</p>
|
|
198
198
|
|
|
199
199
|
<!-- PAT Display -->
|
|
200
|
-
<div class="p-3.5 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800">
|
|
200
|
+
<div class="mt-3 p-3.5 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800">
|
|
201
201
|
<div class="flex items-center gap-2 text-sm font-semibold text-amber-800 dark:text-amber-200 mb-2">
|
|
202
202
|
<Icon name="lucide:key-round" class="w-4 h-4" />
|
|
203
203
|
<span>Your Personal Access Token</span>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
<script lang="ts">
|
|
6
6
|
import { terminalStore } from '$frontend/stores/features/terminal.svelte';
|
|
7
7
|
import { projectState } from '$frontend/stores/core/projects.svelte';
|
|
8
|
+
import { terminalService } from '$frontend/services/terminal';
|
|
8
9
|
import TerminalTabs from './TerminalTabs.svelte';
|
|
9
10
|
import Icon from '$frontend/components/common/display/Icon.svelte';
|
|
10
11
|
import XTerm from '$frontend/components/common/xterm/XTerm.svelte';
|
|
@@ -160,11 +161,14 @@
|
|
|
160
161
|
if (activeSession) {
|
|
161
162
|
// Clear the terminal store session
|
|
162
163
|
terminalStore.clearSession(activeSession.id);
|
|
163
|
-
|
|
164
|
+
|
|
164
165
|
// Also immediately clear the XTerm display
|
|
165
166
|
if (xterminalRef) {
|
|
166
167
|
xterminalRef.clear();
|
|
167
168
|
}
|
|
169
|
+
|
|
170
|
+
// Sync clear with backend headless terminal
|
|
171
|
+
terminalService.clearHeadlessTerminal(activeSession.id);
|
|
168
172
|
}
|
|
169
173
|
}
|
|
170
174
|
|