@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
|
@@ -14,6 +14,25 @@
|
|
|
14
14
|
const { commits, isLoading, hasMore, onLoadMore, onViewCommit }: Props = $props();
|
|
15
15
|
|
|
16
16
|
let selectedHash = $state('');
|
|
17
|
+
let sentinelEl = $state<HTMLDivElement | null>(null);
|
|
18
|
+
|
|
19
|
+
// Infinite scroll: auto load more when sentinel is visible
|
|
20
|
+
$effect(() => {
|
|
21
|
+
const el = sentinelEl;
|
|
22
|
+
if (!el || !hasMore) return;
|
|
23
|
+
|
|
24
|
+
const observer = new IntersectionObserver(
|
|
25
|
+
(entries) => {
|
|
26
|
+
if (entries[0]?.isIntersecting && hasMore && !isLoading) {
|
|
27
|
+
onLoadMore();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
{ rootMargin: '100px' }
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
observer.observe(el);
|
|
34
|
+
return () => observer.disconnect();
|
|
35
|
+
});
|
|
17
36
|
|
|
18
37
|
// ========================
|
|
19
38
|
// Git Graph Computation
|
|
@@ -25,14 +44,13 @@
|
|
|
25
44
|
lanes: Array<{ col: number; color: string }>;
|
|
26
45
|
mergeFrom: Array<{ col: number; color: string }>;
|
|
27
46
|
branchTo: Array<{ col: number; color: string }>;
|
|
28
|
-
|
|
47
|
+
branchToCols: Set<number>;
|
|
29
48
|
prevLaneCols: Set<number>;
|
|
30
|
-
/** Which lane columns will exist in the next row (for drawing bottom half of lines) */
|
|
31
49
|
nextLaneCols: Set<number>;
|
|
32
|
-
/** Whether this node's lane existed in the previous row */
|
|
33
50
|
nodeHasTop: boolean;
|
|
34
|
-
/** Whether this node's lane continues to the next row */
|
|
35
51
|
nodeHasBottom: boolean;
|
|
52
|
+
/** Max column index used in this row (for per-row graph width) */
|
|
53
|
+
maxCol: number;
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
const LANE_COLORS = [
|
|
@@ -41,10 +59,6 @@
|
|
|
41
59
|
];
|
|
42
60
|
|
|
43
61
|
const graphRows = $derived(computeGraph(commits));
|
|
44
|
-
const maxCols = $derived(graphRows.reduce((max, row) => {
|
|
45
|
-
const cols = [row.col, ...row.lanes.map(l => l.col), ...row.mergeFrom.map(m => m.col), ...row.branchTo.map(b => b.col)];
|
|
46
|
-
return Math.max(max, ...cols);
|
|
47
|
-
}, 0) + 1);
|
|
48
62
|
|
|
49
63
|
function computeGraph(commits: GitCommit[]): GraphRow[] {
|
|
50
64
|
if (commits.length === 0) return [];
|
|
@@ -53,6 +67,7 @@
|
|
|
53
67
|
const laneColorMap = new Map<number, string>();
|
|
54
68
|
let colorIdx = 0;
|
|
55
69
|
const rows: GraphRow[] = [];
|
|
70
|
+
const processedSet = new Set<string>();
|
|
56
71
|
|
|
57
72
|
function getColor(col: number): string {
|
|
58
73
|
if (!laneColorMap.has(col)) {
|
|
@@ -70,7 +85,6 @@
|
|
|
70
85
|
return s;
|
|
71
86
|
}
|
|
72
87
|
|
|
73
|
-
// Track previous row's active lane columns
|
|
74
88
|
let prevActiveCols = new Set<number>();
|
|
75
89
|
|
|
76
90
|
for (const commit of commits) {
|
|
@@ -81,11 +95,13 @@
|
|
|
81
95
|
|
|
82
96
|
let col: number;
|
|
83
97
|
const mergeFrom: Array<{ col: number; color: string }> = [];
|
|
98
|
+
const mergeFromCols = new Set<number>();
|
|
84
99
|
|
|
85
100
|
if (myLanes.length > 0) {
|
|
86
101
|
col = myLanes[0];
|
|
87
102
|
for (let i = 1; i < myLanes.length; i++) {
|
|
88
103
|
mergeFrom.push({ col: myLanes[i], color: getColor(myLanes[i]) });
|
|
104
|
+
mergeFromCols.add(myLanes[i]);
|
|
89
105
|
lanes[myLanes[i]] = null;
|
|
90
106
|
}
|
|
91
107
|
} else {
|
|
@@ -97,17 +113,26 @@
|
|
|
97
113
|
getColor(col);
|
|
98
114
|
const nodeHasTop = prevActiveCols.has(col);
|
|
99
115
|
|
|
100
|
-
// Snapshot current active lanes (before parent assignment)
|
|
101
116
|
const currentPrevCols = new Set(prevActiveCols);
|
|
102
117
|
|
|
103
118
|
const branchTo: Array<{ col: number; color: string }> = [];
|
|
119
|
+
const branchToCols = new Set<number>();
|
|
104
120
|
if (commit.parents.length > 0) {
|
|
105
|
-
|
|
121
|
+
// First parent: skip if already processed (non-topo edge case)
|
|
122
|
+
if (processedSet.has(commit.parents[0])) {
|
|
123
|
+
lanes[col] = null;
|
|
124
|
+
} else {
|
|
125
|
+
lanes[col] = commit.parents[0];
|
|
126
|
+
}
|
|
106
127
|
|
|
107
128
|
for (let p = 1; p < commit.parents.length; p++) {
|
|
129
|
+
// Skip parents that were already processed
|
|
130
|
+
if (processedSet.has(commit.parents[p])) continue;
|
|
131
|
+
|
|
108
132
|
const existingIdx = lanes.indexOf(commit.parents[p]);
|
|
109
133
|
if (existingIdx >= 0 && existingIdx !== col) {
|
|
110
134
|
branchTo.push({ col: existingIdx, color: getColor(existingIdx) });
|
|
135
|
+
branchToCols.add(existingIdx);
|
|
111
136
|
} else {
|
|
112
137
|
let newCol = -1;
|
|
113
138
|
for (let i = 0; i < lanes.length; i++) {
|
|
@@ -119,6 +144,7 @@
|
|
|
119
144
|
}
|
|
120
145
|
lanes[newCol] = commit.parents[p];
|
|
121
146
|
branchTo.push({ col: newCol, color: getColor(newCol) });
|
|
147
|
+
branchToCols.add(newCol);
|
|
122
148
|
}
|
|
123
149
|
}
|
|
124
150
|
} else {
|
|
@@ -134,18 +160,27 @@
|
|
|
134
160
|
|
|
135
161
|
const nextActiveCols = getActiveCols();
|
|
136
162
|
|
|
163
|
+
// Calculate max column used in this row
|
|
164
|
+
let rowMaxCol = col;
|
|
165
|
+
for (const lane of activeLanes) rowMaxCol = Math.max(rowMaxCol, lane.col);
|
|
166
|
+
for (const m of mergeFrom) rowMaxCol = Math.max(rowMaxCol, m.col);
|
|
167
|
+
for (const b of branchTo) rowMaxCol = Math.max(rowMaxCol, b.col);
|
|
168
|
+
|
|
137
169
|
rows.push({
|
|
138
170
|
col,
|
|
139
171
|
nodeColor: getColor(col),
|
|
140
172
|
lanes: activeLanes,
|
|
141
173
|
mergeFrom,
|
|
142
174
|
branchTo,
|
|
175
|
+
branchToCols,
|
|
143
176
|
prevLaneCols: currentPrevCols,
|
|
144
177
|
nextLaneCols: nextActiveCols,
|
|
145
178
|
nodeHasTop,
|
|
146
|
-
nodeHasBottom: nextActiveCols.has(col)
|
|
179
|
+
nodeHasBottom: nextActiveCols.has(col),
|
|
180
|
+
maxCol: rowMaxCol
|
|
147
181
|
});
|
|
148
182
|
|
|
183
|
+
processedSet.add(commit.hash);
|
|
149
184
|
prevActiveCols = nextActiveCols;
|
|
150
185
|
|
|
151
186
|
while (lanes.length > 0 && lanes[lanes.length - 1] === null) {
|
|
@@ -160,9 +195,18 @@
|
|
|
160
195
|
// Helpers
|
|
161
196
|
// ========================
|
|
162
197
|
|
|
163
|
-
const LANE_WIDTH =
|
|
164
|
-
const ROW_HEIGHT =
|
|
165
|
-
const NODE_R =
|
|
198
|
+
const LANE_WIDTH = 10;
|
|
199
|
+
const ROW_HEIGHT = 48;
|
|
200
|
+
const NODE_R = 3;
|
|
201
|
+
const GRAPH_PAD = 3;
|
|
202
|
+
const LINE_W = '1.5';
|
|
203
|
+
const MAX_VISIBLE_REFS = 3;
|
|
204
|
+
const MAX_REF_LENGTH = 22;
|
|
205
|
+
|
|
206
|
+
function truncateRef(ref: string): string {
|
|
207
|
+
if (ref.length <= MAX_REF_LENGTH) return ref;
|
|
208
|
+
return ref.substring(0, MAX_REF_LENGTH - 1) + '\u2026';
|
|
209
|
+
}
|
|
166
210
|
|
|
167
211
|
function formatDate(dateStr: string): string {
|
|
168
212
|
const date = new Date(dateStr);
|
|
@@ -194,7 +238,7 @@
|
|
|
194
238
|
}
|
|
195
239
|
</script>
|
|
196
240
|
|
|
197
|
-
<div class="h-
|
|
241
|
+
<div class="flex-1 min-h-0 flex flex-col">
|
|
198
242
|
{#if isLoading && commits.length === 0}
|
|
199
243
|
<div class="flex-1 flex items-center justify-center">
|
|
200
244
|
<div class="w-5 h-5 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
|
|
@@ -205,15 +249,16 @@
|
|
|
205
249
|
<span>No commits yet</span>
|
|
206
250
|
</div>
|
|
207
251
|
{:else}
|
|
208
|
-
<div class="flex-1 overflow-y-auto">
|
|
252
|
+
<div class="flex-1 overflow-y-auto overflow-x-hidden pt-2">
|
|
209
253
|
{#each commits as commit, idx (commit.hash)}
|
|
210
254
|
{@const graph = graphRows[idx]}
|
|
211
|
-
{@const graphWidth =
|
|
255
|
+
{@const graphWidth = (graph ? graph.maxCol + 1 : 1) * LANE_WIDTH + GRAPH_PAD * 2}
|
|
212
256
|
<div
|
|
213
257
|
class="group flex items-stretch w-full text-left cursor-pointer transition-colors
|
|
214
258
|
{selectedHash === commit.hash
|
|
215
259
|
? 'bg-violet-50 dark:bg-violet-900/10'
|
|
216
260
|
: 'hover:bg-slate-50 dark:hover:bg-slate-800/40'}"
|
|
261
|
+
style="height: {ROW_HEIGHT}px;"
|
|
217
262
|
role="button"
|
|
218
263
|
tabindex="0"
|
|
219
264
|
onclick={() => handleViewCommit(commit.hash)}
|
|
@@ -221,139 +266,134 @@
|
|
|
221
266
|
>
|
|
222
267
|
<!-- Git Graph Column -->
|
|
223
268
|
{#if graph}
|
|
224
|
-
<div class="shrink-0 relative" style="width: {graphWidth}px;
|
|
225
|
-
<svg class="absolute inset-0 w-full h-full"
|
|
226
|
-
<!-- Vertical lines for
|
|
269
|
+
<div class="shrink-0 relative" style="width: {graphWidth}px;">
|
|
270
|
+
<svg class="absolute inset-0 w-full h-full">
|
|
271
|
+
<!-- Vertical lines for pass-through lanes (skip node col and branchTo cols) -->
|
|
227
272
|
{#each graph.lanes as lane}
|
|
228
|
-
{
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
273
|
+
{#if lane.col !== graph.col && !graph.branchToCols.has(lane.col)}
|
|
274
|
+
{@const lx = lane.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
275
|
+
{@const hasTop = graph.prevLaneCols.has(lane.col)}
|
|
276
|
+
<line
|
|
277
|
+
x1={lx} y1={hasTop ? 0 : ROW_HEIGHT / 2}
|
|
278
|
+
x2={lx} y2={ROW_HEIGHT}
|
|
279
|
+
stroke={lane.color}
|
|
280
|
+
stroke-width={LINE_W}
|
|
281
|
+
/>
|
|
282
|
+
{/if}
|
|
283
|
+
{/each}
|
|
284
|
+
|
|
285
|
+
<!-- Top-half lines for branchTo cols that existed in previous row (skip if also mergeFrom) -->
|
|
286
|
+
{#each graph.branchTo as branch}
|
|
287
|
+
{#if graph.prevLaneCols.has(branch.col) && !graph.mergeFrom.some(m => m.col === branch.col)}
|
|
288
|
+
{@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
289
|
+
<line
|
|
290
|
+
x1={bx} y1={0}
|
|
291
|
+
x2={bx} y2={ROW_HEIGHT / 2}
|
|
292
|
+
stroke={branch.color}
|
|
293
|
+
stroke-width={LINE_W}
|
|
294
|
+
/>
|
|
295
|
+
{/if}
|
|
238
296
|
{/each}
|
|
239
297
|
|
|
240
|
-
<!-- Merge
|
|
298
|
+
<!-- Merge curves (from other lanes into this node) -->
|
|
241
299
|
{#each graph.mergeFrom as merge}
|
|
242
|
-
{@const mx = merge.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
243
|
-
{@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
300
|
+
{@const mx = merge.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
301
|
+
{@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
244
302
|
<path
|
|
245
303
|
d="M {mx} 0 C {mx} {ROW_HEIGHT * 0.4}, {nx} {ROW_HEIGHT * 0.3}, {nx} {ROW_HEIGHT / 2}"
|
|
246
304
|
fill="none"
|
|
247
305
|
stroke={merge.color}
|
|
248
|
-
stroke-width=
|
|
249
|
-
opacity="0.5"
|
|
306
|
+
stroke-width={LINE_W}
|
|
250
307
|
/>
|
|
251
308
|
{/each}
|
|
252
309
|
|
|
253
|
-
<!-- Branch
|
|
310
|
+
<!-- Branch curves (from this node to new lanes) -->
|
|
254
311
|
{#each graph.branchTo as branch}
|
|
255
|
-
{@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
256
|
-
{@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
312
|
+
{@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
313
|
+
{@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
257
314
|
<path
|
|
258
315
|
d="M {nx} {ROW_HEIGHT / 2} C {nx} {ROW_HEIGHT * 0.7}, {bx} {ROW_HEIGHT * 0.6}, {bx} {ROW_HEIGHT}"
|
|
259
316
|
fill="none"
|
|
260
317
|
stroke={branch.color}
|
|
261
|
-
stroke-width=
|
|
262
|
-
opacity="0.5"
|
|
318
|
+
stroke-width={LINE_W}
|
|
263
319
|
/>
|
|
264
320
|
{/each}
|
|
265
321
|
|
|
266
322
|
<!-- Main vertical line through this node's lane -->
|
|
267
323
|
<line
|
|
268
|
-
x1={graph.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
269
|
-
x2={graph.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
324
|
+
x1={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD} y1={graph.nodeHasTop ? 0 : ROW_HEIGHT / 2}
|
|
325
|
+
x2={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD} y2={graph.nodeHasBottom ? ROW_HEIGHT : ROW_HEIGHT / 2}
|
|
270
326
|
stroke={graph.nodeColor}
|
|
271
|
-
stroke-width=
|
|
272
|
-
opacity="0.4"
|
|
327
|
+
stroke-width={LINE_W}
|
|
273
328
|
/>
|
|
274
329
|
|
|
275
330
|
<!-- Node circle -->
|
|
276
331
|
<circle
|
|
277
|
-
cx={graph.col * LANE_WIDTH + LANE_WIDTH / 2 +
|
|
332
|
+
cx={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
|
|
278
333
|
cy={ROW_HEIGHT / 2}
|
|
279
|
-
r={
|
|
334
|
+
r={NODE_R}
|
|
280
335
|
fill={graph.nodeColor}
|
|
281
336
|
stroke="white"
|
|
282
|
-
stroke-width="
|
|
337
|
+
stroke-width="1.5"
|
|
283
338
|
/>
|
|
284
|
-
{#if commit.parents.length > 1}
|
|
285
|
-
<circle
|
|
286
|
-
cx={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
|
|
287
|
-
cy={ROW_HEIGHT / 2}
|
|
288
|
-
r={NODE_R + 3}
|
|
289
|
-
fill="none"
|
|
290
|
-
stroke={graph.nodeColor}
|
|
291
|
-
stroke-width="1.5"
|
|
292
|
-
opacity="0.5"
|
|
293
|
-
/>
|
|
294
|
-
{/if}
|
|
295
339
|
</svg>
|
|
296
340
|
</div>
|
|
297
341
|
{/if}
|
|
298
342
|
|
|
299
|
-
<!-- Commit info -->
|
|
300
|
-
<div class="flex-1 min-w-0
|
|
301
|
-
|
|
302
|
-
|
|
343
|
+
<!-- Commit info (3-line layout) -->
|
|
344
|
+
<div class="flex-1 min-w-0 px-1.5 py-0.5 flex flex-col justify-center overflow-hidden">
|
|
345
|
+
<!-- Line 1: Message + Date -->
|
|
346
|
+
<div class="flex items-center gap-2">
|
|
347
|
+
<p class="flex-1 min-w-0 text-sm text-slate-900 dark:text-slate-100 leading-tight truncate" title={commit.message}>
|
|
303
348
|
{commit.message}
|
|
304
349
|
</p>
|
|
305
|
-
<
|
|
306
|
-
<span class="text-xs font-mono text-violet-600 dark:text-violet-400">
|
|
307
|
-
{commit.hashShort}
|
|
308
|
-
</span>
|
|
309
|
-
<span class="text-xs text-slate-500 truncate">{commit.author}</span>
|
|
310
|
-
{#if commit.refs && commit.refs.length > 0}
|
|
311
|
-
{#each commit.refs as ref}
|
|
312
|
-
<span class="text-3xs px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 truncate shrink-0">
|
|
313
|
-
{ref}
|
|
314
|
-
</span>
|
|
315
|
-
{/each}
|
|
316
|
-
{/if}
|
|
317
|
-
</div>
|
|
350
|
+
<span class="text-3xs text-slate-400 shrink-0">{formatDate(commit.date)}</span>
|
|
318
351
|
</div>
|
|
319
352
|
|
|
320
|
-
<!--
|
|
321
|
-
<div class="items-center gap-
|
|
353
|
+
<!-- Line 2: Hash + Author -->
|
|
354
|
+
<div class="flex items-center gap-1.5 mt-px">
|
|
322
355
|
<button
|
|
323
356
|
type="button"
|
|
324
|
-
class="
|
|
357
|
+
class="text-xs font-mono text-violet-600 dark:text-violet-400 hover:text-violet-800 dark:hover:text-violet-300 bg-transparent border-none cursor-pointer p-0 shrink-0 transition-colors"
|
|
325
358
|
onclick={(e) => copyCommitHash(commit.hash, e)}
|
|
326
|
-
title="Copy
|
|
327
|
-
>
|
|
328
|
-
<Icon name="lucide:copy" class="w-3.5 h-3.5" />
|
|
329
|
-
</button>
|
|
330
|
-
<button
|
|
331
|
-
type="button"
|
|
332
|
-
class="flex items-center justify-center w-7 h-7 rounded-md text-slate-400 hover:bg-violet-500/10 hover:text-violet-600 transition-colors bg-transparent border-none cursor-pointer"
|
|
333
|
-
onclick={(e) => { e.stopPropagation(); copyCommitHash(commit.hashShort, e); }}
|
|
334
|
-
title="Copy short hash"
|
|
359
|
+
title="Copy commit hash"
|
|
335
360
|
>
|
|
336
|
-
|
|
361
|
+
{commit.hashShort}
|
|
337
362
|
</button>
|
|
363
|
+
<span class="text-xs text-slate-500 truncate">{commit.author}</span>
|
|
338
364
|
</div>
|
|
339
365
|
|
|
340
|
-
<!--
|
|
341
|
-
|
|
366
|
+
<!-- Line 3: Refs -->
|
|
367
|
+
{#if commit.refs && commit.refs.length > 0}
|
|
368
|
+
<div class="flex items-center gap-1 mt-px overflow-hidden">
|
|
369
|
+
{#each commit.refs.slice(0, MAX_VISIBLE_REFS) as ref}
|
|
370
|
+
<span
|
|
371
|
+
class="text-3xs px-1 rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 shrink-0 truncate max-w-28"
|
|
372
|
+
title={ref}
|
|
373
|
+
>
|
|
374
|
+
{truncateRef(ref)}
|
|
375
|
+
</span>
|
|
376
|
+
{/each}
|
|
377
|
+
{#if commit.refs.length > MAX_VISIBLE_REFS}
|
|
378
|
+
<span
|
|
379
|
+
class="text-3xs px-1 py-px rounded bg-slate-500/10 text-slate-500 shrink-0 cursor-default"
|
|
380
|
+
title={commit.refs.slice(MAX_VISIBLE_REFS).join(', ')}
|
|
381
|
+
>
|
|
382
|
+
+{commit.refs.length - MAX_VISIBLE_REFS}
|
|
383
|
+
</span>
|
|
384
|
+
{/if}
|
|
385
|
+
</div>
|
|
386
|
+
{/if}
|
|
342
387
|
</div>
|
|
343
388
|
</div>
|
|
344
389
|
{/each}
|
|
345
390
|
|
|
346
|
-
<!--
|
|
391
|
+
<!-- Infinite scroll sentinel -->
|
|
347
392
|
{#if hasMore}
|
|
348
|
-
<div class="flex justify-center py-3">
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
onclick={onLoadMore}
|
|
353
|
-
disabled={isLoading}
|
|
354
|
-
>
|
|
355
|
-
{isLoading ? 'Loading...' : 'Load More'}
|
|
356
|
-
</button>
|
|
393
|
+
<div bind:this={sentinelEl} class="flex justify-center py-3">
|
|
394
|
+
{#if isLoading}
|
|
395
|
+
<div class="w-4 h-4 border-2 border-slate-200 dark:border-slate-700 border-t-violet-600 rounded-full animate-spin"></div>
|
|
396
|
+
{/if}
|
|
357
397
|
</div>
|
|
358
398
|
{/if}
|
|
359
399
|
</div>
|
|
@@ -214,26 +214,21 @@
|
|
|
214
214
|
</script>
|
|
215
215
|
|
|
216
216
|
<!-- Preview Toolbar -->
|
|
217
|
-
<div class="relative
|
|
218
|
-
<!-- Tabs bar -->
|
|
217
|
+
<div class="relative bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
|
|
218
|
+
<!-- Tabs bar (Git-style underline tabs) — separated with its own border-bottom -->
|
|
219
219
|
{#if tabs.length > 0}
|
|
220
|
-
<div class="flex items-center
|
|
221
|
-
<!-- Tabs -->
|
|
220
|
+
<div class="relative flex items-center overflow-x-auto border-b border-slate-200 dark:border-slate-700">
|
|
222
221
|
{#each tabs as tab}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
222
|
+
{@const isActive = tab.id === activeTabId}
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
class="group relative flex items-center justify-center gap-1 px-2 py-2 text-xs font-medium transition-colors min-w-0 max-w-xs cursor-pointer
|
|
226
|
+
{isActive
|
|
227
|
+
? 'text-violet-600 dark:text-violet-400'
|
|
228
|
+
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-300'}"
|
|
228
229
|
onclick={() => onSwitchTab(tab.id)}
|
|
229
230
|
role="tab"
|
|
230
231
|
tabindex="0"
|
|
231
|
-
onkeydown={(e) => {
|
|
232
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
233
|
-
e.preventDefault();
|
|
234
|
-
onSwitchTab(tab.id);
|
|
235
|
-
}
|
|
236
|
-
}}
|
|
237
232
|
>
|
|
238
233
|
{#if tab.id === mcpControlledTabId}
|
|
239
234
|
<Icon name="lucide:bot" class="w-3 h-3 flex-shrink-0 text-amber-500" />
|
|
@@ -242,81 +237,95 @@
|
|
|
242
237
|
{:else}
|
|
243
238
|
<Icon name="lucide:globe" class="w-3 h-3 flex-shrink-0" />
|
|
244
239
|
{/if}
|
|
245
|
-
<span class="
|
|
240
|
+
<span class="truncate max-w-28" title={tab.url}>
|
|
246
241
|
{tab.title || 'New Tab'}
|
|
247
242
|
</span>
|
|
248
243
|
{#if tab.id === mcpControlledTabId}
|
|
249
|
-
<span title="MCP Controlled" class="flex">
|
|
250
|
-
|
|
244
|
+
<span title="MCP Controlled" class="flex"><Icon name="lucide:lock" class="w-3 h-3 flex-shrink-0 text-amber-500" /></span>
|
|
245
|
+
{/if}
|
|
246
|
+
<!-- Close button -->
|
|
247
|
+
{#if tab.id !== mcpControlledTabId}
|
|
248
|
+
<span
|
|
249
|
+
role="button"
|
|
250
|
+
tabindex="0"
|
|
251
|
+
onclick={(e) => {
|
|
252
|
+
e.stopPropagation();
|
|
253
|
+
onCloseTab(tab.id);
|
|
254
|
+
}}
|
|
255
|
+
onkeydown={(e) => {
|
|
256
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
257
|
+
e.stopPropagation();
|
|
258
|
+
onCloseTab(tab.id);
|
|
259
|
+
}
|
|
260
|
+
}}
|
|
261
|
+
class="flex items-center justify-center w-4 h-4 rounded hover:bg-slate-200 dark:hover:bg-slate-700 transition-all duration-200 flex-shrink-0"
|
|
262
|
+
title="Close tab"
|
|
263
|
+
>
|
|
264
|
+
<Icon name="lucide:x" class="w-2.5 h-2.5" />
|
|
251
265
|
</span>
|
|
252
266
|
{/if}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}}
|
|
258
|
-
class="flex hover:bg-slate-300 dark:hover:bg-slate-600 rounded p-0.5 transition-all duration-200 {tab.id === mcpControlledTabId ? 'hidden' : ''}"
|
|
259
|
-
title="Close tab"
|
|
260
|
-
disabled={tab.id === mcpControlledTabId}
|
|
261
|
-
>
|
|
262
|
-
<Icon name="lucide:x" class="w-3 h-3" />
|
|
263
|
-
</button>
|
|
264
|
-
</div>
|
|
267
|
+
{#if isActive}
|
|
268
|
+
<span class="absolute bottom-0 inset-x-0 h-px bg-violet-600 dark:bg-violet-400"></span>
|
|
269
|
+
{/if}
|
|
270
|
+
</button>
|
|
265
271
|
{/each}
|
|
266
|
-
|
|
272
|
+
|
|
267
273
|
<!-- New tab button -->
|
|
268
274
|
<button
|
|
275
|
+
type="button"
|
|
269
276
|
onclick={() => onNewTab()}
|
|
270
|
-
class="flex items-center justify-center w-
|
|
277
|
+
class="flex items-center justify-center w-6 h-6 rounded-md text-slate-400 hover:text-slate-600 dark:hover:text-slate-300 hover:bg-slate-200/60 dark:hover:bg-slate-700/60 transition-all duration-200 flex-shrink-0 ml-1"
|
|
271
278
|
title="Open new tab"
|
|
272
279
|
>
|
|
273
280
|
<Icon name="lucide:plus" class="w-3 h-3" />
|
|
274
281
|
</button>
|
|
275
282
|
</div>
|
|
276
283
|
{/if}
|
|
277
|
-
|
|
278
|
-
<!-- Main toolbar header -->
|
|
279
|
-
<div class="px-
|
|
280
|
-
<div class="
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
284
|
+
|
|
285
|
+
<!-- Main toolbar header (URL bar) -->
|
|
286
|
+
<div class="px-2.5 py-1.5">
|
|
287
|
+
<div class="px-1 py-0.5 bg-white dark:bg-slate-800 rounded-lg border border-slate-200 dark:border-slate-700">
|
|
288
|
+
<div class="flex items-center justify-between gap-3">
|
|
289
|
+
<!-- Left section: URL navigation -->
|
|
290
|
+
<div class="flex items-center gap-2 flex-1 min-w-0">
|
|
291
|
+
<!-- URL input with integrated controls -->
|
|
292
|
+
<input
|
|
293
|
+
type="text"
|
|
294
|
+
bind:value={urlInput}
|
|
295
|
+
onkeydown={handleUrlKeydown}
|
|
296
|
+
oninput={handleUrlInput}
|
|
297
|
+
onfocus={() => isUserTyping = true}
|
|
298
|
+
onblur={() => isUserTyping = false}
|
|
299
|
+
placeholder="Enter URL to preview..."
|
|
300
|
+
class="flex-1 pl-3 py-2 text-sm bg-transparent border-0 focus:outline-none min-w-0 text-ellipsis"
|
|
301
|
+
/>
|
|
302
|
+
<div class="flex items-center gap-1 px-1.5">
|
|
303
|
+
{#if url}
|
|
304
|
+
<button
|
|
305
|
+
onclick={handleOpenInExternalBrowser}
|
|
306
|
+
class="flex p-1.5 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200"
|
|
307
|
+
title="Open in external browser"
|
|
308
|
+
>
|
|
309
|
+
<Icon name="lucide:external-link" class="w-4 h-4" />
|
|
310
|
+
</button>
|
|
311
|
+
<button
|
|
312
|
+
onclick={handleRefresh}
|
|
313
|
+
disabled={isLoading}
|
|
314
|
+
class="flex p-1.5 rounded-md hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 disabled:opacity-50"
|
|
315
|
+
title="Refresh current page"
|
|
316
|
+
>
|
|
317
|
+
<Icon name={isLoading ? 'lucide:loader-circle' : 'lucide:refresh-cw'} class="w-4 h-4 transition-transform duration-200 {isLoading ? 'animate-spin' : 'hover:rotate-180'}" />
|
|
318
|
+
</button>
|
|
319
|
+
{/if}
|
|
303
320
|
<button
|
|
304
|
-
onclick={
|
|
305
|
-
disabled={isLoading}
|
|
306
|
-
class="
|
|
307
|
-
title="
|
|
321
|
+
onclick={handleGoClick}
|
|
322
|
+
disabled={!urlInput.trim() || isLoading}
|
|
323
|
+
class="ml-0.5 px-3.5 py-1 text-xs font-medium rounded-md bg-violet-500 hover:bg-violet-600 disabled:bg-slate-300 disabled:dark:bg-slate-600 text-white transition-all duration-200 disabled:opacity-50"
|
|
324
|
+
title="Navigate to URL"
|
|
308
325
|
>
|
|
309
|
-
|
|
326
|
+
Go
|
|
310
327
|
</button>
|
|
311
|
-
|
|
312
|
-
<button
|
|
313
|
-
onclick={handleGoClick}
|
|
314
|
-
disabled={!urlInput.trim() || isLoading}
|
|
315
|
-
class="ml-1 px-4 py-1.5 text-sm font-medium rounded-lg bg-violet-500 hover:bg-violet-600 disabled:bg-slate-300 disabled:dark:bg-slate-600 text-white transition-all duration-200 disabled:opacity-50"
|
|
316
|
-
title="Navigate to URL"
|
|
317
|
-
>
|
|
318
|
-
Go
|
|
319
|
-
</button>
|
|
328
|
+
</div>
|
|
320
329
|
</div>
|
|
321
330
|
</div>
|
|
322
331
|
</div>
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
import UserSettings from './user/UserSettings.svelte';
|
|
18
18
|
import NotificationSettings from './notifications/NotificationSettings.svelte';
|
|
19
19
|
import GeneralSettings from './general/GeneralSettings.svelte';
|
|
20
|
-
import pkg from '../../../../package.json';
|
|
21
20
|
|
|
22
21
|
// Responsive state
|
|
23
22
|
let isMobileMenuOpen = $state(false);
|
|
@@ -179,13 +178,7 @@
|
|
|
179
178
|
{/each}
|
|
180
179
|
</nav>
|
|
181
180
|
|
|
182
|
-
|
|
183
|
-
<div class="flex items-center gap-2 text-xs text-slate-600 dark:text-slate-500">
|
|
184
|
-
<Icon name="lucide:info" class="w-4 h-4" />
|
|
185
|
-
<span>Clopen v{pkg.version}</span>
|
|
186
|
-
</div>
|
|
187
|
-
</footer>
|
|
188
|
-
</aside>
|
|
181
|
+
</aside>
|
|
189
182
|
|
|
190
183
|
<!-- Mobile Menu Overlay -->
|
|
191
184
|
{#if isMobile && isMobileMenuOpen}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { isDarkMode } from '$frontend/lib/utils/theme';
|
|
8
8
|
import { ENGINES } from '$shared/constants/engines';
|
|
9
9
|
import { claudeAccountsStore, type ClaudeAccountItem as ClaudeCodeAccountItem } from '$frontend/lib/stores/features/claude-accounts.svelte';
|
|
10
|
-
import type { Terminal } from 'xterm';
|
|
10
|
+
import type { Terminal } from '@xterm/xterm';
|
|
11
11
|
import type { FitAddon } from '@xterm/addon-fit';
|
|
12
12
|
|
|
13
13
|
const claudeCodeEngine = ENGINES.find(e => e.type === 'claude-code')!;
|
|
@@ -99,11 +99,11 @@
|
|
|
99
99
|
if (!browser || !debugTermContainer || debugTerminal) return;
|
|
100
100
|
|
|
101
101
|
const [{ Terminal }, { FitAddon }] = await Promise.all([
|
|
102
|
-
import('xterm'),
|
|
102
|
+
import('@xterm/xterm'),
|
|
103
103
|
import('@xterm/addon-fit')
|
|
104
104
|
]);
|
|
105
105
|
|
|
106
|
-
await import('xterm/css/xterm.css');
|
|
106
|
+
await import('@xterm/xterm/css/xterm.css');
|
|
107
107
|
|
|
108
108
|
debugTerminal = new Terminal({
|
|
109
109
|
theme: {
|
|
@@ -258,7 +258,7 @@
|
|
|
258
258
|
aria-label="Terminal application">
|
|
259
259
|
|
|
260
260
|
<!-- Terminal Header with Tabs -->
|
|
261
|
-
<div class="flex-shrink-0
|
|
261
|
+
<div class="flex-shrink-0 bg-slate-100 dark:bg-slate-900 border-b border-slate-200 dark:border-slate-700">
|
|
262
262
|
<!-- Terminal Tabs -->
|
|
263
263
|
<TerminalTabs
|
|
264
264
|
sessions={terminalStore.sessions}
|