@myrialabs/clopen 0.1.7 → 0.1.8

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.
@@ -85,33 +85,8 @@
85
85
  // PDF blob URL
86
86
  let pdfBlobUrl = $state<string | null>(null);
87
87
 
88
- // Container ref for measuring height
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" bind:this={containerRef}>
581
- <div class="flex-1 bg-slate-50 dark:bg-slate-950 overflow-hidden">
582
- <div class="h-full flex flex-col">
583
- <div class="flex-1">
584
- {#key themeKey}
585
- <MonacoEditor
586
- bind:this={monacoEditorRef}
587
- bind:value={editableContent}
588
- language="xml"
589
- readonly={false}
590
- onChange={handleContentChange}
591
- height={editorHeight}
592
- options={{
593
- minimap: { enabled: false },
594
- wordWrap: 'off',
595
- renderWhitespace: 'none',
596
- mouseWheelZoom: false
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 flex flex-col" bind:this={containerRef}>
648
- <div class="flex-1 bg-slate-50 dark:bg-slate-950 overflow-hidden">
649
- <div class="h-full flex flex-col">
650
- <div class="flex-1">
651
- {#key themeKey}
652
- <MonacoEditor
653
- bind:this={monacoEditorRef}
654
- bind:value={editableContent}
655
- language={getDetectedLanguage()}
656
- readonly={false}
657
- onChange={handleContentChange}
658
- height={editorHeight}
659
- options={{
660
- minimap: { enabled: false },
661
- wordWrap: wordWrap ? 'on' : 'off',
662
- renderWhitespace: 'none',
663
- mouseWheelZoom: false
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}
@@ -42,7 +42,7 @@
42
42
  }
43
43
  </script>
44
44
 
45
- <div class="px-2 pb-2">
45
+ <div class="px-2 py-2">
46
46
  <div class="flex flex-col gap-1.5">
47
47
  <textarea
48
48
  bind:this={textareaEl}
@@ -25,14 +25,13 @@
25
25
  lanes: Array<{ col: number; color: string }>;
26
26
  mergeFrom: Array<{ col: number; color: string }>;
27
27
  branchTo: Array<{ col: number; color: string }>;
28
- /** Which lane columns existed in the previous row (for drawing top half of lines) */
28
+ branchToCols: Set<number>;
29
29
  prevLaneCols: Set<number>;
30
- /** Which lane columns will exist in the next row (for drawing bottom half of lines) */
31
30
  nextLaneCols: Set<number>;
32
- /** Whether this node's lane existed in the previous row */
33
31
  nodeHasTop: boolean;
34
- /** Whether this node's lane continues to the next row */
35
32
  nodeHasBottom: boolean;
33
+ /** Max column index used in this row (for per-row graph width) */
34
+ maxCol: number;
36
35
  }
37
36
 
38
37
  const LANE_COLORS = [
@@ -41,10 +40,6 @@
41
40
  ];
42
41
 
43
42
  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
43
 
49
44
  function computeGraph(commits: GitCommit[]): GraphRow[] {
50
45
  if (commits.length === 0) return [];
@@ -53,6 +48,7 @@
53
48
  const laneColorMap = new Map<number, string>();
54
49
  let colorIdx = 0;
55
50
  const rows: GraphRow[] = [];
51
+ const processedSet = new Set<string>();
56
52
 
57
53
  function getColor(col: number): string {
58
54
  if (!laneColorMap.has(col)) {
@@ -70,7 +66,6 @@
70
66
  return s;
71
67
  }
72
68
 
73
- // Track previous row's active lane columns
74
69
  let prevActiveCols = new Set<number>();
75
70
 
76
71
  for (const commit of commits) {
@@ -81,11 +76,13 @@
81
76
 
82
77
  let col: number;
83
78
  const mergeFrom: Array<{ col: number; color: string }> = [];
79
+ const mergeFromCols = new Set<number>();
84
80
 
85
81
  if (myLanes.length > 0) {
86
82
  col = myLanes[0];
87
83
  for (let i = 1; i < myLanes.length; i++) {
88
84
  mergeFrom.push({ col: myLanes[i], color: getColor(myLanes[i]) });
85
+ mergeFromCols.add(myLanes[i]);
89
86
  lanes[myLanes[i]] = null;
90
87
  }
91
88
  } else {
@@ -97,17 +94,26 @@
97
94
  getColor(col);
98
95
  const nodeHasTop = prevActiveCols.has(col);
99
96
 
100
- // Snapshot current active lanes (before parent assignment)
101
97
  const currentPrevCols = new Set(prevActiveCols);
102
98
 
103
99
  const branchTo: Array<{ col: number; color: string }> = [];
100
+ const branchToCols = new Set<number>();
104
101
  if (commit.parents.length > 0) {
105
- lanes[col] = commit.parents[0];
102
+ // First parent: skip if already processed (non-topo edge case)
103
+ if (processedSet.has(commit.parents[0])) {
104
+ lanes[col] = null;
105
+ } else {
106
+ lanes[col] = commit.parents[0];
107
+ }
106
108
 
107
109
  for (let p = 1; p < commit.parents.length; p++) {
110
+ // Skip parents that were already processed
111
+ if (processedSet.has(commit.parents[p])) continue;
112
+
108
113
  const existingIdx = lanes.indexOf(commit.parents[p]);
109
114
  if (existingIdx >= 0 && existingIdx !== col) {
110
115
  branchTo.push({ col: existingIdx, color: getColor(existingIdx) });
116
+ branchToCols.add(existingIdx);
111
117
  } else {
112
118
  let newCol = -1;
113
119
  for (let i = 0; i < lanes.length; i++) {
@@ -119,6 +125,7 @@
119
125
  }
120
126
  lanes[newCol] = commit.parents[p];
121
127
  branchTo.push({ col: newCol, color: getColor(newCol) });
128
+ branchToCols.add(newCol);
122
129
  }
123
130
  }
124
131
  } else {
@@ -134,18 +141,27 @@
134
141
 
135
142
  const nextActiveCols = getActiveCols();
136
143
 
144
+ // Calculate max column used in this row
145
+ let rowMaxCol = col;
146
+ for (const lane of activeLanes) rowMaxCol = Math.max(rowMaxCol, lane.col);
147
+ for (const m of mergeFrom) rowMaxCol = Math.max(rowMaxCol, m.col);
148
+ for (const b of branchTo) rowMaxCol = Math.max(rowMaxCol, b.col);
149
+
137
150
  rows.push({
138
151
  col,
139
152
  nodeColor: getColor(col),
140
153
  lanes: activeLanes,
141
154
  mergeFrom,
142
155
  branchTo,
156
+ branchToCols,
143
157
  prevLaneCols: currentPrevCols,
144
158
  nextLaneCols: nextActiveCols,
145
159
  nodeHasTop,
146
- nodeHasBottom: nextActiveCols.has(col)
160
+ nodeHasBottom: nextActiveCols.has(col),
161
+ maxCol: rowMaxCol
147
162
  });
148
163
 
164
+ processedSet.add(commit.hash);
149
165
  prevActiveCols = nextActiveCols;
150
166
 
151
167
  while (lanes.length > 0 && lanes[lanes.length - 1] === null) {
@@ -160,9 +176,18 @@
160
176
  // Helpers
161
177
  // ========================
162
178
 
163
- const LANE_WIDTH = 16;
164
- const ROW_HEIGHT = 40;
165
- const NODE_R = 4;
179
+ const LANE_WIDTH = 10;
180
+ const ROW_HEIGHT = 48;
181
+ const NODE_R = 3;
182
+ const GRAPH_PAD = 3;
183
+ const LINE_W = '1.5';
184
+ const MAX_VISIBLE_REFS = 3;
185
+ const MAX_REF_LENGTH = 22;
186
+
187
+ function truncateRef(ref: string): string {
188
+ if (ref.length <= MAX_REF_LENGTH) return ref;
189
+ return ref.substring(0, MAX_REF_LENGTH - 1) + '\u2026';
190
+ }
166
191
 
167
192
  function formatDate(dateStr: string): string {
168
193
  const date = new Date(dateStr);
@@ -194,7 +219,7 @@
194
219
  }
195
220
  </script>
196
221
 
197
- <div class="h-full flex flex-col">
222
+ <div class="flex-1 min-h-0 flex flex-col">
198
223
  {#if isLoading && commits.length === 0}
199
224
  <div class="flex-1 flex items-center justify-center">
200
225
  <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 +230,16 @@
205
230
  <span>No commits yet</span>
206
231
  </div>
207
232
  {:else}
208
- <div class="flex-1 overflow-y-auto">
233
+ <div class="flex-1 overflow-y-auto overflow-x-hidden pt-2">
209
234
  {#each commits as commit, idx (commit.hash)}
210
235
  {@const graph = graphRows[idx]}
211
- {@const graphWidth = Math.max(maxCols, 1) * LANE_WIDTH + 8}
236
+ {@const graphWidth = (graph ? graph.maxCol + 1 : 1) * LANE_WIDTH + GRAPH_PAD * 2}
212
237
  <div
213
238
  class="group flex items-stretch w-full text-left cursor-pointer transition-colors
214
239
  {selectedHash === commit.hash
215
240
  ? 'bg-violet-50 dark:bg-violet-900/10'
216
241
  : 'hover:bg-slate-50 dark:hover:bg-slate-800/40'}"
242
+ style="height: {ROW_HEIGHT}px;"
217
243
  role="button"
218
244
  tabindex="0"
219
245
  onclick={() => handleViewCommit(commit.hash)}
@@ -221,124 +247,124 @@
221
247
  >
222
248
  <!-- Git Graph Column -->
223
249
  {#if graph}
224
- <div class="shrink-0 relative" style="width: {graphWidth}px; min-height: {ROW_HEIGHT}px;">
225
- <svg class="absolute inset-0 w-full h-full" style="overflow: visible;">
226
- <!-- Vertical lines for active lanes (pass-through) -->
250
+ <div class="shrink-0 relative" style="width: {graphWidth}px;">
251
+ <svg class="absolute inset-0 w-full h-full">
252
+ <!-- Vertical lines for pass-through lanes (skip node col and branchTo cols) -->
227
253
  {#each graph.lanes as lane}
228
- {@const lx = lane.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
229
- {@const hasTop = graph.prevLaneCols.has(lane.col)}
230
- {@const hasBottom = true}
231
- <line
232
- x1={lx} y1={hasTop ? 0 : ROW_HEIGHT / 2}
233
- x2={lx} y2={hasBottom ? ROW_HEIGHT : ROW_HEIGHT / 2}
234
- stroke={lane.color}
235
- stroke-width="2"
236
- opacity="0.4"
237
- />
254
+ {#if lane.col !== graph.col && !graph.branchToCols.has(lane.col)}
255
+ {@const lx = lane.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
256
+ {@const hasTop = graph.prevLaneCols.has(lane.col)}
257
+ <line
258
+ x1={lx} y1={hasTop ? 0 : ROW_HEIGHT / 2}
259
+ x2={lx} y2={ROW_HEIGHT}
260
+ stroke={lane.color}
261
+ stroke-width={LINE_W}
262
+ />
263
+ {/if}
238
264
  {/each}
239
265
 
240
- <!-- Merge lines (from other lanes into this commit's node) -->
266
+ <!-- Top-half lines for branchTo cols that existed in previous row (skip if also mergeFrom) -->
267
+ {#each graph.branchTo as branch}
268
+ {#if graph.prevLaneCols.has(branch.col) && !graph.mergeFrom.some(m => m.col === branch.col)}
269
+ {@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
270
+ <line
271
+ x1={bx} y1={0}
272
+ x2={bx} y2={ROW_HEIGHT / 2}
273
+ stroke={branch.color}
274
+ stroke-width={LINE_W}
275
+ />
276
+ {/if}
277
+ {/each}
278
+
279
+ <!-- Merge curves (from other lanes into this node) -->
241
280
  {#each graph.mergeFrom as merge}
242
- {@const mx = merge.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
243
- {@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
281
+ {@const mx = merge.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
282
+ {@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
244
283
  <path
245
284
  d="M {mx} 0 C {mx} {ROW_HEIGHT * 0.4}, {nx} {ROW_HEIGHT * 0.3}, {nx} {ROW_HEIGHT / 2}"
246
285
  fill="none"
247
286
  stroke={merge.color}
248
- stroke-width="2"
249
- opacity="0.5"
287
+ stroke-width={LINE_W}
250
288
  />
251
289
  {/each}
252
290
 
253
- <!-- Branch lines (from this node to new lanes) -->
291
+ <!-- Branch curves (from this node to new lanes) -->
254
292
  {#each graph.branchTo as branch}
255
- {@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
256
- {@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
293
+ {@const bx = branch.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
294
+ {@const nx = graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
257
295
  <path
258
296
  d="M {nx} {ROW_HEIGHT / 2} C {nx} {ROW_HEIGHT * 0.7}, {bx} {ROW_HEIGHT * 0.6}, {bx} {ROW_HEIGHT}"
259
297
  fill="none"
260
298
  stroke={branch.color}
261
- stroke-width="2"
262
- opacity="0.5"
299
+ stroke-width={LINE_W}
263
300
  />
264
301
  {/each}
265
302
 
266
303
  <!-- Main vertical line through this node's lane -->
267
304
  <line
268
- x1={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4} y1={graph.nodeHasTop ? 0 : ROW_HEIGHT / 2}
269
- x2={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4} y2={graph.nodeHasBottom ? ROW_HEIGHT : ROW_HEIGHT / 2}
305
+ x1={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD} y1={graph.nodeHasTop ? 0 : ROW_HEIGHT / 2}
306
+ x2={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD} y2={graph.nodeHasBottom ? ROW_HEIGHT : ROW_HEIGHT / 2}
270
307
  stroke={graph.nodeColor}
271
- stroke-width="2"
272
- opacity="0.4"
308
+ stroke-width={LINE_W}
273
309
  />
274
310
 
275
311
  <!-- Node circle -->
276
312
  <circle
277
- cx={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + 4}
313
+ cx={graph.col * LANE_WIDTH + LANE_WIDTH / 2 + GRAPH_PAD}
278
314
  cy={ROW_HEIGHT / 2}
279
- r={commit.parents.length > 1 ? NODE_R + 1 : NODE_R}
315
+ r={NODE_R}
280
316
  fill={graph.nodeColor}
281
317
  stroke="white"
282
- stroke-width="2"
318
+ stroke-width="1.5"
283
319
  />
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
320
  </svg>
296
321
  </div>
297
322
  {/if}
298
323
 
299
- <!-- Commit info -->
300
- <div class="flex-1 min-w-0 flex items-center gap-2 px-2 py-1.5">
301
- <div class="flex-1 min-w-0">
302
- <p class="text-sm text-slate-900 dark:text-slate-100 leading-snug truncate">
324
+ <!-- Commit info (3-line layout) -->
325
+ <div class="flex-1 min-w-0 px-1.5 py-0.5 flex flex-col justify-center overflow-hidden">
326
+ <!-- Line 1: Message + Date -->
327
+ <div class="flex items-center gap-2">
328
+ <p class="flex-1 min-w-0 text-sm text-slate-900 dark:text-slate-100 leading-tight truncate">
303
329
  {commit.message}
304
330
  </p>
305
- <div class="flex items-center gap-2 mt-0.5">
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>
331
+ <span class="text-3xs text-slate-400 shrink-0">{formatDate(commit.date)}</span>
318
332
  </div>
319
333
 
320
- <!-- Actions -->
321
- <div class="items-center gap-0.5 shrink-0 hidden group-hover:flex">
334
+ <!-- Line 2: Hash + Author -->
335
+ <div class="flex items-center gap-1.5 mt-px">
322
336
  <button
323
337
  type="button"
324
- 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"
338
+ 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
339
  onclick={(e) => copyCommitHash(commit.hash, e)}
326
- title="Copy full commit hash"
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"
340
+ title="Copy commit hash"
335
341
  >
336
- <Icon name="lucide:hash" class="w-3.5 h-3.5" />
342
+ {commit.hashShort}
337
343
  </button>
344
+ <span class="text-xs text-slate-500 truncate">{commit.author}</span>
338
345
  </div>
339
346
 
340
- <!-- Date -->
341
- <span class="text-xs text-slate-400 shrink-0">{formatDate(commit.date)}</span>
347
+ <!-- Line 3: Refs -->
348
+ {#if commit.refs && commit.refs.length > 0}
349
+ <div class="flex items-center gap-1 mt-px overflow-hidden">
350
+ {#each commit.refs.slice(0, MAX_VISIBLE_REFS) as ref}
351
+ <span
352
+ class="text-3xs px-1 py-px rounded bg-blue-500/10 text-blue-600 dark:text-blue-400 shrink-0 truncate max-w-28"
353
+ title={ref}
354
+ >
355
+ {truncateRef(ref)}
356
+ </span>
357
+ {/each}
358
+ {#if commit.refs.length > MAX_VISIBLE_REFS}
359
+ <span
360
+ class="text-3xs px-1 py-px rounded bg-slate-500/10 text-slate-500 shrink-0 cursor-default"
361
+ title={commit.refs.slice(MAX_VISIBLE_REFS).join(', ')}
362
+ >
363
+ +{commit.refs.length - MAX_VISIBLE_REFS}
364
+ </span>
365
+ {/if}
366
+ </div>
367
+ {/if}
342
368
  </div>
343
369
  </div>
344
370
  {/each}
@@ -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: {