gitmaps 1.0.0 → 1.1.1
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/README.md +265 -122
- package/app/[...slug]/page.client.tsx +1 -0
- package/app/[...slug]/page.tsx +6 -0
- package/app/[owner]/[repo]/page.client.tsx +5 -0
- package/app/[slug]/page.client.tsx +5 -0
- package/app/analytics.db +0 -0
- package/app/api/analytics/route.ts +64 -0
- package/app/api/auth/positions/route.ts +95 -33
- package/app/api/build-info/route.ts +19 -0
- package/app/api/chat/route.ts +13 -2
- package/app/api/manifest.json/route.ts +20 -0
- package/app/api/og-image/route.ts +14 -0
- package/app/api/pwa-icon/route.ts +14 -0
- package/app/api/repo/clone-stream/route.ts +20 -12
- package/app/api/repo/file-content/route.ts +73 -20
- package/app/api/repo/imports/route.ts +21 -3
- package/app/api/repo/list/route.ts +30 -0
- package/app/api/repo/load/route.test.ts +62 -0
- package/app/api/repo/load/route.ts +41 -1
- package/app/api/repo/pdf-thumb/route.ts +127 -0
- package/app/api/repo/resolve-slug/route.ts +51 -0
- package/app/api/repo/tree/route.ts +188 -104
- package/app/api/repo/upload/route.ts +6 -9
- package/app/api/sw.js/route.ts +70 -0
- package/app/api/version/route.ts +26 -0
- package/app/galaxy-canvas/page.client.tsx +2 -0
- package/app/galaxy-canvas/page.tsx +5 -0
- package/app/globals.css +5844 -4694
- package/app/icon.png +0 -0
- package/app/layout.tsx +1284 -467
- package/app/lib/auto-arrange.test.ts +158 -0
- package/app/lib/auto-arrange.ts +147 -0
- package/app/lib/canvas-export.ts +358 -358
- package/app/lib/canvas-text.ts +4 -72
- package/app/lib/canvas.ts +625 -564
- package/app/lib/card-arrangement.ts +21 -7
- package/app/lib/card-context-menu.tsx +2 -2
- package/app/lib/card-groups.ts +9 -2
- package/app/lib/cards.tsx +1361 -914
- package/app/lib/chat.tsx +65 -9
- package/app/lib/code-editor.ts +86 -2
- package/app/lib/connections.tsx +34 -43
- package/app/lib/context.test.ts +32 -0
- package/app/lib/context.ts +19 -3
- package/app/lib/cursor-sharing.ts +34 -0
- package/app/lib/events.tsx +76 -73
- package/app/lib/export-canvas.ts +287 -0
- package/app/lib/file-card-plugin.ts +148 -134
- package/app/lib/file-modal.tsx +49 -0
- package/app/lib/file-preview.ts +486 -400
- package/app/lib/github-import.test.ts +424 -0
- package/app/lib/global-search.ts +48 -27
- package/app/lib/initial-route-hydration.test.ts +283 -0
- package/app/lib/initial-route-hydration.ts +202 -0
- package/app/lib/landing-reset.test.ts +99 -0
- package/app/lib/landing-reset.ts +106 -0
- package/app/lib/landing-shell.test.ts +75 -0
- package/app/lib/large-repo-optimization.ts +37 -0
- package/app/lib/layers.tsx +17 -18
- package/app/lib/layout-snapshots.ts +320 -0
- package/app/lib/loading.test.ts +69 -0
- package/app/lib/loading.tsx +160 -45
- package/app/lib/mount-cleanup.test.ts +52 -0
- package/app/lib/mount-cleanup.ts +34 -0
- package/app/lib/mount-init.test.ts +123 -0
- package/app/lib/mount-init.ts +107 -0
- package/app/lib/mount-lifecycle.test.ts +39 -0
- package/app/lib/mount-lifecycle.ts +12 -0
- package/app/lib/mount-route-wiring.test.ts +87 -0
- package/app/lib/mount-route-wiring.ts +84 -0
- package/app/lib/multi-repo.ts +14 -0
- package/app/lib/onboarding-tutorial.ts +278 -0
- package/app/lib/perf-overlay.ts +78 -0
- package/app/lib/positions.ts +191 -122
- package/app/lib/recent-commits.test.ts +869 -0
- package/app/lib/recent-commits.ts +227 -0
- package/app/lib/repo-handoff.test.ts +23 -0
- package/app/lib/repo-handoff.ts +16 -0
- package/app/lib/repo-progressive.ts +119 -0
- package/app/lib/repo-select.test.ts +61 -0
- package/app/lib/repo-select.ts +74 -0
- package/app/lib/repo.tsx +1383 -977
- package/app/lib/role.ts +228 -0
- package/app/lib/route-catchall.test.ts +27 -0
- package/app/lib/route-repo-entry.test.ts +95 -0
- package/app/lib/route-repo-entry.ts +36 -0
- package/app/lib/router-contract.test.ts +22 -0
- package/app/lib/router-contract.ts +19 -0
- package/app/lib/shared-layout.test.ts +86 -0
- package/app/lib/shared-layout.ts +82 -0
- package/app/lib/shortcuts-panel.ts +2 -0
- package/app/lib/status-bar.test.ts +118 -0
- package/app/lib/status-bar.ts +365 -128
- package/app/lib/sync-controls.test.ts +43 -0
- package/app/lib/sync-controls.tsx +303 -0
- package/app/lib/test-dom.ts +145 -0
- package/app/lib/test-fixtures/router-contract/[...slug]/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/api/health/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/api/version/route.ts +3 -0
- package/app/lib/test-fixtures/router-contract/galaxy-canvas/page.tsx +3 -0
- package/app/lib/test-fixtures/router-contract/page.tsx +3 -0
- package/app/lib/transclusion-smoke.test.ts +163 -0
- package/app/lib/tutorial.ts +301 -0
- package/app/lib/version.ts +93 -0
- package/app/lib/viewport-culling.ts +740 -728
- package/app/lib/virtual-files.ts +456 -0
- package/app/lib/webgl-text.ts +189 -0
- package/app/lib/{galaxydraw-bridge.ts → xydraw-bridge.ts} +485 -477
- package/app/lib/{galaxydraw.test.ts → xydraw.test.ts} +228 -229
- package/app/og-image.png +0 -0
- package/app/page.client.tsx +70 -215
- package/app/page.tsx +27 -92
- package/app/state/machine.js +13 -0
- package/banner.png +0 -0
- package/package.json +17 -8
- package/server.ts +11 -1
- package/app/api/connections/route.ts +0 -72
- package/app/api/positions/route.ts +0 -80
- package/app/api/repo/browse/route.ts +0 -55
- package/app/lib/pr-review.ts +0 -374
- package/packages/galaxydraw/README.md +0 -296
- package/packages/galaxydraw/banner.png +0 -0
- package/packages/galaxydraw/demo/build-static.ts +0 -100
- package/packages/galaxydraw/demo/client.ts +0 -154
- package/packages/galaxydraw/demo/dist/client.js +0 -8
- package/packages/galaxydraw/demo/index.html +0 -256
- package/packages/galaxydraw/demo/server.ts +0 -96
- package/packages/galaxydraw/dist/index.js +0 -984
- package/packages/galaxydraw/dist/index.js.map +0 -16
- package/packages/galaxydraw/node_modules/.bin/tsc.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsc.exe +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.bunx +0 -0
- package/packages/galaxydraw/node_modules/.bin/tsserver.exe +0 -0
- package/packages/galaxydraw/package.json +0 -49
- package/packages/galaxydraw/perf.test.ts +0 -284
- package/packages/galaxydraw/src/core/cards.ts +0 -435
- package/packages/galaxydraw/src/core/engine.ts +0 -339
- package/packages/galaxydraw/src/core/events.ts +0 -81
- package/packages/galaxydraw/src/core/layout.ts +0 -136
- package/packages/galaxydraw/src/core/minimap.ts +0 -216
- package/packages/galaxydraw/src/core/state.ts +0 -177
- package/packages/galaxydraw/src/core/viewport.ts +0 -106
- package/packages/galaxydraw/src/galaxydraw.css +0 -166
- package/packages/galaxydraw/src/index.ts +0 -40
- package/packages/galaxydraw/tsconfig.json +0 -30
package/app/lib/repo.tsx
CHANGED
|
@@ -1,977 +1,1383 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* Repository management — load, commit timeline, select commit, all-files.
|
|
4
|
-
*/
|
|
5
|
-
import { measure } from
|
|
6
|
-
import { render } from
|
|
7
|
-
import type { CanvasContext } from
|
|
8
|
-
import { escapeHtml, formatDate, showToast } from
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
ctx
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
//
|
|
698
|
-
function
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
const
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* Repository management — load, commit timeline, select commit, all-files.
|
|
4
|
+
*/
|
|
5
|
+
import { measure } from "measure-fn";
|
|
6
|
+
import { render } from "melina/client";
|
|
7
|
+
import type { CanvasContext } from "./context";
|
|
8
|
+
import { escapeHtml, formatDate, showToast } from "./utils";
|
|
9
|
+
import {
|
|
10
|
+
clearCanvas,
|
|
11
|
+
getAutoColumnCount,
|
|
12
|
+
updateCanvasTransform,
|
|
13
|
+
updateZoomUI,
|
|
14
|
+
updateMinimap,
|
|
15
|
+
forceMinimapRebuild,
|
|
16
|
+
} from "./canvas";
|
|
17
|
+
import { performViewportCulling } from "./viewport-culling";
|
|
18
|
+
import { getPositionKey, loadSavedPositions } from "./positions";
|
|
19
|
+
import { updateHiddenUI } from "./hidden-files";
|
|
20
|
+
import { processVirtualFileSet } from "./virtual-files";
|
|
21
|
+
import {
|
|
22
|
+
showLoadingProgress,
|
|
23
|
+
updateLoadingProgress,
|
|
24
|
+
updateLoadingFileCount,
|
|
25
|
+
updateLoadingMessage,
|
|
26
|
+
hideLoadingProgress,
|
|
27
|
+
} from "./loading";
|
|
28
|
+
import {
|
|
29
|
+
createFileCard,
|
|
30
|
+
createAllFileCard,
|
|
31
|
+
debounceSaveScroll,
|
|
32
|
+
expandCardByPath,
|
|
33
|
+
} from "./cards";
|
|
34
|
+
import { getActiveLayer } from "./layers";
|
|
35
|
+
import { renderConnections, buildConnectionMarkers } from "./connections";
|
|
36
|
+
import {
|
|
37
|
+
renderAllFilesViaCardManager,
|
|
38
|
+
materializeViewport,
|
|
39
|
+
} from "./xydraw-bridge";
|
|
40
|
+
import {
|
|
41
|
+
registerRepo,
|
|
42
|
+
renderRepoTabs,
|
|
43
|
+
getNextRepoOffset,
|
|
44
|
+
isMultiRepoLoad,
|
|
45
|
+
getLoadedRepos,
|
|
46
|
+
} from "./multi-repo";
|
|
47
|
+
import {
|
|
48
|
+
updateStatusBarRepo,
|
|
49
|
+
updateStatusBarCommit,
|
|
50
|
+
updateStatusBarFiles,
|
|
51
|
+
} from "./status-bar";
|
|
52
|
+
|
|
53
|
+
// Shared: reference to ctx for changed-files panel navigation
|
|
54
|
+
let _panelCtx: CanvasContext | null = null;
|
|
55
|
+
export function setPanelCtx(ctx: CanvasContext) {
|
|
56
|
+
_panelCtx = ctx;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Dedup guard: prevent concurrent or duplicate loadRepository calls
|
|
60
|
+
let _loadingRepo: string | null = null;
|
|
61
|
+
let _repoLoadRequestId = 0;
|
|
62
|
+
const LARGE_REPO_AUTO_COMMIT_THRESHOLD = 1000;
|
|
63
|
+
|
|
64
|
+
// ─── Load repository ─────────────────────────────────────
|
|
65
|
+
export async function loadRepository(ctx: CanvasContext, repoPath: string) {
|
|
66
|
+
if (!repoPath) return;
|
|
67
|
+
|
|
68
|
+
// Always init layers when loading a repo — ensures layers bar is visible
|
|
69
|
+
const { initLayers, renderLayersUI } = await import('./layers');
|
|
70
|
+
ctx.snap().context.repoPath = repoPath;
|
|
71
|
+
initLayers(ctx);
|
|
72
|
+
renderLayersUI(ctx);
|
|
73
|
+
|
|
74
|
+
// Prevent duplicate loads of the same repo (e.g. mount triggers both hash + localStorage paths)
|
|
75
|
+
if (_loadingRepo === repoPath) {
|
|
76
|
+
console.log(
|
|
77
|
+
`[repo] Skipping duplicate load for "${repoPath}" — already loading`,
|
|
78
|
+
);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const requestId = ++_repoLoadRequestId;
|
|
83
|
+
const isStale = () => requestId !== _repoLoadRequestId;
|
|
84
|
+
const clearLoadingGuard = () => {
|
|
85
|
+
if (!isStale() && _loadingRepo === repoPath) {
|
|
86
|
+
_loadingRepo = null;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
_loadingRepo = repoPath;
|
|
91
|
+
_panelCtx = ctx;
|
|
92
|
+
ctx.actor.send({ type: "LOAD_REPO", path: repoPath });
|
|
93
|
+
|
|
94
|
+
return measure("repo:load", async () => {
|
|
95
|
+
try {
|
|
96
|
+
document.body.classList.remove("landing-placeholder-visible");
|
|
97
|
+
showLoadingProgress(ctx, "Loading repository...", 0);
|
|
98
|
+
updateLoadingProgress(ctx, repoPath, 10);
|
|
99
|
+
|
|
100
|
+
const response = await fetch("/api/repo/load", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/json" },
|
|
103
|
+
body: JSON.stringify({ path: repoPath }),
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
if (!response.ok) throw new Error(await response.text());
|
|
107
|
+
|
|
108
|
+
updateLoadingProgress(ctx, "Parsing commits...", 30);
|
|
109
|
+
const data = await response.json();
|
|
110
|
+
if (isStale()) {
|
|
111
|
+
console.log(`[repo] Ignoring stale load result for "${repoPath}"`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
ctx.actor.send({ type: "REPO_LOADED", commits: data.commits });
|
|
115
|
+
|
|
116
|
+
// Add to recent repos
|
|
117
|
+
const { addRecentRepo } = require("./recent-commits");
|
|
118
|
+
addRecentRepo(repoPath, data.commits.length);
|
|
119
|
+
|
|
120
|
+
// Set global repo path for image URLs
|
|
121
|
+
(window as any).__GITCANVAS_REPO_PATH__ = repoPath;
|
|
122
|
+
|
|
123
|
+
// Hide landing overlay
|
|
124
|
+
const landing = document.getElementById("landingOverlay");
|
|
125
|
+
if (landing) landing.style.display = "none";
|
|
126
|
+
|
|
127
|
+
// Determine the best URL slug to display:
|
|
128
|
+
// If the current URL is already a GitHub owner/repo slug that maps to this repo, keep it.
|
|
129
|
+
// Otherwise fall back to the short folder name.
|
|
130
|
+
const canonicalSlug = data.canonicalSlug || "";
|
|
131
|
+
const currentPath = decodeURIComponent(
|
|
132
|
+
window.location.pathname.replace(/^\//, ""),
|
|
133
|
+
);
|
|
134
|
+
const isCurrentCanonicalSlug =
|
|
135
|
+
currentPath.includes("/") &&
|
|
136
|
+
!currentPath.includes("\\") &&
|
|
137
|
+
!currentPath.includes(":") &&
|
|
138
|
+
localStorage.getItem(`gitcanvas:slug:${currentPath}`) === repoPath;
|
|
139
|
+
const repoSlug =
|
|
140
|
+
repoPath.replace(/\\/g, "/").split("/").filter(Boolean).pop() ||
|
|
141
|
+
repoPath;
|
|
142
|
+
const displaySlug = isCurrentCanonicalSlug
|
|
143
|
+
? currentPath
|
|
144
|
+
: canonicalSlug || repoSlug;
|
|
145
|
+
const commitHash = data.commits[0]?.hash || "";
|
|
146
|
+
history.replaceState(
|
|
147
|
+
null,
|
|
148
|
+
"",
|
|
149
|
+
"/" +
|
|
150
|
+
(displaySlug.includes("/")
|
|
151
|
+
? displaySlug
|
|
152
|
+
: encodeURIComponent(displaySlug)) +
|
|
153
|
+
(commitHash ? `#${commitHash}` : ""),
|
|
154
|
+
);
|
|
155
|
+
localStorage.setItem("gitcanvas:lastRepo", repoPath);
|
|
156
|
+
// Store slug→path mapping for URL-based loading (both short and GitHub-style)
|
|
157
|
+
localStorage.setItem(`gitcanvas:slug:${repoSlug}`, repoPath);
|
|
158
|
+
if (canonicalSlug) {
|
|
159
|
+
localStorage.setItem(`gitcanvas:slug:${canonicalSlug}`, repoPath);
|
|
160
|
+
}
|
|
161
|
+
if (isCurrentCanonicalSlug) {
|
|
162
|
+
localStorage.setItem(`gitcanvas:slug:${currentPath}`, repoPath);
|
|
163
|
+
}
|
|
164
|
+
updateStatusBarRepo(
|
|
165
|
+
repoPath,
|
|
166
|
+
canonicalSlug || "",
|
|
167
|
+
data.canonicalSlugSource || "",
|
|
168
|
+
);
|
|
169
|
+
if (canonicalSlug) {
|
|
170
|
+
console.info(
|
|
171
|
+
`[gitmaps] canonical slug: ${canonicalSlug} ← ${repoPath}${data.canonicalSlugSource ? ` (${data.canonicalSlugSource})` : ""}`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
// Update dropdown if it exists
|
|
175
|
+
const sel = document.getElementById("repoSelect") as HTMLSelectElement;
|
|
176
|
+
if (sel) sel.value = repoPath;
|
|
177
|
+
|
|
178
|
+
updateLoadingProgress(
|
|
179
|
+
ctx,
|
|
180
|
+
`Found ${data.commits.length} commits, rendering timeline...`,
|
|
181
|
+
50,
|
|
182
|
+
);
|
|
183
|
+
renderCommitTimeline(ctx);
|
|
184
|
+
|
|
185
|
+
// Reload positions for the new repo BEFORE rendering files
|
|
186
|
+
// so cards get placed at their correct saved locations
|
|
187
|
+
ctx.snap().context.repoPath = repoPath;
|
|
188
|
+
await loadSavedPositions(ctx);
|
|
189
|
+
if (isStale()) return;
|
|
190
|
+
|
|
191
|
+
const viewState = ctx.snap().value?.view;
|
|
192
|
+
// Always load all files first — loadAllFiles now shows real file count progress
|
|
193
|
+
await loadAllFiles(ctx);
|
|
194
|
+
if (isStale()) return;
|
|
195
|
+
|
|
196
|
+
// Then select commit (from URL hash or first commit)
|
|
197
|
+
if (data.commits.length > 0) {
|
|
198
|
+
const totalFiles = ctx.allFilesData?.length || 0;
|
|
199
|
+
const hashFromUrl = window.location.hash?.replace("#", "");
|
|
200
|
+
const commitToSelect =
|
|
201
|
+
hashFromUrl && data.commits.find((c) => c.hash === hashFromUrl)
|
|
202
|
+
? hashFromUrl
|
|
203
|
+
: data.commits[0].hash;
|
|
204
|
+
|
|
205
|
+
if (totalFiles > LARGE_REPO_AUTO_COMMIT_THRESHOLD) {
|
|
206
|
+
const selectedCommit =
|
|
207
|
+
data.commits.find((c) => c.hash === commitToSelect) || data.commits[0];
|
|
208
|
+
ctx.actor.send({ type: "SELECT_COMMIT", hash: commitToSelect });
|
|
209
|
+
updateCommitInfo(commitToSelect, selectedCommit?.message || "", true);
|
|
210
|
+
updateStatusBarCommit(commitToSelect);
|
|
211
|
+
const [basePath] = window.location.href.split("#");
|
|
212
|
+
history.replaceState(null, "", `${basePath}#${commitToSelect}`);
|
|
213
|
+
console.info(
|
|
214
|
+
`[repo] Skipping initial commit diff render for large repo (${totalFiles} files): ${repoPath}`,
|
|
215
|
+
);
|
|
216
|
+
} else {
|
|
217
|
+
updateLoadingMessage(
|
|
218
|
+
ctx,
|
|
219
|
+
totalFiles > 0
|
|
220
|
+
? `Loading commit diff — ${totalFiles} files indexed`
|
|
221
|
+
: "Loading commit diff...",
|
|
222
|
+
);
|
|
223
|
+
if (totalFiles > 0) {
|
|
224
|
+
updateLoadingFileCount(
|
|
225
|
+
ctx,
|
|
226
|
+
totalFiles,
|
|
227
|
+
totalFiles,
|
|
228
|
+
`Comparing selected commit against ${totalFiles} indexed files`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
await selectCommit(ctx, commitToSelect);
|
|
232
|
+
if (isStale()) return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Generate virtual transclusion cards only after the final commit/file render settles.
|
|
237
|
+
setTimeout(() => {
|
|
238
|
+
if (!isStale()) processVirtualFiles(ctx);
|
|
239
|
+
}, 120);
|
|
240
|
+
|
|
241
|
+
updateLoadingProgress(ctx, "Done!", 100);
|
|
242
|
+
hideLoadingProgress(ctx);
|
|
243
|
+
clearLoadingGuard(); // Allow future reloads
|
|
244
|
+
|
|
245
|
+
// Re-render timeline after all async work — the initial renderCommitTimeline
|
|
246
|
+
// at line 76 can get clobbered if DOM re-renders during loadAllFiles/selectCommit
|
|
247
|
+
renderCommitTimeline(ctx);
|
|
248
|
+
|
|
249
|
+
showToast(`Loaded ${data.commits.length} commits`, "success");
|
|
250
|
+
|
|
251
|
+
// Register in multi-repo workspace
|
|
252
|
+
registerRepo(ctx, repoPath, data.commits, ctx.allFilesData || []);
|
|
253
|
+
renderRepoTabs(ctx);
|
|
254
|
+
|
|
255
|
+
// Onboarding removed — users learn by exploring the canvas directly
|
|
256
|
+
} catch (err) {
|
|
257
|
+
if (!isStale()) {
|
|
258
|
+
hideLoadingProgress(ctx);
|
|
259
|
+
clearLoadingGuard(); // Allow retry
|
|
260
|
+
ctx.actor.send({ type: "REPO_ERROR", error: err.message });
|
|
261
|
+
measure("repo:loadError", () => err);
|
|
262
|
+
console.error("[repo:loadError] Full error:", err, err?.stack);
|
|
263
|
+
(window as any).__lastLoadError = { message: err?.message, stack: err?.stack, name: err?.name, err: String(err) };
|
|
264
|
+
showToast(`Failed: ${err.message} `, "error");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ─── Load all files (working tree) ───────────────────────
|
|
271
|
+
export async function loadAllFiles(ctx: CanvasContext) {
|
|
272
|
+
const state = ctx.snap().context;
|
|
273
|
+
if (!state.repoPath) return;
|
|
274
|
+
|
|
275
|
+
return measure("allfiles:load", async () => {
|
|
276
|
+
try {
|
|
277
|
+
// Use streaming endpoint for real progress
|
|
278
|
+
const response = await fetch("/api/repo/tree", {
|
|
279
|
+
signal: AbortSignal.timeout(300000),
|
|
280
|
+
method: "POST",
|
|
281
|
+
headers: { "Content-Type": "application/json" },
|
|
282
|
+
body: JSON.stringify({ path: state.repoPath, stream: true }),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!response.ok) throw new Error(await response.text());
|
|
286
|
+
|
|
287
|
+
const allFiles: any[] = [];
|
|
288
|
+
let total = 0;
|
|
289
|
+
|
|
290
|
+
// Parse NDJSON stream
|
|
291
|
+
const reader = response.body!.getReader();
|
|
292
|
+
const decoder = new TextDecoder();
|
|
293
|
+
let buffer = "";
|
|
294
|
+
|
|
295
|
+
while (true) {
|
|
296
|
+
const { done, value } = await reader.read();
|
|
297
|
+
if (done) break;
|
|
298
|
+
|
|
299
|
+
buffer += decoder.decode(value, { stream: true });
|
|
300
|
+
const lines = buffer.split("\n");
|
|
301
|
+
buffer = lines.pop() || ""; // Keep incomplete last line in buffer
|
|
302
|
+
|
|
303
|
+
for (const line of lines) {
|
|
304
|
+
if (!line.trim()) continue;
|
|
305
|
+
try {
|
|
306
|
+
const chunk = JSON.parse(line);
|
|
307
|
+
|
|
308
|
+
if (chunk.total !== undefined && !chunk.files) {
|
|
309
|
+
// First message: total file count
|
|
310
|
+
total = chunk.total;
|
|
311
|
+
updateLoadingMessage(ctx, `Loading files — ${total} total`);
|
|
312
|
+
updateLoadingFileCount(ctx, 0, total, state.repoPath);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (chunk.files) {
|
|
317
|
+
allFiles.push(...chunk.files);
|
|
318
|
+
const loaded = chunk.loaded || allFiles.length;
|
|
319
|
+
const remaining = Math.max(total - loaded, 0);
|
|
320
|
+
updateLoadingFileCount(
|
|
321
|
+
ctx,
|
|
322
|
+
loaded,
|
|
323
|
+
total,
|
|
324
|
+
`${loaded} loaded • ${remaining} remaining`,
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
} catch (e) {
|
|
328
|
+
console.warn("[tree-stream] Failed to parse line:", line, e);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Process remaining buffer
|
|
334
|
+
if (buffer.trim()) {
|
|
335
|
+
try {
|
|
336
|
+
const chunk = JSON.parse(buffer);
|
|
337
|
+
if (chunk.files) allFiles.push(...chunk.files);
|
|
338
|
+
} catch (e) { /* ignore */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
ctx.actor.send({ type: "ALL_FILES_LOADED", files: allFiles });
|
|
342
|
+
ctx.allFilesData = allFiles;
|
|
343
|
+
updateLoadingFileCount(
|
|
344
|
+
ctx,
|
|
345
|
+
total,
|
|
346
|
+
total,
|
|
347
|
+
`Rendering ${total} cards • 0 remaining`,
|
|
348
|
+
);
|
|
349
|
+
renderAllFilesOnCanvas(ctx, allFiles);
|
|
350
|
+
const fileCountEl = document.getElementById("fileCount");
|
|
351
|
+
if (fileCountEl) fileCountEl.textContent = allFiles.length;
|
|
352
|
+
// Auto-fit view after loading files
|
|
353
|
+
setTimeout(() => {
|
|
354
|
+
const { fitAllFiles } = require("./canvas");
|
|
355
|
+
fitAllFiles(ctx);
|
|
356
|
+
}, 100);
|
|
357
|
+
// Virtual transclusion cards are generated after the final repo render
|
|
358
|
+
// (later in loadRepository), otherwise commit selection can clear them.
|
|
359
|
+
} catch (err) {
|
|
360
|
+
measure("allfiles:loadError", () => err);
|
|
361
|
+
showToast(`Failed to load files: ${err.message} `, "error");
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// ─── JSX Components for commit sidebar ──────────────────
|
|
367
|
+
function CommitItem({
|
|
368
|
+
commit,
|
|
369
|
+
lane,
|
|
370
|
+
color,
|
|
371
|
+
onClick,
|
|
372
|
+
}: {
|
|
373
|
+
commit: any;
|
|
374
|
+
lane: number;
|
|
375
|
+
color: string;
|
|
376
|
+
onClick: () => void;
|
|
377
|
+
}) {
|
|
378
|
+
// Derive handle from email (part before @) — more useful than git config name
|
|
379
|
+
const handle = commit.email ? commit.email.split("@")[0] : commit.author;
|
|
380
|
+
|
|
381
|
+
// Calculate indentation based on visual lanes
|
|
382
|
+
const paddingLeft = 16 + lane * 14;
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<div
|
|
386
|
+
className="commit-item"
|
|
387
|
+
data-hash={commit.hash}
|
|
388
|
+
data-lane={lane}
|
|
389
|
+
style={`padding-left: ${paddingLeft}px; --timeline-color: ${color};`}
|
|
390
|
+
onClick={onClick}
|
|
391
|
+
>
|
|
392
|
+
<div className="commit-hash">{commit.hash.substring(0, 7)}</div>
|
|
393
|
+
<div className="commit-message">
|
|
394
|
+
{commit.refs && commit.refs.length > 0 && (
|
|
395
|
+
<span className="commit-refs">
|
|
396
|
+
{commit.refs.map((r) => (
|
|
397
|
+
<span className="commit-ref-badge">{r}</span>
|
|
398
|
+
))}
|
|
399
|
+
</span>
|
|
400
|
+
)}
|
|
401
|
+
{commit.message}
|
|
402
|
+
</div>
|
|
403
|
+
<div className="commit-meta">
|
|
404
|
+
<span className="commit-author">👤 {handle}</span>
|
|
405
|
+
<span>{formatDate(commit.date)}</span>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function CommitInfo({
|
|
412
|
+
hash,
|
|
413
|
+
message,
|
|
414
|
+
allFiles,
|
|
415
|
+
changedCount,
|
|
416
|
+
}: {
|
|
417
|
+
hash?: string;
|
|
418
|
+
message?: string;
|
|
419
|
+
allFiles?: boolean;
|
|
420
|
+
changedCount?: number;
|
|
421
|
+
}) {
|
|
422
|
+
return (
|
|
423
|
+
<>
|
|
424
|
+
{allFiles && <span style="color: var(--accent-tertiary)">All Files</span>}
|
|
425
|
+
{hash ? (
|
|
426
|
+
<span className="commit-hash">{hash.substring(0, 7)}</span>
|
|
427
|
+
) : null}
|
|
428
|
+
{message ? (
|
|
429
|
+
<span style="color: var(--text-secondary)">{message}</span>
|
|
430
|
+
) : null}
|
|
431
|
+
{!hash && allFiles ? (
|
|
432
|
+
<span style="color: var(--text-muted)">Working tree</span>
|
|
433
|
+
) : null}
|
|
434
|
+
{changedCount !== undefined ? (
|
|
435
|
+
<span style="color: var(--text-muted); font-size: 0.7rem">
|
|
436
|
+
• {changedCount} changed
|
|
437
|
+
</span>
|
|
438
|
+
) : null}
|
|
439
|
+
</>
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function updateCommitInfo(
|
|
444
|
+
hash?: string,
|
|
445
|
+
message?: string,
|
|
446
|
+
allFiles?: boolean,
|
|
447
|
+
changedCount?: number,
|
|
448
|
+
) {
|
|
449
|
+
const el = document.getElementById("currentCommitInfo");
|
|
450
|
+
if (el)
|
|
451
|
+
render(
|
|
452
|
+
<CommitInfo
|
|
453
|
+
hash={hash}
|
|
454
|
+
message={message}
|
|
455
|
+
allFiles={allFiles}
|
|
456
|
+
changedCount={changedCount}
|
|
457
|
+
/>,
|
|
458
|
+
el,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ─── Commit timeline render ──────────────────────────────
|
|
463
|
+
export function renderCommitTimeline(ctx: CanvasContext) {
|
|
464
|
+
measure("timeline:render", () => {
|
|
465
|
+
const container = document.getElementById("timelineContainer");
|
|
466
|
+
const countBadge = document.getElementById("commitCount");
|
|
467
|
+
const state = ctx.snap().context;
|
|
468
|
+
const commitsList = state.commits;
|
|
469
|
+
|
|
470
|
+
if (countBadge) countBadge.textContent = commitsList.length;
|
|
471
|
+
|
|
472
|
+
if (!container) return;
|
|
473
|
+
|
|
474
|
+
if (commitsList.length === 0) {
|
|
475
|
+
render(
|
|
476
|
+
<div className="empty-state">
|
|
477
|
+
<span style="opacity:0.4;font-size:32px">🕐</span>
|
|
478
|
+
<p>No commits found</p>
|
|
479
|
+
</div>,
|
|
480
|
+
container,
|
|
481
|
+
);
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Branch graph calculation
|
|
486
|
+
const lanes: (string | null)[] = [];
|
|
487
|
+
const nodes: any[] = [];
|
|
488
|
+
const colors = [
|
|
489
|
+
"#7c3aed",
|
|
490
|
+
"#3b82f6",
|
|
491
|
+
"#10b981",
|
|
492
|
+
"#f59e0b",
|
|
493
|
+
"#ef4444",
|
|
494
|
+
"#ec4899",
|
|
495
|
+
"#06b6d4",
|
|
496
|
+
];
|
|
497
|
+
|
|
498
|
+
commitsList.forEach((commit, i) => {
|
|
499
|
+
// Find the lane reserved for this commit by a previous parent assignment
|
|
500
|
+
let laneIndex = lanes.indexOf(commit.hash);
|
|
501
|
+
if (laneIndex < 0) {
|
|
502
|
+
// No lane reserved — find first empty slot
|
|
503
|
+
laneIndex = lanes.findIndex((h) => !h);
|
|
504
|
+
if (laneIndex < 0) laneIndex = lanes.length;
|
|
505
|
+
}
|
|
506
|
+
// Clear the reservation (we're processing this commit now)
|
|
507
|
+
lanes[laneIndex] = null;
|
|
508
|
+
nodes.push({ hash: commit.hash, lane: laneIndex, index: i });
|
|
509
|
+
|
|
510
|
+
if (commit.parents && commit.parents.length > 0) {
|
|
511
|
+
commit.parents.forEach((pHash, pIndex) => {
|
|
512
|
+
const pLaneIndex = lanes.indexOf(pHash);
|
|
513
|
+
if (pIndex === 0) {
|
|
514
|
+
// First parent: continue in the same lane
|
|
515
|
+
if (pLaneIndex < 0) {
|
|
516
|
+
lanes[laneIndex] = pHash;
|
|
517
|
+
}
|
|
518
|
+
// If parent already has a lane (from another child),
|
|
519
|
+
// just leave laneIndex free — the edge drawing handles the visual connection
|
|
520
|
+
} else {
|
|
521
|
+
// Additional parents (merge): assign to a different lane
|
|
522
|
+
if (pLaneIndex < 0) {
|
|
523
|
+
let empty = lanes.findIndex((h) => !h);
|
|
524
|
+
if (empty < 0) empty = lanes.length;
|
|
525
|
+
lanes[empty] = pHash;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
render(
|
|
533
|
+
<div style="position:relative;">
|
|
534
|
+
<svg
|
|
535
|
+
id="timelineGraph"
|
|
536
|
+
style="position:absolute; top:0; left:0; width:100%; height:100%; pointer-events:none; z-index:0;"
|
|
537
|
+
></svg>
|
|
538
|
+
<div id="timelineItems">
|
|
539
|
+
{commitsList.map((commit, i) => (
|
|
540
|
+
<CommitItem
|
|
541
|
+
key={commit.hash}
|
|
542
|
+
commit={commit}
|
|
543
|
+
lane={nodes[i].lane}
|
|
544
|
+
color={colors[nodes[i].lane % colors.length]}
|
|
545
|
+
onClick={() => selectCommit(ctx, commit.hash)}
|
|
546
|
+
/>
|
|
547
|
+
))}
|
|
548
|
+
</div>
|
|
549
|
+
</div>,
|
|
550
|
+
container,
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
requestAnimationFrame(() => {
|
|
554
|
+
const graph = document.getElementById("timelineGraph");
|
|
555
|
+
if (!graph) return;
|
|
556
|
+
const items = document.querySelectorAll(".commit-item");
|
|
557
|
+
const coords = new Map<string, { x: number; y: number; color: string }>();
|
|
558
|
+
|
|
559
|
+
let maxLane = 0;
|
|
560
|
+
items.forEach((item: HTMLElement) => {
|
|
561
|
+
const hash = item.dataset.hash;
|
|
562
|
+
const lane = parseInt(item.dataset.lane || "0");
|
|
563
|
+
if (lane > maxLane) maxLane = lane;
|
|
564
|
+
// Center of the lane dot, shifted to accommodate the graph drawing
|
|
565
|
+
const x = 16 + lane * 14;
|
|
566
|
+
// offsetTop is relative to the relative parent div we just wrapped it in
|
|
567
|
+
const y = item.offsetTop + item.offsetHeight / 2;
|
|
568
|
+
coords.set(hash, { x, y, color: colors[lane % colors.length] });
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
let svgContent = "";
|
|
572
|
+
|
|
573
|
+
// Draw edges
|
|
574
|
+
commitsList.forEach((commit) => {
|
|
575
|
+
const start = coords.get(commit.hash);
|
|
576
|
+
if (!start) return;
|
|
577
|
+
|
|
578
|
+
(commit.parents || []).forEach((pHash, pIdx) => {
|
|
579
|
+
const end = coords.get(pHash);
|
|
580
|
+
if (!end) return;
|
|
581
|
+
|
|
582
|
+
const isMerge = pIdx > 0;
|
|
583
|
+
const pathColor = isMerge ? end.color : start.color;
|
|
584
|
+
|
|
585
|
+
if (start.x === end.x) {
|
|
586
|
+
svgContent += `<line x1="${start.x}" y1="${start.y}" x2="${end.x}" y2="${end.y}" stroke="${pathColor}" stroke-opacity="0.6" stroke-width="2" />`;
|
|
587
|
+
} else {
|
|
588
|
+
const midY = start.y + (end.y - start.y) / 2;
|
|
589
|
+
svgContent += `<path d="M ${start.x} ${start.y} C ${start.x} ${midY}, ${end.x} ${midY}, ${end.x} ${end.y}" fill="none" stroke="${pathColor}" stroke-opacity="0.6" stroke-width="2" />`;
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
// Draw nodes
|
|
595
|
+
commitsList.forEach((commit) => {
|
|
596
|
+
const p = coords.get(commit.hash);
|
|
597
|
+
if (!p) return;
|
|
598
|
+
let dot = `<circle cx="${p.x}" cy="${p.y}" r="4.5" fill="${p.color}" stroke="var(--bg-secondary)" stroke-width="2" />`;
|
|
599
|
+
if (commit.refs && commit.refs.length > 0) {
|
|
600
|
+
dot += `<circle cx="${p.x}" cy="${p.y}" r="7" fill="none" stroke="${p.color}" stroke-opacity="0.8" stroke-width="1.5" />`;
|
|
601
|
+
}
|
|
602
|
+
svgContent += dot;
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
graph.innerHTML = svgContent;
|
|
606
|
+
});
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// ─── Select commit ───────────────────────────────────────
|
|
611
|
+
export async function selectCommit(ctx: CanvasContext, hash: string) {
|
|
612
|
+
return measure("commit:select", async () => {
|
|
613
|
+
ctx.actor.send({ type: "SELECT_COMMIT", hash });
|
|
614
|
+
|
|
615
|
+
document.querySelectorAll(".commit-item").forEach((el) => {
|
|
616
|
+
el.classList.toggle("active", el.dataset.hash === hash);
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
const state = ctx.snap().context;
|
|
620
|
+
const commit = state.commits.find((c) => c.hash === hash);
|
|
621
|
+
|
|
622
|
+
// Show non-blocking inline progress bar (not overlay)
|
|
623
|
+
const indexedFiles = ctx.allFilesData?.length || 0;
|
|
624
|
+
_showCommitProgress(
|
|
625
|
+
true,
|
|
626
|
+
indexedFiles > 0
|
|
627
|
+
? `${hash.substring(0, 7)} • ${indexedFiles} indexed files`
|
|
628
|
+
: `${hash.substring(0, 7)} — ${commit?.message || ""}`,
|
|
629
|
+
);
|
|
630
|
+
|
|
631
|
+
try {
|
|
632
|
+
const response = await fetch("/api/repo/files", {
|
|
633
|
+
method: "POST",
|
|
634
|
+
headers: { "Content-Type": "application/json" },
|
|
635
|
+
body: JSON.stringify({ path: state.repoPath, commit: hash }),
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
if (!response.ok) throw new Error(await response.text());
|
|
639
|
+
|
|
640
|
+
const data = await response.json();
|
|
641
|
+
ctx.actor.send({ type: "COMMIT_FILES_LOADED", files: data.files });
|
|
642
|
+
ctx.commitFilesData = data.files;
|
|
643
|
+
|
|
644
|
+
// Always re-render all files with highlighted changes
|
|
645
|
+
ctx.changedFilePaths = new Set(data.files.map((f) => f.path));
|
|
646
|
+
if (ctx.allFilesData && ctx.allFilesData.length > 0) {
|
|
647
|
+
renderAllFilesOnCanvas(ctx, ctx.allFilesData);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
updateCommitInfo(hash, commit?.message || "", true, data.files.length);
|
|
651
|
+
|
|
652
|
+
const fileCountEl = document.getElementById("fileCount");
|
|
653
|
+
if (fileCountEl) fileCountEl.textContent = ctx.fileCards.size;
|
|
654
|
+
_showCommitProgress(false);
|
|
655
|
+
updateStatusBarCommit(hash);
|
|
656
|
+
updateStatusBarFiles(ctx.fileCards.size);
|
|
657
|
+
|
|
658
|
+
// Update URL hash for shareable links
|
|
659
|
+
const [basePath] = window.location.href.split("#");
|
|
660
|
+
history.replaceState(null, "", `${basePath}#${hash}`);
|
|
661
|
+
|
|
662
|
+
// Populate changed files panel with diff stats
|
|
663
|
+
populateChangedFilesPanel(ctx, data.files);
|
|
664
|
+
} catch (err) {
|
|
665
|
+
_showCommitProgress(false);
|
|
666
|
+
measure("commit:selectError", () => err);
|
|
667
|
+
showToast(`Failed: ${err.message} `, "error");
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// ─── Inline commit progress bar (non-blocking) ──────────
|
|
673
|
+
function _showCommitProgress(show: boolean, text?: string) {
|
|
674
|
+
let bar = document.getElementById("commitProgressBar");
|
|
675
|
+
if (show) {
|
|
676
|
+
if (!bar) {
|
|
677
|
+
bar = document.createElement("div");
|
|
678
|
+
bar.id = "commitProgressBar";
|
|
679
|
+
bar.className = "commit-progress-bar";
|
|
680
|
+
const canvasArea = document.querySelector(".canvas-area");
|
|
681
|
+
if (canvasArea) {
|
|
682
|
+
canvasArea.insertBefore(
|
|
683
|
+
bar,
|
|
684
|
+
canvasArea.querySelector(".canvas-viewport"),
|
|
685
|
+
);
|
|
686
|
+
} else {
|
|
687
|
+
document.body.appendChild(bar);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
bar.innerHTML = `<div class="commit-progress-track"><div class="commit-progress-fill"></div></div>${text ? `<span class="commit-progress-text">${text}</span>` : ""}`;
|
|
691
|
+
bar.style.display = "flex";
|
|
692
|
+
} else if (bar) {
|
|
693
|
+
bar.style.display = "none";
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ─── Render files on canvas (commits mode) ───────────────
|
|
698
|
+
export function renderFilesOnCanvas(
|
|
699
|
+
ctx: CanvasContext,
|
|
700
|
+
files: any[],
|
|
701
|
+
commitHash: string,
|
|
702
|
+
) {
|
|
703
|
+
measure("canvas:renderFiles", () => {
|
|
704
|
+
clearCanvas(ctx);
|
|
705
|
+
|
|
706
|
+
const visibleFiles = files.filter((f) => !ctx.hiddenFiles.has(f.path));
|
|
707
|
+
let layerFiles = visibleFiles;
|
|
708
|
+
const activeLayer = getActiveLayer();
|
|
709
|
+
if (activeLayer) {
|
|
710
|
+
layerFiles = visibleFiles.filter((f) => !!activeLayer.files[f.path]);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const cols = Math.min(layerFiles.length, getAutoColumnCount(ctx));
|
|
714
|
+
const cardWidth = 580;
|
|
715
|
+
const cardHeight = 700;
|
|
716
|
+
const gap = 40;
|
|
717
|
+
|
|
718
|
+
layerFiles.forEach((f, index) => {
|
|
719
|
+
const posKey = getPositionKey(f.path, commitHash);
|
|
720
|
+
let x: number, y: number;
|
|
721
|
+
|
|
722
|
+
if (ctx.positions.has(posKey)) {
|
|
723
|
+
const pos = ctx.positions.get(posKey);
|
|
724
|
+
x = pos.x;
|
|
725
|
+
y = pos.y;
|
|
726
|
+
} else {
|
|
727
|
+
const col = index % cols;
|
|
728
|
+
const row = Math.floor(index / cols);
|
|
729
|
+
x = 50 + col * (cardWidth + gap);
|
|
730
|
+
y = 50 + row * (cardHeight + gap);
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const file = { ...f };
|
|
734
|
+
if (activeLayer && activeLayer.files[file.path]) {
|
|
735
|
+
file.layerSections = activeLayer.files[file.path].sections;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const card = createFileCard(ctx, file, x, y, commitHash);
|
|
739
|
+
ctx.canvas.appendChild(card);
|
|
740
|
+
ctx.fileCards.set(file.path, card);
|
|
741
|
+
});
|
|
742
|
+
renderConnections(ctx);
|
|
743
|
+
buildConnectionMarkers(ctx);
|
|
744
|
+
forceMinimapRebuild(ctx);
|
|
745
|
+
// Cull off-screen cards after browser layout (needs rAF for valid dimensions)
|
|
746
|
+
requestAnimationFrame(() => performViewportCulling(ctx));
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// ─── Render all files on canvas (working tree) ──────────
|
|
751
|
+
// Virtualized: only creates DOM for cards in/near the viewport.
|
|
752
|
+
// Remaining cards are deferred and materialized on-demand by viewport culling.
|
|
753
|
+
export function renderAllFilesOnCanvas(ctx: CanvasContext, files: any[]) {
|
|
754
|
+
measure("canvas:renderAllFiles", () => {
|
|
755
|
+
// In multi-repo mode, don't clear canvas if adding a second repo
|
|
756
|
+
const isAdditionalRepo = isMultiRepoLoad();
|
|
757
|
+
if (!isAdditionalRepo) {
|
|
758
|
+
clearCanvas(ctx);
|
|
759
|
+
ctx.deferredCards.clear();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// ── Phase 4c: Try CardManager path first ──
|
|
763
|
+
const handled = renderAllFilesViaCardManager(ctx, files);
|
|
764
|
+
if (handled) {
|
|
765
|
+
renderConnections(ctx);
|
|
766
|
+
buildConnectionMarkers(ctx);
|
|
767
|
+
forceMinimapRebuild(ctx);
|
|
768
|
+
// Materialize any deferred cards visible in initial viewport
|
|
769
|
+
requestAnimationFrame(() => {
|
|
770
|
+
materializeViewport(ctx);
|
|
771
|
+
performViewportCulling(ctx);
|
|
772
|
+
});
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
// ── Legacy fallback (CardManager not initialized) ──
|
|
777
|
+
const visibleFiles = files.filter((f) => !ctx.hiddenFiles.has(f.path));
|
|
778
|
+
updateHiddenUI(ctx);
|
|
779
|
+
|
|
780
|
+
// Build a map of changed file data (commit diff info)
|
|
781
|
+
const changedFileDataMap = new Map<string, any>();
|
|
782
|
+
if (ctx.commitFilesData) {
|
|
783
|
+
ctx.commitFilesData.forEach((f) => changedFileDataMap.set(f.path, f));
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
let layerFiles = visibleFiles;
|
|
787
|
+
const activeLayer = getActiveLayer();
|
|
788
|
+
if (activeLayer) {
|
|
789
|
+
layerFiles = visibleFiles.filter((f) => !!activeLayer.files[f.path]);
|
|
790
|
+
} else {
|
|
791
|
+
// Default layer: exclude files that have been moved to other layers
|
|
792
|
+
const { isFileMovedFromDefault } = require("./layers");
|
|
793
|
+
layerFiles = visibleFiles.filter((f) => !isFileMovedFromDefault(f.path));
|
|
794
|
+
}
|
|
795
|
+
// Sort by directory to group files spatially (makes dir-labels coherent)
|
|
796
|
+
layerFiles.sort((a, b) => {
|
|
797
|
+
const dirA = a.path.includes("/")
|
|
798
|
+
? a.path.substring(0, a.path.lastIndexOf("/"))
|
|
799
|
+
: ".";
|
|
800
|
+
const dirB = b.path.includes("/")
|
|
801
|
+
? b.path.substring(0, b.path.lastIndexOf("/"))
|
|
802
|
+
: ".";
|
|
803
|
+
if (dirA !== dirB) return dirA.localeCompare(dirB);
|
|
804
|
+
const nameA = a.path.split("/").pop() || a.path;
|
|
805
|
+
const nameB = b.path.split("/").pop() || b.path;
|
|
806
|
+
return nameA.localeCompare(nameB);
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// Square-ish grid: use ceil(sqrt(n)) columns for a dense rectangle
|
|
810
|
+
const count = layerFiles.length;
|
|
811
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
|
|
812
|
+
const defaultCardWidth = 580;
|
|
813
|
+
const defaultCardHeight = 700;
|
|
814
|
+
const gap = 20;
|
|
815
|
+
const cellW = defaultCardWidth + gap;
|
|
816
|
+
const cellH = defaultCardHeight + gap;
|
|
817
|
+
|
|
818
|
+
// Auto-arrange: group files by directory for spatial clustering
|
|
819
|
+
const { arrangeByDirectory } = require("./auto-arrange");
|
|
820
|
+
const autoPositions = arrangeByDirectory(layerFiles, {
|
|
821
|
+
cardWidth: defaultCardWidth,
|
|
822
|
+
cardHeight: defaultCardHeight,
|
|
823
|
+
fileGap: gap,
|
|
824
|
+
dirGap: 80,
|
|
825
|
+
originX: isAdditionalRepo ? getNextRepoOffset() : 50,
|
|
826
|
+
originY: 50,
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Determine initial viewport rect for virtualization
|
|
830
|
+
const MARGIN = 800; // px beyond viewport to pre-create
|
|
831
|
+
const state = ctx.snap().context;
|
|
832
|
+
const vpEl = ctx.canvasViewport;
|
|
833
|
+
const vpW = vpEl?.clientWidth || window.innerWidth;
|
|
834
|
+
const vpH = vpEl?.clientHeight || window.innerHeight;
|
|
835
|
+
const zoom = state.zoom || 1;
|
|
836
|
+
const offsetX = state.offsetX || 0;
|
|
837
|
+
const offsetY = state.offsetY || 0;
|
|
838
|
+
const worldLeft = (-offsetX - MARGIN) / zoom;
|
|
839
|
+
const worldTop = (-offsetY - MARGIN) / zoom;
|
|
840
|
+
const worldRight = (vpW - offsetX + MARGIN) / zoom;
|
|
841
|
+
const worldBottom = (vpH - offsetY + MARGIN) / zoom;
|
|
842
|
+
|
|
843
|
+
let createdCount = 0;
|
|
844
|
+
let deferredCount = 0;
|
|
845
|
+
|
|
846
|
+
// Cache XState state once outside the loop — avoids N snapshots for N files
|
|
847
|
+
const cachedCardSizes = ctx.snap().context.cardSizes || {};
|
|
848
|
+
|
|
849
|
+
layerFiles.forEach((f, index) => {
|
|
850
|
+
const isChanged = ctx.changedFilePaths.has(f.path);
|
|
851
|
+
const posKey = `allfiles:${f.path}`;
|
|
852
|
+
let x: number, y: number;
|
|
853
|
+
|
|
854
|
+
if (ctx.positions.has(posKey)) {
|
|
855
|
+
const pos = ctx.positions.get(posKey);
|
|
856
|
+
x = pos.x;
|
|
857
|
+
y = pos.y;
|
|
858
|
+
} else if (autoPositions.has(f.path)) {
|
|
859
|
+
const pos = autoPositions.get(f.path);
|
|
860
|
+
x = pos.x;
|
|
861
|
+
y = pos.y;
|
|
862
|
+
} else {
|
|
863
|
+
const col = index % cols;
|
|
864
|
+
const row = Math.floor(index / cols);
|
|
865
|
+
x = 50 + col * cellW;
|
|
866
|
+
y = 50 + row * cellH;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
// Get saved size (from cached snapshot — no per-file ctx.snap() call)
|
|
870
|
+
let size = cachedCardSizes[f.path];
|
|
871
|
+
if (!size && ctx.positions.has(posKey)) {
|
|
872
|
+
const pos = ctx.positions.get(posKey);
|
|
873
|
+
if (pos.width) size = { width: pos.width, height: pos.height };
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Merge diff data into the file for highlighting
|
|
877
|
+
let fileWithDiff = { ...f };
|
|
878
|
+
if (activeLayer && activeLayer.files[fileWithDiff.path]) {
|
|
879
|
+
fileWithDiff.layerSections =
|
|
880
|
+
activeLayer.files[fileWithDiff.path].sections;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
if (isChanged && changedFileDataMap.has(fileWithDiff.path)) {
|
|
884
|
+
const diffData = changedFileDataMap.get(fileWithDiff.path);
|
|
885
|
+
|
|
886
|
+
// Use full content from diff data if available (has the latest version)
|
|
887
|
+
if (diffData.content) {
|
|
888
|
+
fileWithDiff.content = diffData.content;
|
|
889
|
+
fileWithDiff.lines = diffData.content.split("\n").length;
|
|
890
|
+
}
|
|
891
|
+
fileWithDiff.status = diffData.status;
|
|
892
|
+
fileWithDiff.hunks = diffData.hunks;
|
|
893
|
+
|
|
894
|
+
// Compute added/deleted line info from hunks
|
|
895
|
+
if (diffData.hunks?.length > 0) {
|
|
896
|
+
const addedLines = new Set<number>();
|
|
897
|
+
// Map: newLineNumber → array of deleted line texts to show before that line
|
|
898
|
+
const deletedBeforeLine = new Map<number, string[]>();
|
|
899
|
+
for (const hunk of diffData.hunks) {
|
|
900
|
+
let newLine = hunk.newStart;
|
|
901
|
+
let pendingDeleted: string[] = [];
|
|
902
|
+
for (const l of hunk.lines) {
|
|
903
|
+
if (l.type === "add") {
|
|
904
|
+
addedLines.add(newLine);
|
|
905
|
+
// Attach any pending deleted lines before this added line
|
|
906
|
+
if (pendingDeleted.length > 0) {
|
|
907
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
908
|
+
deletedBeforeLine.set(
|
|
909
|
+
newLine,
|
|
910
|
+
existing.concat(pendingDeleted),
|
|
911
|
+
);
|
|
912
|
+
pendingDeleted = [];
|
|
913
|
+
}
|
|
914
|
+
newLine++;
|
|
915
|
+
} else if (l.type === "del") {
|
|
916
|
+
pendingDeleted.push(l.content);
|
|
917
|
+
} else {
|
|
918
|
+
// Context line — flush pending deleted before this
|
|
919
|
+
if (pendingDeleted.length > 0) {
|
|
920
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
921
|
+
deletedBeforeLine.set(
|
|
922
|
+
newLine,
|
|
923
|
+
existing.concat(pendingDeleted),
|
|
924
|
+
);
|
|
925
|
+
pendingDeleted = [];
|
|
926
|
+
}
|
|
927
|
+
newLine++;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
// Flush remaining deleted lines after the hunk
|
|
931
|
+
if (pendingDeleted.length > 0) {
|
|
932
|
+
const existing = deletedBeforeLine.get(newLine) || [];
|
|
933
|
+
deletedBeforeLine.set(newLine, existing.concat(pendingDeleted));
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
fileWithDiff.addedLines = addedLines;
|
|
937
|
+
fileWithDiff.deletedBeforeLine = deletedBeforeLine;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// All files use uniform default size unless user has a custom saved size
|
|
942
|
+
if (!size) {
|
|
943
|
+
size = { width: defaultCardWidth, height: defaultCardHeight };
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ── Virtualization: check if card is near the viewport ──
|
|
947
|
+
const cardW = size?.width || defaultCardWidth;
|
|
948
|
+
const cardH = size?.height || defaultCardHeight;
|
|
949
|
+
const inViewport =
|
|
950
|
+
x + cardW > worldLeft &&
|
|
951
|
+
x < worldRight &&
|
|
952
|
+
y + cardH > worldTop &&
|
|
953
|
+
y < worldBottom;
|
|
954
|
+
|
|
955
|
+
if (inViewport) {
|
|
956
|
+
// Create DOM immediately
|
|
957
|
+
const card = createAllFileCard(ctx, fileWithDiff, x, y, size);
|
|
958
|
+
if (isChanged) {
|
|
959
|
+
card.classList.add("file-card--changed");
|
|
960
|
+
card.dataset.changed = "true";
|
|
961
|
+
}
|
|
962
|
+
ctx.canvas.appendChild(card);
|
|
963
|
+
ctx.fileCards.set(f.path, card);
|
|
964
|
+
|
|
965
|
+
// Restore scroll position
|
|
966
|
+
const scrollKey = `scroll:${f.path}`;
|
|
967
|
+
if (ctx.positions.has(scrollKey)) {
|
|
968
|
+
const savedScroll = ctx.positions.get(scrollKey);
|
|
969
|
+
requestAnimationFrame(() => {
|
|
970
|
+
const body = card.querySelector(".file-card-body");
|
|
971
|
+
if (body && savedScroll.x) body.scrollTop = savedScroll.x;
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
createdCount++;
|
|
975
|
+
} else {
|
|
976
|
+
// Defer: store data for lazy creation when it enters viewport
|
|
977
|
+
ctx.deferredCards.set(f.path, {
|
|
978
|
+
file: fileWithDiff,
|
|
979
|
+
x,
|
|
980
|
+
y,
|
|
981
|
+
size,
|
|
982
|
+
isChanged,
|
|
983
|
+
});
|
|
984
|
+
deferredCount++;
|
|
985
|
+
}
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
console.log(
|
|
989
|
+
`[render] Created ${createdCount} cards, deferred ${deferredCount} (total: ${count})`,
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
renderConnections(ctx);
|
|
993
|
+
buildConnectionMarkers(ctx);
|
|
994
|
+
renderDirectoryLabels(ctx);
|
|
995
|
+
forceMinimapRebuild(ctx);
|
|
996
|
+
// Cull off-screen cards after browser layout (needs rAF for valid dimensions)
|
|
997
|
+
requestAnimationFrame(() => performViewportCulling(ctx));
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ─── Directory labels on canvas ──────────────────────────
|
|
1002
|
+
// Groups visible file cards by parent directory and renders
|
|
1003
|
+
// a world-space label above each directory cluster.
|
|
1004
|
+
function renderDirectoryLabels(ctx: CanvasContext) {
|
|
1005
|
+
// Remove existing labels
|
|
1006
|
+
ctx.canvas?.querySelectorAll(".dir-label").forEach((el) => el.remove());
|
|
1007
|
+
|
|
1008
|
+
// Group cards by parent directory
|
|
1009
|
+
const groups = new Map<
|
|
1010
|
+
string,
|
|
1011
|
+
{ minX: number; minY: number; maxX: number; count: number }
|
|
1012
|
+
>();
|
|
1013
|
+
|
|
1014
|
+
const processCard = (path: string, x: number, y: number, w: number) => {
|
|
1015
|
+
const dir = path.includes("/")
|
|
1016
|
+
? path.substring(0, path.lastIndexOf("/"))
|
|
1017
|
+
: ".";
|
|
1018
|
+
const g = groups.get(dir);
|
|
1019
|
+
if (g) {
|
|
1020
|
+
g.minX = Math.min(g.minX, x);
|
|
1021
|
+
g.minY = Math.min(g.minY, y);
|
|
1022
|
+
g.maxX = Math.max(g.maxX, x + w);
|
|
1023
|
+
g.count++;
|
|
1024
|
+
} else {
|
|
1025
|
+
groups.set(dir, { minX: x, minY: y, maxX: x + w, count: 1 });
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// Created cards (in DOM)
|
|
1030
|
+
ctx.fileCards.forEach((card, path) => {
|
|
1031
|
+
const x = parseFloat(card.style.left) || 0;
|
|
1032
|
+
const y = parseFloat(card.style.top) || 0;
|
|
1033
|
+
const w = card.offsetWidth || 580;
|
|
1034
|
+
processCard(path, x, y, w);
|
|
1035
|
+
});
|
|
1036
|
+
|
|
1037
|
+
// Deferred cards (not yet in DOM)
|
|
1038
|
+
ctx.deferredCards.forEach((info, path) => {
|
|
1039
|
+
const w = info.size?.width || 580;
|
|
1040
|
+
processCard(path, info.x, info.y, w);
|
|
1041
|
+
});
|
|
1042
|
+
|
|
1043
|
+
// Only show labels if we have multiple directories
|
|
1044
|
+
if (groups.size <= 1) return;
|
|
1045
|
+
|
|
1046
|
+
const frag = document.createDocumentFragment();
|
|
1047
|
+
for (const [dir, g] of groups) {
|
|
1048
|
+
const label = document.createElement("div");
|
|
1049
|
+
label.className = "dir-label";
|
|
1050
|
+
label.dataset.dir = dir;
|
|
1051
|
+
const centerX = (g.minX + g.maxX) / 2;
|
|
1052
|
+
label.style.left = `${centerX}px`;
|
|
1053
|
+
label.style.top = `${g.minY - 36}px`;
|
|
1054
|
+
label.style.transform = "translateX(-50%)";
|
|
1055
|
+
label.innerHTML = `<span class="dir-label-icon">📁</span> ${dir}<span class="dir-label-count">${g.count}</span>`;
|
|
1056
|
+
|
|
1057
|
+
// Click to collapse directory into a group card
|
|
1058
|
+
label.addEventListener("click", (e) => {
|
|
1059
|
+
e.stopPropagation();
|
|
1060
|
+
import("./card-groups").then(({ toggleDirectoryCollapse }) => {
|
|
1061
|
+
toggleDirectoryCollapse(ctx, dir);
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
|
|
1065
|
+
frag.appendChild(label);
|
|
1066
|
+
}
|
|
1067
|
+
ctx.canvas?.appendChild(frag);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
// ─── Highlight changed files without re-rendering ────────
|
|
1071
|
+
export function highlightChangedFiles(ctx: CanvasContext) {
|
|
1072
|
+
measure("allfiles:highlight", () => {
|
|
1073
|
+
const hasChanges = ctx.changedFilePaths.size > 0;
|
|
1074
|
+
ctx.fileCards.forEach((card, path) => {
|
|
1075
|
+
const isChanged = hasChanges && ctx.changedFilePaths.has(path);
|
|
1076
|
+
card.classList.toggle("file-card--changed", isChanged);
|
|
1077
|
+
card.classList.toggle("file-card--unchanged", hasChanges && !isChanged);
|
|
1078
|
+
card.dataset.changed = isChanged ? "true" : "";
|
|
1079
|
+
});
|
|
1080
|
+
|
|
1081
|
+
// Rebuild minimap to reflect new highlighting
|
|
1082
|
+
forceMinimapRebuild(ctx);
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// ─── Switch view mode ────────────────────────────────────
|
|
1087
|
+
export function switchView(ctx: CanvasContext, mode: string) {
|
|
1088
|
+
if (mode === "allfiles") {
|
|
1089
|
+
ctx.actor.send({ type: "SWITCH_TO_ALLFILES" });
|
|
1090
|
+
ctx.allFilesActive = true;
|
|
1091
|
+
} else {
|
|
1092
|
+
ctx.actor.send({ type: "SWITCH_TO_COMMITS" });
|
|
1093
|
+
ctx.allFilesActive = false;
|
|
1094
|
+
ctx.changedFilePaths.clear();
|
|
1095
|
+
ctx.commitFilesData = null;
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
document
|
|
1099
|
+
.getElementById("modeCommits")
|
|
1100
|
+
?.classList.toggle("active", mode === "commits");
|
|
1101
|
+
document
|
|
1102
|
+
.getElementById("modeAllFiles")
|
|
1103
|
+
?.classList.toggle("active", mode === "allfiles");
|
|
1104
|
+
|
|
1105
|
+
if (mode === "allfiles") {
|
|
1106
|
+
const state = ctx.snap().context;
|
|
1107
|
+
const commitInfo = document.getElementById("currentCommitInfo");
|
|
1108
|
+
|
|
1109
|
+
if (state.currentCommitHash) {
|
|
1110
|
+
const commit = state.commits.find(
|
|
1111
|
+
(c) => c.hash === state.currentCommitHash,
|
|
1112
|
+
);
|
|
1113
|
+
if (commitInfo) {
|
|
1114
|
+
updateCommitInfo(state.currentCommitHash, commit?.message || "", true);
|
|
1115
|
+
}
|
|
1116
|
+
} else {
|
|
1117
|
+
if (commitInfo) {
|
|
1118
|
+
updateCommitInfo(undefined, undefined, true);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (state.repoPath) {
|
|
1123
|
+
// If we have a selected commit, fetch its changed files first
|
|
1124
|
+
// so we can properly highlight/render them as diff cards
|
|
1125
|
+
const doRender = async () => {
|
|
1126
|
+
// Fetch commit files if we have a commit but don't have diff data yet
|
|
1127
|
+
if (
|
|
1128
|
+
state.currentCommitHash &&
|
|
1129
|
+
(!ctx.commitFilesData || ctx.commitFilesData.length === 0)
|
|
1130
|
+
) {
|
|
1131
|
+
try {
|
|
1132
|
+
const response = await fetch("/api/repo/files", {
|
|
1133
|
+
method: "POST",
|
|
1134
|
+
headers: { "Content-Type": "application/json" },
|
|
1135
|
+
body: JSON.stringify({
|
|
1136
|
+
path: state.repoPath,
|
|
1137
|
+
commit: state.currentCommitHash,
|
|
1138
|
+
}),
|
|
1139
|
+
});
|
|
1140
|
+
if (response.ok) {
|
|
1141
|
+
const data = await response.json();
|
|
1142
|
+
ctx.commitFilesData = data.files;
|
|
1143
|
+
ctx.changedFilePaths = new Set(data.files.map((f) => f.path));
|
|
1144
|
+
ctx.actor.send({
|
|
1145
|
+
type: "COMMIT_FILES_LOADED",
|
|
1146
|
+
files: data.files,
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
// Continue without diff data
|
|
1151
|
+
}
|
|
1152
|
+
} else if (state.commitFiles.length > 0) {
|
|
1153
|
+
ctx.commitFilesData = state.commitFiles;
|
|
1154
|
+
ctx.changedFilePaths = new Set(state.commitFiles.map((f) => f.path));
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// Now load and render all files
|
|
1158
|
+
if (ctx.allFilesData && ctx.allFilesData.length > 0) {
|
|
1159
|
+
renderAllFilesOnCanvas(ctx, ctx.allFilesData);
|
|
1160
|
+
const fileCountEl = document.getElementById("fileCount");
|
|
1161
|
+
if (fileCountEl) fileCountEl.textContent = ctx.allFilesData.length;
|
|
1162
|
+
} else {
|
|
1163
|
+
await loadAllFiles(ctx);
|
|
1164
|
+
}
|
|
1165
|
+
};
|
|
1166
|
+
doRender();
|
|
1167
|
+
}
|
|
1168
|
+
} else {
|
|
1169
|
+
const state = ctx.snap().context;
|
|
1170
|
+
|
|
1171
|
+
// Always re-render the commit timeline sidebar
|
|
1172
|
+
renderCommitTimeline(ctx);
|
|
1173
|
+
|
|
1174
|
+
if (state.currentCommitHash) {
|
|
1175
|
+
const commit = state.commits.find(
|
|
1176
|
+
(c) => c.hash === state.currentCommitHash,
|
|
1177
|
+
);
|
|
1178
|
+
updateCommitInfo(state.currentCommitHash, commit?.message || "");
|
|
1179
|
+
|
|
1180
|
+
if (state.commitFiles.length > 0) {
|
|
1181
|
+
// We have commit files in state — render them
|
|
1182
|
+
ctx.commitFilesData = state.commitFiles;
|
|
1183
|
+
renderFilesOnCanvas(ctx, state.commitFiles, state.currentCommitHash);
|
|
1184
|
+
populateChangedFilesPanel(ctx, state.commitFiles);
|
|
1185
|
+
const fileCountEl = document.getElementById("fileCount");
|
|
1186
|
+
if (fileCountEl) fileCountEl.textContent = state.commitFiles.length;
|
|
1187
|
+
} else {
|
|
1188
|
+
// Re-fetch commit files since we cleared commitFilesData
|
|
1189
|
+
selectCommit(ctx, state.currentCommitHash);
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// Re-highlight active commit in sidebar
|
|
1193
|
+
requestAnimationFrame(() => {
|
|
1194
|
+
document.querySelectorAll(".commit-item").forEach((el) => {
|
|
1195
|
+
(el as HTMLElement).classList.toggle(
|
|
1196
|
+
"active",
|
|
1197
|
+
(el as HTMLElement).dataset.hash === state.currentCommitHash,
|
|
1198
|
+
);
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// ─── Re-render current view ──────────────────────────────
|
|
1206
|
+
export function rerenderCurrentView(ctx: CanvasContext) {
|
|
1207
|
+
const data = ctx.allFilesData || ctx.snap().context.allFiles;
|
|
1208
|
+
if (data && data.length > 0) {
|
|
1209
|
+
renderAllFilesOnCanvas(ctx, data);
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// ─── Changed files panel (JSX) ──────────────────────────
|
|
1214
|
+
function ChangedFilesList({
|
|
1215
|
+
fileStats,
|
|
1216
|
+
totalAdd,
|
|
1217
|
+
totalDel,
|
|
1218
|
+
count,
|
|
1219
|
+
}: {
|
|
1220
|
+
fileStats: any[];
|
|
1221
|
+
totalAdd: number;
|
|
1222
|
+
totalDel: number;
|
|
1223
|
+
count: number;
|
|
1224
|
+
}) {
|
|
1225
|
+
const statusColors = {
|
|
1226
|
+
added: "#22c55e",
|
|
1227
|
+
modified: "#eab308",
|
|
1228
|
+
deleted: "#ef4444",
|
|
1229
|
+
renamed: "#a78bfa",
|
|
1230
|
+
copied: "#60a5fa",
|
|
1231
|
+
};
|
|
1232
|
+
const statusIcons = {
|
|
1233
|
+
added: "+",
|
|
1234
|
+
modified: "~",
|
|
1235
|
+
deleted: "−",
|
|
1236
|
+
renamed: "→",
|
|
1237
|
+
copied: "⊕",
|
|
1238
|
+
};
|
|
1239
|
+
|
|
1240
|
+
return (
|
|
1241
|
+
<div
|
|
1242
|
+
className="changed-files-container-inner"
|
|
1243
|
+
style={{
|
|
1244
|
+
width: "100%",
|
|
1245
|
+
height: "100%",
|
|
1246
|
+
display: "flex",
|
|
1247
|
+
flexDirection: "column",
|
|
1248
|
+
}}
|
|
1249
|
+
>
|
|
1250
|
+
<div className="changed-files-summary">
|
|
1251
|
+
<span className="stat-add">+{totalAdd}</span>
|
|
1252
|
+
<span className="stat-del">−{totalDel}</span>
|
|
1253
|
+
<span className="stat-files">
|
|
1254
|
+
{count} file{count > 1 ? "s" : ""}
|
|
1255
|
+
</span>
|
|
1256
|
+
</div>
|
|
1257
|
+
{fileStats.map((f) => {
|
|
1258
|
+
const color = statusColors[f.status] || "#a855f7";
|
|
1259
|
+
const icon = statusIcons[f.status] || "~";
|
|
1260
|
+
const name = f.path.split("/").pop();
|
|
1261
|
+
const dir = f.path.includes("/")
|
|
1262
|
+
? f.path.substring(0, f.path.lastIndexOf("/"))
|
|
1263
|
+
: "";
|
|
1264
|
+
return (
|
|
1265
|
+
<div
|
|
1266
|
+
key={f.path}
|
|
1267
|
+
className="changed-file-item"
|
|
1268
|
+
title={f.path}
|
|
1269
|
+
onClick={() => {
|
|
1270
|
+
if (!_panelCtx) return;
|
|
1271
|
+
// Animated zoom+pan to the file
|
|
1272
|
+
import("./canvas").then(({ jumpToFile }) => {
|
|
1273
|
+
jumpToFile(_panelCtx!, f.path);
|
|
1274
|
+
});
|
|
1275
|
+
}}
|
|
1276
|
+
>
|
|
1277
|
+
<span className="changed-file-status" style={`color: ${color} `}>
|
|
1278
|
+
{icon}
|
|
1279
|
+
</span>
|
|
1280
|
+
<span className="changed-file-name">{name}</span>
|
|
1281
|
+
{dir ? <span className="changed-file-dir">{dir}</span> : null}
|
|
1282
|
+
<span className="changed-file-stats">
|
|
1283
|
+
{f.additions > 0 ? (
|
|
1284
|
+
<span className="stat-add">+{f.additions}</span>
|
|
1285
|
+
) : null}
|
|
1286
|
+
{f.deletions > 0 ? (
|
|
1287
|
+
<span className="stat-del">−{f.deletions}</span>
|
|
1288
|
+
) : null}
|
|
1289
|
+
</span>
|
|
1290
|
+
</div>
|
|
1291
|
+
);
|
|
1292
|
+
})}
|
|
1293
|
+
</div>
|
|
1294
|
+
);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
export function populateChangedFilesPanel(ctx: CanvasContext, files: any[]) {
|
|
1298
|
+
setPanelCtx(ctx);
|
|
1299
|
+
const panel = document.getElementById("changedFilesPanel");
|
|
1300
|
+
const listEl = document.getElementById("changedFilesList");
|
|
1301
|
+
if (!panel || !listEl) return;
|
|
1302
|
+
|
|
1303
|
+
if (files.length === 0) {
|
|
1304
|
+
panel.style.display = "none";
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Filter by active layer — only show changed files that are in the layer
|
|
1309
|
+
const activeLayer = getActiveLayer();
|
|
1310
|
+
const filteredFiles = activeLayer
|
|
1311
|
+
? files.filter((f) => !!activeLayer.files[f.path])
|
|
1312
|
+
: files;
|
|
1313
|
+
|
|
1314
|
+
if (filteredFiles.length === 0) {
|
|
1315
|
+
panel.style.display = "none";
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
let totalAdd = 0,
|
|
1320
|
+
totalDel = 0;
|
|
1321
|
+
const fileStats = filteredFiles.map((f) => {
|
|
1322
|
+
let additions = 0,
|
|
1323
|
+
deletions = 0;
|
|
1324
|
+
if (f.hunks) {
|
|
1325
|
+
f.hunks.forEach((h) => {
|
|
1326
|
+
h.lines.forEach((l) => {
|
|
1327
|
+
if (l.type === "add") additions++;
|
|
1328
|
+
else if (l.type === "del") deletions++;
|
|
1329
|
+
});
|
|
1330
|
+
});
|
|
1331
|
+
} else if (f.status === "added" && f.content) {
|
|
1332
|
+
additions = f.content.split("\n").length;
|
|
1333
|
+
} else if (f.status === "deleted" && f.content) {
|
|
1334
|
+
deletions = f.content.split("\n").length;
|
|
1335
|
+
}
|
|
1336
|
+
totalAdd += additions;
|
|
1337
|
+
totalDel += deletions;
|
|
1338
|
+
return { ...f, additions, deletions };
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
render(
|
|
1342
|
+
<ChangedFilesList
|
|
1343
|
+
fileStats={fileStats}
|
|
1344
|
+
totalAdd={totalAdd}
|
|
1345
|
+
totalDel={totalDel}
|
|
1346
|
+
count={filteredFiles.length}
|
|
1347
|
+
/>,
|
|
1348
|
+
listEl,
|
|
1349
|
+
);
|
|
1350
|
+
|
|
1351
|
+
if (panel.dataset.manuallyClosed !== "true") {
|
|
1352
|
+
panel.style.display = "flex";
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
// ─── Virtual Files Integration ───────────────────────────
|
|
1357
|
+
/**
|
|
1358
|
+
* Process large files for virtual card creation
|
|
1359
|
+
* Called after files are loaded to detect compression opportunities
|
|
1360
|
+
*/
|
|
1361
|
+
export async function processVirtualFiles(ctx: CanvasContext): Promise<void> {
|
|
1362
|
+
const files = ctx.allFilesData || [];
|
|
1363
|
+
if (files.length === 0) return;
|
|
1364
|
+
|
|
1365
|
+
try {
|
|
1366
|
+
const created = await processVirtualFileSet(ctx, files);
|
|
1367
|
+
(window as any).__virtualStats = {
|
|
1368
|
+
fileCount: files.length,
|
|
1369
|
+
created,
|
|
1370
|
+
virtualCards: document.querySelectorAll('.virtual-card').length,
|
|
1371
|
+
};
|
|
1372
|
+
if (created > 0) {
|
|
1373
|
+
console.log(`[virtual-files] Created ${created} transclusion cards`);
|
|
1374
|
+
}
|
|
1375
|
+
} catch (err) {
|
|
1376
|
+
(window as any).__virtualStats = {
|
|
1377
|
+
fileCount: files.length,
|
|
1378
|
+
created: 0,
|
|
1379
|
+
error: err?.message || String(err),
|
|
1380
|
+
};
|
|
1381
|
+
console.warn(`[virtual-files] Failed to process transclusion cards:`, err);
|
|
1382
|
+
}
|
|
1383
|
+
}
|