dsh-ide-git 0.19.2 → 0.19.4

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/src/client.js CHANGED
@@ -101,7 +101,8 @@ window.__ModuleLoader__.load({
101
101
  'branches.head': 'HEAD(当前分支)',
102
102
  'branches.favorites': '收藏',
103
103
  'branches.starHint': "已收藏",
104
- 'branches.pickHint': '单击选中,双击签出',
104
+ 'branches.pickHint': '单击选中,双击查看提交历史',
105
+ 'branches.headHint': '双击查看当前分支的提交历史',
105
106
  'branches.worktree': '已在另一工作区检出',
106
107
  'action.stash': '贮藏',
107
108
  'action.favorite': '收藏/取消收藏当前分支',
@@ -347,6 +348,7 @@ window.__ModuleLoader__.load({
347
348
  'action.unavailable': '当前状态不可用',
348
349
  'filter.branch': '分支',
349
350
  'filter.user': '用户',
351
+ 'history.me': '我',
350
352
  'filter.date': '日期',
351
353
  'filter.path': '路径',
352
354
  'filter.all': '全部',
@@ -426,7 +428,8 @@ window.__ModuleLoader__.load({
426
428
  'branches.head': 'HEAD (current branch)',
427
429
  'branches.favorites': 'Favorites',
428
430
  'branches.starHint': "Favorited",
429
- 'branches.pickHint': 'Click to select, double-click to check out',
431
+ 'branches.pickHint': 'Click to select, double-click to browse its history',
432
+ 'branches.headHint': 'Double-click to browse the current branch\'s history',
430
433
  'branches.worktree': 'Checked out in another working tree',
431
434
  'action.stash': 'Stash',
432
435
  'action.favorite': 'Favorite / unfavorite current branch',
@@ -672,6 +675,7 @@ window.__ModuleLoader__.load({
672
675
  'action.unavailable': 'Not available in the current state',
673
676
  'filter.branch': 'Branch',
674
677
  'filter.user': 'User',
678
+ 'history.me': 'Me',
675
679
  'filter.date': 'Date',
676
680
  'filter.path': 'Path',
677
681
  'filter.all': 'All',
@@ -759,7 +763,8 @@ window.__ModuleLoader__.load({
759
763
  'branches.head': 'HEAD(當前分支)',
760
764
  'branches.favorites': '我的最愛',
761
765
  'branches.starHint': "已收藏",
762
- 'branches.pickHint': '單擊選取,雙擊簽出',
766
+ 'branches.pickHint': '單擊選中,雙擊查看提交歷史',
767
+ 'branches.headHint': '雙擊查看當前分支的提交歷史',
763
768
  'branches.worktree': '已喺另一個工作區簽出',
764
769
  'action.stash': '貯藏',
765
770
  'action.favorite': '加入/取消我的最愛',
@@ -1005,6 +1010,7 @@ window.__ModuleLoader__.load({
1005
1010
  'action.unavailable': '當前狀態下唔可用',
1006
1011
  'filter.branch': '分支',
1007
1012
  'filter.user': '提交者',
1013
+ 'history.me': '我',
1008
1014
  'filter.date': '日期',
1009
1015
  'filter.path': '路徑',
1010
1016
  'filter.all': '全部',
@@ -1084,7 +1090,8 @@ window.__ModuleLoader__.load({
1084
1090
  'branches.head': 'HEAD(目前分支)',
1085
1091
  'branches.favorites': '我的最愛',
1086
1092
  'branches.starHint': "已收藏",
1087
- 'branches.pickHint': '單擊選取,雙擊簽出',
1093
+ 'branches.pickHint': '單擊選取,雙擊檢視提交歷史',
1094
+ 'branches.headHint': '雙擊檢視目前分支的提交歷史',
1088
1095
  'branches.worktree': '已在另一個工作區簽出',
1089
1096
  'action.stash': '暫存擱置',
1090
1097
  'action.favorite': '加入/移除我的最愛',
@@ -1330,6 +1337,7 @@ window.__ModuleLoader__.load({
1330
1337
  'action.unavailable': '目前狀態下無法使用',
1331
1338
  'filter.branch': '分支',
1332
1339
  'filter.user': '提交者',
1340
+ 'history.me': '我',
1333
1341
  'filter.date': '日期',
1334
1342
  'filter.path': '路徑',
1335
1343
  'filter.all': '全部',
@@ -1409,7 +1417,8 @@ window.__ModuleLoader__.load({
1409
1417
  'branches.head': 'HEAD(當前分支)',
1410
1418
  'branches.favorites': '我的最愛',
1411
1419
  'branches.starHint': "已收藏",
1412
- 'branches.pickHint': '單擊選取,雙擊簽出',
1420
+ 'branches.pickHint': '單擊選中,雙擊查看提交歷史',
1421
+ 'branches.headHint': '雙擊查看當前分支的提交歷史',
1413
1422
  'branches.worktree': '已喺另一個工作區簽出',
1414
1423
  'action.stash': '貯藏',
1415
1424
  'action.favorite': '加入/取消我的最愛',
@@ -1655,6 +1664,7 @@ window.__ModuleLoader__.load({
1655
1664
  'action.unavailable': '當前狀態下唔可用',
1656
1665
  'filter.branch': '分支',
1657
1666
  'filter.user': '提交者',
1667
+ 'history.me': '我',
1658
1668
  'filter.date': '日期',
1659
1669
  'filter.path': '路徑',
1660
1670
  'filter.all': '全部',
@@ -1734,7 +1744,8 @@ window.__ModuleLoader__.load({
1734
1744
  'branches.head': 'HEAD(現在のブランチ)',
1735
1745
  'branches.favorites': 'お気に入り',
1736
1746
  'branches.starHint': "お気に入り済み",
1737
- 'branches.pickHint': 'クリックで選択、ダブルクリックでチェックアウト',
1747
+ 'branches.pickHint': 'クリックで選択、ダブルクリックで履歴を表示',
1748
+ 'branches.headHint': 'ダブルクリックで現在のブランチの履歴を表示',
1738
1749
  'branches.worktree': '別のワークツリーでチェックアウト済み',
1739
1750
  'action.stash': 'スタッシュ',
1740
1751
  'action.favorite': '現在のブランチをお気に入りに追加/解除',
@@ -1980,6 +1991,7 @@ window.__ModuleLoader__.load({
1980
1991
  'action.unavailable': '現在の状態では使用できません',
1981
1992
  'filter.branch': 'ブランチ',
1982
1993
  'filter.user': 'コミット者',
1994
+ 'history.me': '自分',
1983
1995
  'filter.date': '日付',
1984
1996
  'filter.path': 'パス',
1985
1997
  'filter.all': 'すべて',
@@ -2059,7 +2071,8 @@ window.__ModuleLoader__.load({
2059
2071
  'branches.head': 'HEAD(현재 브랜치)',
2060
2072
  'branches.favorites': '즐겨찾기',
2061
2073
  'branches.starHint': "즐겨찾기",
2062
- 'branches.pickHint': '클릭하여 선택, 더블클릭하여 체크아웃',
2074
+ 'branches.pickHint': '클릭하여 선택, 더블클릭으로 커밋 히스토리 보기',
2075
+ 'branches.headHint': '더블클릭으로 현재 브랜치의 히스토리 보기',
2063
2076
  'branches.worktree': '다른 워크트리에서 체크아웃됨',
2064
2077
  'action.stash': '스태시',
2065
2078
  'action.favorite': '현재 브랜치 즐겨찾기 추가/해제',
@@ -2305,6 +2318,7 @@ window.__ModuleLoader__.load({
2305
2318
  'action.unavailable': '현재 상태에서는 사용할 수 없습니다',
2306
2319
  'filter.branch': '브랜치',
2307
2320
  'filter.user': '작성자',
2321
+ 'history.me': '나',
2308
2322
  'filter.date': '날짜',
2309
2323
  'filter.path': '경로',
2310
2324
  'filter.all': '전체',
@@ -2384,7 +2398,8 @@ window.__ModuleLoader__.load({
2384
2398
  'branches.head': 'HEAD (aktueller Branch)',
2385
2399
  'branches.favorites': 'Favoriten',
2386
2400
  'branches.starHint': "Favorit",
2387
- 'branches.pickHint': 'Klicken zum Auswählen, doppelklicken zum Auschecken',
2401
+ 'branches.pickHint': 'Klicken zum Auswählen, Doppelklick zeigt den Verlauf',
2402
+ 'branches.headHint': 'Doppelklick zeigt den Verlauf des aktuellen Branches',
2388
2403
  'branches.worktree': 'In einem anderen Arbeitsbaum ausgecheckt',
2389
2404
  'action.stash': 'Stash',
2390
2405
  'action.favorite': 'Aktuellen Branch als Favorit setzen/entfernen',
@@ -2630,6 +2645,7 @@ window.__ModuleLoader__.load({
2630
2645
  'action.unavailable': 'Im aktuellen Zustand nicht verfügbar',
2631
2646
  'filter.branch': 'Branch',
2632
2647
  'filter.user': 'Autor',
2648
+ 'history.me': 'Ich',
2633
2649
  'filter.date': 'Datum',
2634
2650
  'filter.path': 'Pfad',
2635
2651
  'filter.all': 'Alle',
@@ -2709,7 +2725,8 @@ window.__ModuleLoader__.load({
2709
2725
  'branches.head': 'HEAD (branche courante)',
2710
2726
  'branches.favorites': 'Favoris',
2711
2727
  'branches.starHint': "Favori",
2712
- 'branches.pickHint': 'Cliquer pour sélectionner, double-cliquer pour extraire',
2728
+ 'branches.pickHint': 'Clic pour sélectionner, double-clic pour ouvrir l\'historique',
2729
+ 'branches.headHint': 'Double-clic pour l\'historique de la branche courante',
2713
2730
  'branches.worktree': 'Extraite dans un autre arbre de travail',
2714
2731
  'action.stash': 'Remisage',
2715
2732
  'action.favorite': 'Ajouter/retirer la branche courante des favoris',
@@ -2955,6 +2972,7 @@ window.__ModuleLoader__.load({
2955
2972
  'action.unavailable': 'Indisponible dans l\'état actuel',
2956
2973
  'filter.branch': 'Branche',
2957
2974
  'filter.user': 'Auteur',
2975
+ 'history.me': 'Moi',
2958
2976
  'filter.date': 'Date',
2959
2977
  'filter.path': 'Chemin',
2960
2978
  'filter.all': 'Tout',
@@ -3034,7 +3052,8 @@ window.__ModuleLoader__.load({
3034
3052
  'branches.head': 'HEAD (текущая ветка)',
3035
3053
  'branches.favorites': 'Избранное',
3036
3054
  'branches.starHint': "В избранном",
3037
- 'branches.pickHint': 'Щёлкните для выбора, двойной щелчок — переключение',
3055
+ 'branches.pickHint': 'Клик — выбор, двойной клик — история коммитов',
3056
+ 'branches.headHint': 'Двойной клик — история текущей ветки',
3038
3057
  'branches.worktree': 'Уже развёрнута в другом рабочем дереве',
3039
3058
  'action.stash': 'Отложить',
3040
3059
  'action.favorite': 'Добавить/убрать текущую ветку из избранного',
@@ -3280,6 +3299,7 @@ window.__ModuleLoader__.load({
3280
3299
  'action.unavailable': 'Недоступно в текущем состоянии',
3281
3300
  'filter.branch': 'Ветка',
3282
3301
  'filter.user': 'Автор',
3302
+ 'history.me': 'Я',
3283
3303
  'filter.date': 'Дата',
3284
3304
  'filter.path': 'Путь',
3285
3305
  'filter.all': 'Все',
@@ -3359,7 +3379,8 @@ window.__ModuleLoader__.load({
3359
3379
  'branches.head': 'HEAD (branch atual)',
3360
3380
  'branches.favorites': 'Favoritas',
3361
3381
  'branches.starHint': "Favorito",
3362
- 'branches.pickHint': 'Clique para selecionar, clique duplo para fazer checkout',
3382
+ 'branches.pickHint': 'Clique para selecionar, duplo clique abre o histórico',
3383
+ 'branches.headHint': 'Duplo clique abre o histórico do branch atual',
3363
3384
  'branches.worktree': 'Já em checkout em outra árvore de trabalho',
3364
3385
  'action.stash': 'Stash',
3365
3386
  'action.favorite': 'Adicionar/remover a branch atual dos favoritos',
@@ -3605,6 +3626,7 @@ window.__ModuleLoader__.load({
3605
3626
  'action.unavailable': 'Indisponível no estado atual',
3606
3627
  'filter.branch': 'Branch',
3607
3628
  'filter.user': 'Autor',
3629
+ 'history.me': 'Eu',
3608
3630
  'filter.date': 'Data',
3609
3631
  'filter.path': 'Caminho',
3610
3632
  'filter.all': 'Tudo',
@@ -3684,7 +3706,8 @@ window.__ModuleLoader__.load({
3684
3706
  'branches.head': 'HEAD (branch corrente)',
3685
3707
  'branches.favorites': 'Preferiti',
3686
3708
  'branches.starHint': "Preferito",
3687
- 'branches.pickHint': 'Clic per selezionare, doppio clic per fare checkout',
3709
+ 'branches.pickHint': 'Clic per selezionare, doppio clic apre la cronologia',
3710
+ 'branches.headHint': 'Doppio clic apre la cronologia del branch corrente',
3688
3711
  'branches.worktree': 'Già in checkout in un altro albero di lavoro',
3689
3712
  'action.stash': 'Stash',
3690
3713
  'action.favorite': 'Aggiungi/rimuovi il branch corrente dai preferiti',
@@ -3930,6 +3953,7 @@ window.__ModuleLoader__.load({
3930
3953
  'action.unavailable': 'Non disponibile nello stato attuale',
3931
3954
  'filter.branch': 'Branch',
3932
3955
  'filter.user': 'Autore',
3956
+ 'history.me': 'Io',
3933
3957
  'filter.date': 'Data',
3934
3958
  'filter.path': 'Percorso',
3935
3959
  'filter.all': 'Tutti',
@@ -4009,7 +4033,8 @@ window.__ModuleLoader__.load({
4009
4033
  'branches.head': 'HEAD (huidige branch)',
4010
4034
  'branches.favorites': 'Favorieten',
4011
4035
  'branches.starHint': "Favoriet",
4012
- 'branches.pickHint': 'Klik om te selecteren, dubbelklik om uit te checken',
4036
+ 'branches.pickHint': 'Klik om te selecteren, dubbelklik opent de geschiedenis',
4037
+ 'branches.headHint': 'Dubbelklik opent de geschiedenis van de huidige branch',
4013
4038
  'branches.worktree': 'Al uitgecheckt in een andere werkboom',
4014
4039
  'action.stash': 'Stash',
4015
4040
  'action.favorite': 'Huidige branch aan favorieten toevoegen/onttrekken',
@@ -4255,6 +4280,7 @@ window.__ModuleLoader__.load({
4255
4280
  'action.unavailable': 'Niet beschikbaar in de huidige staat',
4256
4281
  'filter.branch': 'Branch',
4257
4282
  'filter.user': 'Auteur',
4283
+ 'history.me': 'Ik',
4258
4284
  'filter.date': 'Datum',
4259
4285
  'filter.path': 'Pad',
4260
4286
  'filter.all': 'Alles',
@@ -4334,7 +4360,8 @@ window.__ModuleLoader__.load({
4334
4360
  'branches.head': 'HEAD (bieżąca gałąź)',
4335
4361
  'branches.favorites': 'Ulubione',
4336
4362
  'branches.starHint': "Ulubione",
4337
- 'branches.pickHint': 'Kliknij, aby wybrać; kliknij dwukrotnie, aby przełączyć',
4363
+ 'branches.pickHint': 'Kliknij, aby zaznaczyć, dwuklik otwiera historię',
4364
+ 'branches.headHint': 'Dwuklik otwiera historię bieżącej gałęzi',
4338
4365
  'branches.worktree': 'Już wybrana w innym drzewie roboczym',
4339
4366
  'action.stash': 'Schowek',
4340
4367
  'action.favorite': 'Dodaj/usuń bieżącą gałąź z ulubionych',
@@ -4580,6 +4607,7 @@ window.__ModuleLoader__.load({
4580
4607
  'action.unavailable': 'Niedostępne w bieżącym stanie',
4581
4608
  'filter.branch': 'Gałąź',
4582
4609
  'filter.user': 'Autor',
4610
+ 'history.me': 'Ja',
4583
4611
  'filter.date': 'Data',
4584
4612
  'filter.path': 'Ścieżka',
4585
4613
  'filter.all': 'Wszystkie',
@@ -4659,7 +4687,8 @@ window.__ModuleLoader__.load({
4659
4687
  'branches.head': 'HEAD (aktuell gren)',
4660
4688
  'branches.favorites': 'Favoriter',
4661
4689
  'branches.starHint': "Favorit",
4662
- 'branches.pickHint': 'Klicka för att välja, dubbelklicka för att checka ut',
4690
+ 'branches.pickHint': 'Klicka för att välja, dubbelklick visar historiken',
4691
+ 'branches.headHint': 'Dubbelklick visar historiken för aktuell gren',
4663
4692
  'branches.worktree': 'Redan utcheckad i ett annat arbetsträd',
4664
4693
  'action.stash': 'Stash',
4665
4694
  'action.favorite': 'Lägg till/ta bort aktuell gren som favorit',
@@ -4905,6 +4934,7 @@ window.__ModuleLoader__.load({
4905
4934
  'action.unavailable': 'Inte tillgängligt i aktuellt läge',
4906
4935
  'filter.branch': 'Gren',
4907
4936
  'filter.user': 'Författare',
4937
+ 'history.me': 'Jag',
4908
4938
  'filter.date': 'Datum',
4909
4939
  'filter.path': 'Sökväg',
4910
4940
  'filter.all': 'Alla',
@@ -4984,7 +5014,8 @@ window.__ModuleLoader__.load({
4984
5014
  'branches.head': 'HEAD (geçerli dal)',
4985
5015
  'branches.favorites': 'Sık kullanılanlar',
4986
5016
  'branches.starHint': "Favori",
4987
- 'branches.pickHint': 'Seçmek için tıklayın, çıkarmak için çift tıklayın',
5017
+ 'branches.pickHint': 'Seçmek için tek tık, geçmişi görmek için çift tık',
5018
+ 'branches.headHint': 'Çift tık, geçerli dalın geçmişini açar',
4988
5019
  'branches.worktree': 'Başka bir çalışma ağacında çıkarılmış',
4989
5020
  'action.stash': 'Saklama',
4990
5021
  'action.favorite': 'Geçerli dalı sık kullanılanlara ekle/çıkar',
@@ -5230,6 +5261,7 @@ window.__ModuleLoader__.load({
5230
5261
  'action.unavailable': 'Geçerli durumda kullanılamaz',
5231
5262
  'filter.branch': 'Dal',
5232
5263
  'filter.user': 'Yazar',
5264
+ 'history.me': 'Ben',
5233
5265
  'filter.date': 'Tarih',
5234
5266
  'filter.path': 'Yol',
5235
5267
  'filter.all': 'Tümü',
@@ -5309,7 +5341,8 @@ window.__ModuleLoader__.load({
5309
5341
  'branches.head': 'HEAD (branch saat ini)',
5310
5342
  'branches.favorites': 'Favorit',
5311
5343
  'branches.starHint': "Favorit",
5312
- 'branches.pickHint': 'Klik untuk memilih, klik ganda untuk checkout',
5344
+ 'branches.pickHint': 'Klik untuk memilih, klik ganda membuka riwayat',
5345
+ 'branches.headHint': 'Klik ganda membuka riwayat branch saat ini',
5313
5346
  'branches.worktree': 'Sudah di-checkout di worktree lain',
5314
5347
  'action.stash': 'Stash',
5315
5348
  'action.favorite': 'Tambah/hapus branch saat ini dari favorit',
@@ -5555,6 +5588,7 @@ window.__ModuleLoader__.load({
5555
5588
  'action.unavailable': 'Tidak tersedia pada keadaan saat ini',
5556
5589
  'filter.branch': 'Branch',
5557
5590
  'filter.user': 'Penulis',
5591
+ 'history.me': 'Saya',
5558
5592
  'filter.date': 'Tanggal',
5559
5593
  'filter.path': 'Jalur',
5560
5594
  'filter.all': 'Semua',
@@ -5634,7 +5668,8 @@ window.__ModuleLoader__.load({
5634
5668
  'branches.head': 'HEAD (nhánh hiện tại)',
5635
5669
  'branches.favorites': 'Yêu thích',
5636
5670
  'branches.starHint': "Đã yêu thích",
5637
- 'branches.pickHint': 'Nhấp để chọn, nhấp đôi để checkout',
5671
+ 'branches.pickHint': 'Bấm để chọn, nhấp đúp mở lịch sử commit',
5672
+ 'branches.headHint': 'Nhấp đúp mở lịch sử của nhánh hiện tại',
5638
5673
  'branches.worktree': 'Đã được checkout ở một worktree khác',
5639
5674
  'action.stash': 'Tạm cất',
5640
5675
  'action.favorite': 'Thêm/bỏ nhánh hiện tại khỏi yêu thích',
@@ -5880,6 +5915,7 @@ window.__ModuleLoader__.load({
5880
5915
  'action.unavailable': 'Không dùng được ở trạng thái hiện tại',
5881
5916
  'filter.branch': 'Nhánh',
5882
5917
  'filter.user': 'Tác giả',
5918
+ 'history.me': 'Tôi',
5883
5919
  'filter.date': 'Ngày',
5884
5920
  'filter.path': 'Đường dẫn',
5885
5921
  'filter.all': 'Tất cả',
@@ -5959,7 +5995,8 @@ window.__ModuleLoader__.load({
5959
5995
  'branches.head': 'HEAD (الفرع الحالي)',
5960
5996
  'branches.favorites': 'المفضلة',
5961
5997
  'branches.starHint': "مفضل",
5962
- 'branches.pickHint': 'انقر للتحديد، وانقر نقرًا مزدوجًا للتبديل',
5998
+ 'branches.pickHint': 'انقر للتحديد، ونقرتان مزدوجتان لعرض السجل',
5999
+ 'branches.headHint': 'نقرتان مزدوجتان لعرض سجل الفرع الحالي',
5963
6000
  'branches.worktree': 'مُستخرَج بالفعل في شجرة عمل أخرى',
5964
6001
  'action.stash': 'التخزين المؤقت',
5965
6002
  'action.favorite': 'إضافة الفرع الحالي إلى المفضلة أو إزالته',
@@ -6205,6 +6242,7 @@ window.__ModuleLoader__.load({
6205
6242
  'action.unavailable': 'غير متاح في الحالة الحالية',
6206
6243
  'filter.branch': 'الفرع',
6207
6244
  'filter.user': 'المؤلف',
6245
+ 'history.me': 'أنا',
6208
6246
  'filter.date': 'التاريخ',
6209
6247
  'filter.path': 'المسار',
6210
6248
  'filter.all': 'الكل',
@@ -6284,7 +6322,8 @@ window.__ModuleLoader__.load({
6284
6322
  'branches.head': 'HEAD (वर्तमान शाखा)',
6285
6323
  'branches.favorites': 'पसंदीदा',
6286
6324
  'branches.starHint': "पसंदीदा",
6287
- 'branches.pickHint': 'चुनने के लिए क्लिक करें, चेकआउट के लिए डबल-क्लिक करें',
6325
+ 'branches.pickHint': 'चुनने के लिए क्लिक करें, इतिहास देखने के लिए डबल-क्लिक',
6326
+ 'branches.headHint': 'डबल-क्लिक से वर्तमान ब्रांच का इतिहास देखें',
6288
6327
  'branches.worktree': 'पहले से किसी अन्य वर्कट्री में चेकआउट है',
6289
6328
  'action.stash': 'स्टैश',
6290
6329
  'action.favorite': 'वर्तमान शाखा को पसंदीदा में जोड़ें/हटाएँ',
@@ -6530,6 +6569,7 @@ window.__ModuleLoader__.load({
6530
6569
  'action.unavailable': 'वर्तमान स्थिति में उपलब्ध नहीं',
6531
6570
  'filter.branch': 'शाखा',
6532
6571
  'filter.user': 'लेखक',
6572
+ 'history.me': 'मैं',
6533
6573
  'filter.date': 'दिनांक',
6534
6574
  'filter.path': 'पथ',
6535
6575
  'filter.all': 'सभी',
@@ -6609,7 +6649,8 @@ window.__ModuleLoader__.load({
6609
6649
  'branches.head': 'HEAD (แบรนช์ปัจจุบัน)',
6610
6650
  'branches.favorites': 'รายการโปรด',
6611
6651
  'branches.starHint': "รายการโปรด",
6612
- 'branches.pickHint': 'คลิกเพื่อเลือก ดับเบิลคลิกเพื่อเช็กเอาต์',
6652
+ 'branches.pickHint': 'คลิกเพื่อเลือก ดับเบิลคลิกเพื่อดูประวัติคอมมิต',
6653
+ 'branches.headHint': 'ดับเบิลคลิกเพื่อดูประวัติของแบรนช์ปัจจุบัน',
6613
6654
  'branches.worktree': 'เช็กเอาต์อยู่ในเวิร์กทรีอื่นแล้ว',
6614
6655
  'action.stash': 'สแตช',
6615
6656
  'action.favorite': 'เพิ่ม/移除แบรนช์ปัจจุบันในรายการโปรด',
@@ -6855,6 +6896,7 @@ window.__ModuleLoader__.load({
6855
6896
  'action.unavailable': 'ใช้ไม่ได้ในสถานะปัจจุบัน',
6856
6897
  'filter.branch': 'แบรนช์',
6857
6898
  'filter.user': 'ผู้เขียน',
6899
+ 'history.me': 'ฉัน',
6858
6900
  'filter.date': 'วันที่',
6859
6901
  'filter.path': 'พาธ',
6860
6902
  'filter.all': 'ทั้งหมด',
@@ -9302,11 +9344,16 @@ window.__ModuleLoader__.load({
9302
9344
  closeRef.current()
9303
9345
  }
9304
9346
  const onKey = (event) => { if (event.key === 'Escape') closeRef.current() }
9305
- const timer = setTimeout(() => document.addEventListener('mousedown', onDown, true), 0)
9347
+ /* pointerdown, not mousedown: the list pointer-down handlers call
9348
+ preventDefault(), which suppresses the COMPATIBILITY mouse events
9349
+ (mousedown included) — a capture-phase mousedown listener then never
9350
+ fires and the menu stayed open when the next press landed on a list
9351
+ blank (user report). pointerdown always dispatches. */
9352
+ const timer = setTimeout(() => document.addEventListener('pointerdown', onDown, true), 0)
9306
9353
  document.addEventListener('keydown', onKey)
9307
9354
  return () => {
9308
9355
  clearTimeout(timer)
9309
- document.removeEventListener('mousedown', onDown, true)
9356
+ document.removeEventListener('pointerdown', onDown, true)
9310
9357
  document.removeEventListener('keydown', onKey)
9311
9358
  }
9312
9359
  }, [])
@@ -10293,14 +10340,18 @@ window.__ModuleLoader__.load({
10293
10340
  // A single click only SELECTS (now with Shift/Ctrl+click multi-select —
10294
10341
  // the modifiers are resolved one level up in BranchTree): checking a
10295
10342
  // branch out is a real change of working tree, and a stray click in a
10296
- // long list must not do that (reported on v0.3.7). Double-click checks
10297
- // out, right-click has the whole menu.
10343
+ // long list must not do that (reported on v0.3.7). Since v0.19.3 a
10344
+ // double-click BROWSES that branch's commit history (checkout moved into
10345
+ // the right-click menu), right-click has the whole menu.
10298
10346
  return E('div', {
10299
10347
  className: 'dig-row dig-row-' + kind + (props.selected === true ? ' dig-row-selected' : ''),
10300
10348
  style: { paddingLeft: (BRANCH_INDENT_BASE + (props.depth === undefined ? 0 : props.depth) * BRANCH_INDENT_STEP) + 'px' },
10349
+ /* Rubber-band anchor: the marquee intersects [data-mkey] rows to
10350
+ rebuild the picked set (selKey = local:/remote:/tag: prefixed). */
10351
+ 'data-mkey': props.selKey,
10301
10352
  title: where + ' · ' + t('branches.pickHint'),
10302
10353
  onClick: (event) => props.onPick(event, entry, props.selKey),
10303
- onDoubleClick: () => props.onCheckout(entry),
10354
+ onDoubleClick: () => { if (props.onBrowse !== undefined) props.onBrowse(entry) },
10304
10355
  onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, entry) },
10305
10356
  },
10306
10357
  E('span', { className: 'dig-row-icon dig-tone-' + tone + (starred === true ? ' dig-star-filled' : '') }, E(Icon, { name: glyph, size: 12 })),
@@ -10324,6 +10375,24 @@ window.__ModuleLoader__.load({
10324
10375
  selKeys (`local:name` / `remote:name` / `tag:name` — the prefix keeps a
10325
10376
  branch and a tag that share a name from colliding). */
10326
10377
  const [picks, setPicks] = useState(() => new Set())
10378
+ /* v0.19.4: the Panel bumps clearSignal when ANOTHER list gains the
10379
+ selection (or a blank click clears everywhere) — drop ours. Monotonic
10380
+ epochs start at 0, so the mount pass never clears. */
10381
+ const treeClearSignal = props.clearSignal === undefined ? 0 : props.clearSignal
10382
+ const seenTreeClear = useRef(treeClearSignal)
10383
+ /* v0.19.4 echo guard: only USER gestures may flow up to the Panel — the
10384
+ mirror effect below reports picks exactly once per gesture. Reactive
10385
+ setPicks calls (the clear-signal effect above, the survival filter)
10386
+ must NOT re-enter the forwarder, or the epoch it bumps clears the
10387
+ selection the OTHER list just made (the T1b echo bug). */
10388
+ const reportGestureRef = useRef(false)
10389
+ useEffect(() => {
10390
+ if (seenTreeClear.current !== treeClearSignal) {
10391
+ seenTreeClear.current = treeClearSignal
10392
+ reportGestureRef.current = false
10393
+ setPicks(new Set())
10394
+ }
10395
+ }, [treeClearSignal])
10327
10396
  const pickAnchor = useRef(null)
10328
10397
  const visibleRef = useRef([])
10329
10398
  const [foldedFolders, setFoldedFolders] = useState(() => {
@@ -10352,8 +10421,13 @@ window.__ModuleLoader__.load({
10352
10421
  // favorites section and the local tree show the starred rows first.
10353
10422
  const locals = (branches === null ? [] : branches.local).filter((entry) => match(entry.name))
10354
10423
  .sort((a, b) => (favorites.indexOf(a.name) >= 0 ? 0 : 1) - (favorites.indexOf(b.name) >= 0 ? 0 : 1))
10424
+ /* The HEAD row acts like a branch row since v0.19.3: click selects,
10425
+ double-click browses the current branch's history, right-click opens
10426
+ the same row menu (quick jump back to HEAD lives there). */
10427
+ const headEntry = locals.find((entry) => entry.head === true) || null
10355
10428
  /* A refresh that makes a picked ref disappear must not keep it selected. */
10356
10429
  useEffect(() => {
10430
+ reportGestureRef.current = false
10357
10431
  setPicks((previous) => {
10358
10432
  if (previous.size === 0) return previous
10359
10433
  const alive = new Set()
@@ -10371,6 +10445,8 @@ window.__ModuleLoader__.load({
10371
10445
  render-time call would trip the hooks rules): the rail reads them so
10372
10446
  its buttons can act on the selection instead of only on HEAD. */
10373
10447
  useEffect(() => {
10448
+ if (reportGestureRef.current !== true) return
10449
+ reportGestureRef.current = false
10374
10450
  if (props.onPicksChange !== undefined) props.onPicksChange(picks)
10375
10451
  }, [picks])
10376
10452
  /**
@@ -10405,6 +10481,7 @@ window.__ModuleLoader__.load({
10405
10481
  return out
10406
10482
  }
10407
10483
  const handlePick = useCallback((event, entry, selKey) => {
10484
+ reportGestureRef.current = true
10408
10485
  const ordered = visibleRef.current.map((leaf) => branchSelKeyOf(leaf.entry))
10409
10486
  if (event.shiftKey === true) {
10410
10487
  /* No anchor yet (fresh tree, nothing clicked): VS Code semantics —
@@ -10439,7 +10516,7 @@ window.__ModuleLoader__.load({
10439
10516
  selected: picks.has(branchSelKeyOf(entry)) === true,
10440
10517
  starred: entry.tag !== true && entry.remote !== true && favorites.indexOf(entry.name) >= 0,
10441
10518
  onPick: handlePick,
10442
- onCheckout: props.onCheckout, onMenu: onRowMenu,
10519
+ onBrowse: props.onBrowse, onMenu: onRowMenu,
10443
10520
  })
10444
10521
  const rowsOf = (node, prefix, depth) => {
10445
10522
  const out = []
@@ -10504,6 +10581,30 @@ window.__ModuleLoader__.load({
10504
10581
  }
10505
10582
  props.onBranchMenu(event, entry, null)
10506
10583
  }
10584
+ /* v0.19.3: the tree rubber-bands like the other two lists (shared hook).
10585
+ Tree rows have no fixed height and render unfolded-only, so the row
10586
+ rects are a LIVE DOM measure over the [data-mkey] rows BranchRow
10587
+ carries; section/folder headers are buttons and never start a drag.
10588
+ Favorited branches render twice (favorites + local sections) — both
10589
+ map to one selKey, so a Set dedupes them for free. */
10590
+ const treeHostRef = useRef(null)
10591
+ const treeScrollRef = useRef(null)
10592
+ const treeMarquee = useRubberBand({
10593
+ scrollRef: treeScrollRef,
10594
+ drawRef: treeHostRef,
10595
+ rectsOf: () => {
10596
+ const host = treeScrollRef.current
10597
+ if (host === null) return []
10598
+ const hostRect = host.getBoundingClientRect()
10599
+ const rects = []
10600
+ host.querySelectorAll('[data-mkey]').forEach((row) => {
10601
+ const rect = row.getBoundingClientRect()
10602
+ rects.push({ key: row.getAttribute('data-mkey'), top: rect.top - hostRect.top + host.scrollTop, bottom: rect.bottom - hostRect.top + host.scrollTop })
10603
+ })
10604
+ return rects
10605
+ },
10606
+ onPick: (keys) => { reportGestureRef.current = true; setPicks(new Set(keys)) },
10607
+ })
10507
10608
  /* List-scope keyboard: Ctrl/Cmd+C copies the picked names, Delete asks
10508
10609
  Panel for a grouped delete (head branches are skipped — git refuses
10509
10610
  them anyway), Ctrl+A selects every visible row, Esc clears. */
@@ -10533,6 +10634,7 @@ window.__ModuleLoader__.load({
10533
10634
  }
10534
10635
  return E('div', {
10535
10636
  className: 'dig-tree',
10637
+ ref: treeHostRef,
10536
10638
  tabIndex: 0,
10537
10639
  onKeyDown: onTreeKeyDown,
10538
10640
  onPointerDownCapture: (event) => {
@@ -10545,12 +10647,35 @@ window.__ModuleLoader__.load({
10545
10647
  placeholder: t('branches.filter'),
10546
10648
  onChange: (event) => setFilter(event.target.value),
10547
10649
  })),
10548
- E('div', { className: 'dig-tree-scroll' },
10549
- E('div', { className: 'dig-section-head dig-section-head-static' }, t('branches.head')),
10650
+ E('div', {
10651
+ className: 'dig-tree-scroll',
10652
+ ref: treeScrollRef,
10653
+ onPointerDown: treeMarquee.onPointerDown,
10654
+ onClickCapture: (event) => {
10655
+ if (treeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
10656
+ /* v0.19.4: a blank click (not on a branch row or a control) clears
10657
+ the picks here AND everywhere else — "selection follows the last
10658
+ gesture". The Set goes up through the same forwarder a row click
10659
+ uses, so the other two lists reset as well. */
10660
+ const target = event.target
10661
+ if (target instanceof Element && target.closest('[data-mkey],button,input,textarea,select,a') === null) {
10662
+ reportGestureRef.current = true
10663
+ setPicks(new Set())
10664
+ }
10665
+ },
10666
+ },
10667
+ headEntry === null ? E('div', { className: 'dig-section-head dig-section-head-static' }, t('branches.head')) : E('div', {
10668
+ className: 'dig-section-head dig-section-head-static dig-head-branch',
10669
+ title: headEntry.name + ' · ' + t('branches.headHint'),
10670
+ onClick: (event) => { event.stopPropagation(); handlePick(event, headEntry, branchSelKeyOf(headEntry)) },
10671
+ onDoubleClick: () => { if (props.onBrowse !== undefined) props.onBrowse(headEntry) },
10672
+ onContextMenu: (event) => { event.preventDefault(); onRowMenu(event, headEntry) },
10673
+ }, t('branches.head')),
10550
10674
  favorites.length === 0 ? null : section('favorites', t('branches.favorites'), locals.filter((entry) => favorites.indexOf(entry.name) >= 0), (entry) => entry),
10551
10675
  section('local', t('branches.local'), locals, (entry) => entry, true),
10552
10676
  section('remote', t('branches.remote'), remotes, (entry) => Object.assign({}, entry, { remote: true })),
10553
- section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true }))))
10677
+ section('tags', t('branches.tags'), tags, (entry) => Object.assign({}, entry, { tag: true }))),
10678
+ E(MarqueeBox, { box: treeMarquee.box }))
10554
10679
  }
10555
10680
 
10556
10681
  /* ============================== changes ============================== */
@@ -10645,10 +10770,11 @@ window.__ModuleLoader__.load({
10645
10770
  const anchorRef = props.anchorRef
10646
10771
  const changeItemsRef = useRef(null)
10647
10772
  const changeOffsetsRef = useRef(null)
10648
- const [marquee, setMarquee] = useState(null)
10649
- const marqueeFrame = useRef(0)
10650
- const marqueeState = useRef(null)
10651
- const marqueeSwallowClick = useRef(false)
10773
+ /* The marquee lives in the shared useRubberBand hook (v0.19.3: history
10774
+ and the branch tree drag the same way). changesHostRef is the DRAW
10775
+ host — the hook comment explains why the paint rect must be the list
10776
+ wrapper's, not the scroll container's (v0.19.2 offset report). */
10777
+ const changesHostRef = useRef(null)
10652
10778
  /* Rows in their RENDER order (the order windowing flattens them into) —
10653
10779
  Shift-click spans follow what the eye sees, not the git order. */
10654
10780
  const orderedRowKeys = () => {
@@ -10700,96 +10826,10 @@ window.__ModuleLoader__.load({
10700
10826
  not yank the pane away (v0.18.3). */
10701
10827
  setSelection((previous) => previous.size === 1 && previous.has(key) === true ? previous : new Set([key]))
10702
10828
  }, [setSelection, anchorRef, props.onDiff])
10703
- /* ---- rubber-band selection over the windowed list: the fixed row
10704
- heights + the prefix-sum offsets turn a pixel span back into the
10705
- row-index span (the same windowStart binary search the window uses),
10706
- so the drag never needs the DOM rows to exist. The marquee box itself
10707
- is a viewport-anchored absolute div inside .dig-changes (invariant 13:
10708
- never position:fixed inside the workbench's contain:layout). ---- */
10709
- const marqueeKeysAt = (contentTop, contentBottom) => {
10710
- const items = changeItemsRef.current
10711
- const offsets = changeOffsetsRef.current
10712
- if (items === null || offsets === null || items.length === 0) return []
10713
- const first = Math.max(0, windowStart(offsets, contentTop))
10714
- const last = Math.min(items.length - 1, windowStart(offsets, contentBottom))
10715
- const keys = []
10716
- for (let index = first; index <= last; index += 1) {
10717
- const entry = items[index]
10718
- if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) keys.push(entry.key)
10719
- }
10720
- return keys
10721
- }
10722
- const syncMarquee = () => {
10723
- const state = marqueeState.current
10724
- const host = changesScrollRef.current
10725
- if (state === null || host === null) return
10726
- const rect = host.getBoundingClientRect()
10727
- const contentY0 = state.startY - rect.top + state.scrollY0
10728
- const contentY1 = state.clientY - rect.top + host.scrollTop
10729
- setSelection(new Set(marqueeKeysAt(Math.min(contentY0, contentY1), Math.max(contentY0, contentY1))))
10730
- setMarquee({
10731
- x: Math.min(state.startX, state.clientX) - rect.left,
10732
- y: Math.min(state.startY, state.clientY) - rect.top,
10733
- w: Math.abs(state.clientX - state.startX),
10734
- h: Math.abs(state.clientY - state.startY),
10735
- })
10736
- /* Auto-scroll when the pointer rides an edge; keep the loop alive while
10737
- it does so the selection follows the content streaming past. */
10738
- const EDGE = 28
10739
- let speed = 0
10740
- if (state.clientY - rect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - rect.top)) / 3)
10741
- else if (rect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (rect.bottom - state.clientY)) / 3)
10742
- if (speed !== 0) {
10743
- host.scrollTop += speed * 2
10744
- cancelAnimationFrame(marqueeFrame.current)
10745
- marqueeFrame.current = requestAnimationFrame(syncMarquee)
10746
- }
10747
- }
10748
- const scheduleMarqueeSync = () => {
10749
- cancelAnimationFrame(marqueeFrame.current)
10750
- marqueeFrame.current = requestAnimationFrame(syncMarquee)
10751
- }
10752
- const onListPointerDown = (event) => {
10753
- if (event.button !== 0) return
10754
- const host = changesScrollRef.current
10755
- if (host === null) return
10756
- if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
10757
- // Keep the browser from starting a text selection under the drag; the
10758
- // real clicks still fire (only the selection machinery is suppressed).
10759
- event.preventDefault()
10760
- const rect = host.getBoundingClientRect()
10761
- marqueeState.current = {
10762
- startX: event.clientX,
10763
- startY: event.clientY,
10764
- scrollY0: host.scrollTop,
10765
- clientX: event.clientX,
10766
- clientY: event.clientY,
10767
- active: false,
10768
- }
10769
- const onMove = (moveEvent) => {
10770
- const state = marqueeState.current
10771
- if (state === null) return
10772
- state.clientX = moveEvent.clientX
10773
- state.clientY = moveEvent.clientY
10774
- if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
10775
- state.active = true
10776
- marqueeSwallowClick.current = true
10777
- scheduleMarqueeSync()
10778
- }
10779
- const onUp = () => {
10780
- window.removeEventListener('pointermove', onMove)
10781
- window.removeEventListener('pointerup', onUp)
10782
- cancelAnimationFrame(marqueeFrame.current)
10783
- const wasActive = marqueeState.current !== null && marqueeState.current.active === true
10784
- marqueeState.current = null
10785
- setMarquee(null)
10786
- // The drag's pointerup is followed by a synthetic click on whatever
10787
- // row is under it — swallow exactly that one click.
10788
- if (wasActive === true) setTimeout(() => { marqueeSwallowClick.current = false }, 0)
10789
- }
10790
- window.addEventListener('pointermove', onMove)
10791
- window.addEventListener('pointerup', onUp)
10792
- }
10829
+ /* Rubber-band selection now comes from the shared useRubberBand hook
10830
+ (see its comment): the fixed row heights + the prefix-sum offsets
10831
+ still turn a pixel span back into row keys without any DOM rows —
10832
+ the rectsOf below is the exact former marqueeKeysAt, reshaped. */
10793
10833
  const changes = summary === null ? null : summary.changes
10794
10834
  const conflicted = changes === null ? [] : changes.conflicted
10795
10835
  const staged = changes === null ? [] : changes.staged
@@ -10859,6 +10899,22 @@ window.__ModuleLoader__.load({
10859
10899
  mounted. A tree with thousands of files scrolls at frame rate. */
10860
10900
  const changesScrollRef = useRef(null)
10861
10901
  const changesViewport = useViewportBox(changesScrollRef)
10902
+ const changeMarquee = useRubberBand({
10903
+ scrollRef: changesScrollRef,
10904
+ drawRef: changesHostRef,
10905
+ rectsOf: () => {
10906
+ const items = changeItemsRef.current
10907
+ const offsets = changeOffsetsRef.current
10908
+ if (items === null || offsets === null || items.length === 0) return []
10909
+ const rects = []
10910
+ for (let index = 0; index < items.length; index += 1) {
10911
+ const entry = items[index]
10912
+ if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) rects.push({ key: entry.key, top: offsets[index], bottom: offsets[index + 1] })
10913
+ }
10914
+ return rects
10915
+ },
10916
+ onPick: (keys) => setSelection(new Set(keys)),
10917
+ })
10862
10918
  const changeItems = useMemo(() => {
10863
10919
  const out = []
10864
10920
  const groupHead = (key, title, entries) => E('div', { className: 'dig-group-head' },
@@ -11042,7 +11098,7 @@ window.__ModuleLoader__.load({
11042
11098
  props.onBulkDiscard === undefined ? null : E('button', { type: 'button', className: 'dig-link dig-link-danger', onClick: () => props.onBulkDiscard() }, t('action.discard')),
11043
11099
  props.onCopySelection === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onCopySelection() }, t('selection.copyPaths')),
11044
11100
  E('button', { type: 'button', className: 'dig-link', onClick: () => setSelection(new Set()) }, t('selection.clear')))
11045
- return E('div', { className: 'dig-changes' },
11101
+ return E('div', { className: 'dig-changes', ref: changesHostRef },
11046
11102
  aiConfirmDialog,
11047
11103
  head,
11048
11104
  E('div', { className: 'dig-changes-body' },
@@ -11055,9 +11111,13 @@ window.__ModuleLoader__.load({
11055
11111
  onPointerDownCapture: (event) => {
11056
11112
  if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
11057
11113
  },
11058
- onPointerDown: onListPointerDown,
11114
+ onPointerDown: changeMarquee.onPointerDown,
11059
11115
  onClickCapture: (event) => {
11060
- if (marqueeSwallowClick.current === true) { event.preventDefault(); event.stopPropagation() }
11116
+ if (changeMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
11117
+ /* v0.19.4: blank click clears — the wrapped setter also bumps the
11118
+ other lists' epochs, so this is a clear-everywhere gesture. */
11119
+ const target = event.target
11120
+ if (target instanceof Element && target.closest('.dig-row,.dig-group-head,button,input,textarea,select,a') === null) setSelection(new Set())
11061
11121
  },
11062
11122
  },
11063
11123
  collapsed ? null : [
@@ -11074,10 +11134,7 @@ window.__ModuleLoader__.load({
11074
11134
  and nothing on the list (group head, rows) is ever covered. */
11075
11135
  selectionBar,
11076
11136
  composer),
11077
- marquee === null ? null : E('div', {
11078
- className: 'dig-marquee',
11079
- style: { left: marquee.x + 'px', top: marquee.y + 'px', width: marquee.w + 'px', height: marquee.h + 'px' },
11080
- }))
11137
+ E(MarqueeBox, { box: changeMarquee.box }))
11081
11138
  }
11082
11139
 
11083
11140
  /* ============================== history ============================== */
@@ -11143,6 +11200,122 @@ window.__ModuleLoader__.load({
11143
11200
  return { scrollTop: scrollTop, viewH: viewH }
11144
11201
  }
11145
11202
 
11203
+ /* ---- rubber-band (marquee) selection, shared by all three lists ----
11204
+ One hook, three wiring sites (changes, history, branch tree). The
11205
+ contract: `scrollRef` is the scrolling container the SELECTION math
11206
+ runs against, `drawRef` is the element the marquee box is PAINTED in
11207
+ (its own rect — v0.18.x painted from the scroll container's rect while
11208
+ the box lived inside the list wrapper, so the toolbar height above the
11209
+ list shifted every box visually upward; reported v0.19.2), `rectsOf`
11210
+ returns {key,top,bottom} rows in scroll-content coordinates (exact
11211
+ prefix-sums for the windowed lists, a live DOM measure for the
11212
+ variable-height tree), and `onPick` receives the intersecting keys.
11213
+ A 4px threshold keeps plain clicks clicky; pointerdown is preventDefault-ed
11214
+ so drags never start a native text selection; edge auto-scroll and the
11215
+ rAF-throttled sync carry over unchanged from v0.18.x. */
11216
+ function useRubberBand(options) {
11217
+ const [box, setBox] = useState(null)
11218
+ const stateRef = useRef(null)
11219
+ const frameRef = useRef(0)
11220
+ const swallowRef = useRef(false)
11221
+ const optionsRef = useRef(options)
11222
+ optionsRef.current = options
11223
+ const sync = () => {
11224
+ const opts = optionsRef.current
11225
+ const state = stateRef.current
11226
+ const host = opts.scrollRef.current
11227
+ const draw = opts.drawRef.current
11228
+ if (state === null || host === null || draw === null) return
11229
+ const hostRect = host.getBoundingClientRect()
11230
+ const drawRect = draw.getBoundingClientRect()
11231
+ const y0 = state.startY - hostRect.top + state.scrollY0
11232
+ const y1 = state.clientY - hostRect.top + host.scrollTop
11233
+ const low = Math.min(y0, y1)
11234
+ const high = Math.max(y0, y1)
11235
+ const keys = []
11236
+ for (const rect of opts.rectsOf()) {
11237
+ if (rect.top < high && rect.bottom > low) keys.push(rect.key)
11238
+ }
11239
+ opts.onPick(keys)
11240
+ setBox({
11241
+ x: Math.min(state.startX, state.clientX) - drawRect.left,
11242
+ y: Math.min(state.startY, state.clientY) - drawRect.top,
11243
+ w: Math.abs(state.clientX - state.startX),
11244
+ h: Math.abs(state.clientY - state.startY),
11245
+ })
11246
+ /* Auto-scroll when the pointer rides an edge; keep the loop alive while
11247
+ it does so the selection follows the content streaming past. */
11248
+ const EDGE = 28
11249
+ let speed = 0
11250
+ if (state.clientY - hostRect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - hostRect.top)) / 3)
11251
+ else if (hostRect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (hostRect.bottom - state.clientY)) / 3)
11252
+ if (speed !== 0) {
11253
+ host.scrollTop += speed * 2
11254
+ cancelAnimationFrame(frameRef.current)
11255
+ frameRef.current = requestAnimationFrame(sync)
11256
+ }
11257
+ }
11258
+ const schedule = () => {
11259
+ cancelAnimationFrame(frameRef.current)
11260
+ frameRef.current = requestAnimationFrame(sync)
11261
+ }
11262
+ useEffect(() => () => cancelAnimationFrame(frameRef.current), [])
11263
+ const onPointerDown = (event) => {
11264
+ if (event.button !== 0) return
11265
+ const opts = optionsRef.current
11266
+ const host = opts.scrollRef.current
11267
+ if (host === null) return
11268
+ if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
11269
+ // Keep the browser from starting a text selection under the drag (the
11270
+ // history/tree lists dragged out blue text before); the real clicks
11271
+ // still fire — only the selection machinery is suppressed.
11272
+ event.preventDefault()
11273
+ stateRef.current = {
11274
+ startX: event.clientX,
11275
+ startY: event.clientY,
11276
+ scrollY0: host.scrollTop,
11277
+ clientX: event.clientX,
11278
+ clientY: event.clientY,
11279
+ active: false,
11280
+ }
11281
+ const onMove = (moveEvent) => {
11282
+ const state = stateRef.current
11283
+ if (state === null) return
11284
+ state.clientX = moveEvent.clientX
11285
+ state.clientY = moveEvent.clientY
11286
+ if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
11287
+ state.active = true
11288
+ swallowRef.current = true
11289
+ schedule()
11290
+ }
11291
+ const onUp = () => {
11292
+ window.removeEventListener('pointermove', onMove)
11293
+ window.removeEventListener('pointerup', onUp)
11294
+ cancelAnimationFrame(frameRef.current)
11295
+ const wasActive = stateRef.current !== null && stateRef.current.active === true
11296
+ stateRef.current = null
11297
+ setBox(null)
11298
+ // The drag's pointerup is followed by a synthetic click on whatever
11299
+ // row is under it — swallow exactly that one click.
11300
+ if (wasActive === true) setTimeout(() => { swallowRef.current = false }, 0)
11301
+ }
11302
+ window.addEventListener('pointermove', onMove)
11303
+ window.addEventListener('pointerup', onUp)
11304
+ }
11305
+ return { box: box, onPointerDown: onPointerDown, swallowRef: swallowRef }
11306
+ }
11307
+
11308
+ /* The marquee box itself: a viewport-anchored absolute div inside the
11309
+ draw host (invariant 13: never position:fixed inside the workbench's
11310
+ contain:layout). */
11311
+ function MarqueeBox(props) {
11312
+ if (props.box === null) return null
11313
+ return E('div', {
11314
+ className: 'dig-marquee',
11315
+ style: { left: props.box.x + 'px', top: props.box.y + 'px', width: props.box.w + 'px', height: props.box.h + 'px' },
11316
+ })
11317
+ }
11318
+
11146
11319
  /* First index whose cumulative offset exceeds `at` (binary search over the
11147
11320
  prefix-sum array; offsets.length === items.length + 1). */
11148
11321
  function windowStart(offsets, at) {
@@ -11249,11 +11422,44 @@ window.__ModuleLoader__.load({
11249
11422
  return 0
11250
11423
  }
11251
11424
 
11425
+ /* The author filter pins "me (name)" to the top (v0.19.3). The identity
11426
+ is resolved once per repo root and kept for the panel lifetime — git
11427
+ identities don't churn within a session — so re-opening the Git tab or
11428
+ hopping sessions never re-runs `git config`. */
11429
+ const gitMeCache = new Map()
11430
+
11252
11431
  function HistoryList(props) {
11253
11432
  const t = props.t
11254
11433
  const commits = props.commits
11255
11434
  const historyScrollRef = useRef(null)
11435
+ const [me, setMe] = useState(() => (props.base !== undefined && gitMeCache.has(props.base.repoRoot) === true ? gitMeCache.get(props.base.repoRoot) : null))
11436
+ useEffect(() => {
11437
+ const base = props.base
11438
+ if (base === undefined || base.repoRoot === undefined) return
11439
+ if (gitMeCache.has(base.repoRoot) === true) { setMe(gitMeCache.get(base.repoRoot)); return }
11440
+ let alive = true
11441
+ props.request('me', Object.assign({}, base)).then((data) => {
11442
+ const who = data !== null && typeof data === 'object' && typeof data.name === 'string' && data.name !== ''
11443
+ ? { name: data.name, email: typeof data.email === 'string' ? data.email : '' }
11444
+ : null
11445
+ gitMeCache.set(base.repoRoot, who)
11446
+ if (alive === true) setMe(who)
11447
+ }).catch((error) => { void error })
11448
+ return () => { alive = false }
11449
+ }, [props.base])
11256
11450
  const viewport = useViewportBox(historyScrollRef)
11451
+ /* v0.19.3: the commit log rubber-bands exactly like the changes list —
11452
+ same hook, same gestures. Fixed HISTORY_ROW_H rows make the rects a
11453
+ pure index map, so even a windowed (mostly unmounted) list drags
11454
+ perfectly; picks land in the SAME pickedHashes set the Shift/Ctrl
11455
+ clicks and the commit context menu already share. */
11456
+ const historyHostRef = useRef(null)
11457
+ const historyMarquee = useRubberBand({
11458
+ scrollRef: historyScrollRef,
11459
+ drawRef: historyHostRef,
11460
+ rectsOf: () => rows.map((row, index) => ({ key: row.commit.hash, top: index * HISTORY_ROW_H, bottom: (index + 1) * HISTORY_ROW_H })),
11461
+ onPick: (keys) => { if (props.onPicksChange !== undefined) props.onPicksChange(new Set(keys)) },
11462
+ })
11257
11463
  const pickAnchor = useRef(null)
11258
11464
  const [text, setText] = useState('')
11259
11465
  const [author, setAuthor] = useState('')
@@ -11309,6 +11515,92 @@ window.__ModuleLoader__.load({
11309
11515
  }, [commits, text, author, refName, since, order])
11310
11516
 
11311
11517
  const rows = useMemo(() => buildRows(filtered), [filtered])
11518
+
11519
+ /* Weibo-style infinite scroll (v0.19.3): descending lists auto-load when
11520
+ the user scrolls near the bottom edge, ascending ones near the top
11521
+ edge — wheel and scrollbar drags both surface as scroll events, no
11522
+ extra clicks. The button stays as the visible affordance. The guard
11523
+ keeps one request in flight; it resets when busy settles. */
11524
+ const infiniteGuardRef = useRef(false)
11525
+ const growAnchorRef = useRef(null)
11526
+ const mergeLocatedRef = useRef(false)
11527
+ const requestMore = () => {
11528
+ if (props.hasMore !== true || props.busy === true || infiniteGuardRef.current === true) return
11529
+ const host = historyScrollRef.current
11530
+ growAnchorRef.current = { length: rows.length, height: host === null ? 0 : host.scrollHeight }
11531
+ infiniteGuardRef.current = true
11532
+ props.onLoadMore()
11533
+ }
11534
+ /* The button itself is the trigger (weibo-style, v0.19.3): an
11535
+ IntersectionObserver on the load-more button fires wherever it sits —
11536
+ bottom while descending, top while ascending — covering wheel,
11537
+ scrollbar drags, first paint, and short pages alike. Re-created per
11538
+ page so a settled load re-evaluates: if the button is STILL visible
11539
+ (huge viewport / tiny page) the next page follows automatically. */
11540
+ const loadMoreRef = useRef(null)
11541
+ /* A scroll that lands on the button WHILE a guarded load is still in
11542
+ flight loses its one IO callback to the busy bail-out, and the
11543
+ observer is not re-created when that load finishes with the same
11544
+ page size (mount refresh). Re-check the button's geometry when busy
11545
+ settles: still within the rootMargin window → continue loading. */
11546
+ useEffect(() => {
11547
+ if (props.busy !== false) return
11548
+ infiniteGuardRef.current = false
11549
+ const node = loadMoreRef.current
11550
+ const root = historyScrollRef.current
11551
+ if (node === null || root === null || props.hasMore !== true) return
11552
+ const rect = node.getBoundingClientRect()
11553
+ const box = root.getBoundingClientRect()
11554
+ if (rect.bottom >= box.top - 240 && rect.top <= box.bottom + 240) requestMore()
11555
+ }, [props.busy])
11556
+ useEffect(() => {
11557
+ const node = loadMoreRef.current
11558
+ const root = historyScrollRef.current
11559
+ if (node === null || root === null || props.hasMore !== true) return
11560
+ let frame = 0
11561
+ const io = new IntersectionObserver((entries) => {
11562
+ if (entries.some((entry) => entry.isIntersecting === true) === false) return
11563
+ if (frame === 0) {
11564
+ frame = requestAnimationFrame(() => { frame = 0; requestMore() })
11565
+ }
11566
+ }, { root: root, rootMargin: '240px 0px' })
11567
+ io.observe(node)
11568
+ return () => { if (frame !== 0) cancelAnimationFrame(frame); io.disconnect() }
11569
+ }, [props.hasMore, order, rows.length])
11570
+ /* Ascending growth inserts rows above the viewport: re-anchor by the
11571
+ height delta so the rows the user was reading stay put. */
11572
+ useEffect(() => {
11573
+ const anchor = growAnchorRef.current
11574
+ if (anchor === null || rows.length <= anchor.length) return
11575
+ growAnchorRef.current = null
11576
+ const host = historyScrollRef.current
11577
+ if (order !== 'asc' || host === null) return
11578
+ host.scrollTop = host.scrollTop + (host.scrollHeight - anchor.height)
11579
+ }, [rows.length, order])
11580
+ /* v0.19.3 IDEA-style merge memory: when a browsed branch has a merge
11581
+ relationship with the PREVIOUSLY browsed one, its list opens scrolled
11582
+ to that merge commit ('merge-point' resolved it) — the "2 was merged
11583
+ here" moment is on screen immediately, while 1's newer commits stay
11584
+ one wheel-scroll away. Fires only on anchor CHANGE; null = no
11585
+ relationship (or first hop), leave the top alone. */
11586
+ useEffect(() => {
11587
+ const anchor = props.mergeAnchor
11588
+ if (anchor === undefined || anchor === null) { mergeLocatedRef.current = false; return }
11589
+ const index = rows.findIndex((row) => row.commit.hash === anchor)
11590
+ if (index < 0) {
11591
+ /* The merge point can sit below the first page on a long branch
11592
+ (real repos easily outrun LIMIT). Keep auto-loading — the same
11593
+ weibo path as the edge trigger — until it surfaces or the log
11594
+ is exhausted; one location per anchor, so later 'load more'
11595
+ never yanks the view back. */
11596
+ if (mergeLocatedRef.current === false && props.hasMore === true && props.busy !== true) requestMore()
11597
+ return
11598
+ }
11599
+ if (mergeLocatedRef.current === true) return
11600
+ mergeLocatedRef.current = true
11601
+ const host = historyScrollRef.current
11602
+ if (host !== null) host.scrollTop = Math.max(0, index * HISTORY_ROW_H - 8)
11603
+ }, [props.mergeAnchor, rows.length, props.busy])
11312
11604
  /* Multi-select gestures (issue #13): the rendered row order is what the
11313
11605
  Shift-range walks, and the range is computed over the FILTERED rows —
11314
11606
  the same list the eye sees. A plain click only SELECTS (single-replace,
@@ -11410,7 +11702,12 @@ window.__ModuleLoader__.load({
11410
11702
  }),
11411
11703
  E(FilterSelect, {
11412
11704
  label: t('filter.user'), value: author, hint: t('filter.user'),
11413
- options: [allOption].concat(authors.map((name) => ({ value: name, label: name }))),
11705
+ /* "Me" first (v0.19.3): resolved from git config and offered even
11706
+ when this branch has no commits of mine — selecting it over an
11707
+ empty list is the answer "I never committed here". */
11708
+ options: [allOption]
11709
+ .concat(me !== null && me.name !== '' ? [{ value: me.name, label: t('history.me') + ' (' + me.name + ')' }] : [])
11710
+ .concat(authors.map((name) => ({ value: name, label: name }))),
11414
11711
  onOpenMenu: (event, options) => openFilterMenu(event, options, author, setAuthor),
11415
11712
  }),
11416
11713
  E(FilterSelect, {
@@ -11447,12 +11744,27 @@ window.__ModuleLoader__.load({
11447
11744
  const winEnd = Math.min(rows.length, winStart + Math.ceil(viewport.viewH / HISTORY_ROW_H) + WINDOW_OVERSCAN * 2)
11448
11745
  const visibleRows = rows.slice(winStart, winEnd)
11449
11746
 
11450
- return E('div', { className: 'dig-history' }, filterBar,
11747
+ /* Ascending renders oldest-first, so load-more (older commits) lives
11748
+ at the TOP; descending keeps it at the bottom where it always was. */
11749
+ const loadMoreButton = props.hasMore === true
11750
+ ? E('button', { ref: loadMoreRef, type: 'button', className: 'dig-load-more', disabled: props.busy === true, onClick: requestMore }, t('history.loadMore'))
11751
+ : null
11752
+
11753
+ return E('div', { className: 'dig-history', ref: historyHostRef }, filterBar,
11451
11754
  E('div', {
11452
11755
  className: 'dig-history-scroll',
11453
11756
  ref: historyScrollRef,
11454
11757
  tabIndex: 0,
11455
11758
  onKeyDown: onListKeyDown,
11759
+ onPointerDown: historyMarquee.onPointerDown,
11760
+ onClickCapture: (event) => {
11761
+ if (historyMarquee.swallowRef.current === true) { event.preventDefault(); event.stopPropagation(); return }
11762
+ /* v0.19.4: blank click clears — see the changes/tree twins. */
11763
+ const target = event.target
11764
+ if (target instanceof Element && target.closest('.dig-commit,button,input,textarea,select,a') === null) {
11765
+ if (props.onPicksChange !== undefined) props.onPicksChange(new Set())
11766
+ }
11767
+ },
11456
11768
  onPointerDownCapture: (event) => {
11457
11769
  if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
11458
11770
  },
@@ -11471,6 +11783,7 @@ window.__ModuleLoader__.load({
11471
11783
  onMouseLeave: (event) => event.currentTarget.removeAttribute('data-hover-lane'),
11472
11784
  },
11473
11785
  rows.length === 0 ? E('div', { className: 'dig-empty' }, dirtyFilter ? t('filter.none') : t('history.empty')) : null,
11786
+ order === 'asc' ? loadMoreButton : null,
11474
11787
  winStart > 0 ? E('div', { key: 'pad-top', style: { height: String(winStart * HISTORY_ROW_H) + 'px' } }) : null,
11475
11788
  visibleRows.map((row) => E(CommitRow, {
11476
11789
  key: row.commit.hash,
@@ -11480,9 +11793,8 @@ window.__ModuleLoader__.load({
11480
11793
  onRowClick: handleRowClick, onOpenDetail: props.onSelect, onMenu: props.onMenu, openMenuAt: props.openMenuAt,
11481
11794
  })),
11482
11795
  winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
11483
- props.hasMore === true
11484
- ? E('button', { type: 'button', className: 'dig-load-more', disabled: props.busy === true, onClick: props.onLoadMore }, t('history.loadMore'))
11485
- : null))
11796
+ order === 'desc' ? loadMoreButton : null),
11797
+ E(MarqueeBox, { box: historyMarquee.box }))
11486
11798
  }
11487
11799
 
11488
11800
  function CommitDetail(props) {
@@ -11573,6 +11885,28 @@ window.__ModuleLoader__.load({
11573
11885
  /* The branch tree reports its picks up (onPicksChange) so the rail can act
11574
11886
  on the selection: "select main, press pull" must pull main, not HEAD. */
11575
11887
  const [branchPicks, setBranchPicks] = useState(() => new Set())
11888
+ /* v0.19.4: the three lists' selections are mutually exclusive — a pick in
11889
+ one list (row click or rubber band) clears the other two, and a blank
11890
+ click inside a list clears everything too. Each interaction bumps the
11891
+ OTHER lists' epoch counters; the effects below (and a clearSignal prop
11892
+ into BranchTree, whose picks live inside it) reset on change. Epochs
11893
+ are monotonic and start at 0, so the mount pass clears nothing. */
11894
+ const [pickEpochs, setPickEpochs] = useState(() => ({ changes: 0, history: 0, tree: 0 }))
11895
+ const clearPicksExcept = useCallback((owner) => {
11896
+ setPickEpochs((previous) => ({
11897
+ changes: owner === 'changes' ? previous.changes : previous.changes + 1,
11898
+ history: owner === 'history' ? previous.history : previous.history + 1,
11899
+ tree: owner === 'tree' ? previous.tree : previous.tree + 1,
11900
+ }))
11901
+ }, [])
11902
+ useEffect(() => { if (pickEpochs.changes > 0) setChangeSelection(new Set()) }, [pickEpochs.changes])
11903
+ useEffect(() => { if (pickEpochs.history > 0) setPickedCommits(new Set()) }, [pickEpochs.history])
11904
+ /* Forwarders: the raw setters wrapped so EVERY selection gesture reports
11905
+ an owner. The updates arrive either as a Set or as a functional
11906
+ updater (the memoised rows hold stale closures) — forwarded as-is. */
11907
+ const onBranchPicks = useCallback((update) => { setBranchPicks(update); clearPicksExcept('tree') }, [clearPicksExcept])
11908
+ const onChangeSelection = useCallback((update) => { setChangeSelection(update); clearPicksExcept('changes') }, [clearPicksExcept])
11909
+ const onCommitPicks = useCallback((update) => { setPickedCommits(update); clearPicksExcept('history') }, [clearPicksExcept])
11576
11910
  const [menu, setMenu] = useState(null)
11577
11911
  const [dialog, setDialog] = useState(null)
11578
11912
  const [tick, setTick] = useState(0)
@@ -11956,15 +12290,25 @@ window.__ModuleLoader__.load({
11956
12290
  return data
11957
12291
  }, [base])
11958
12292
 
12293
+ /* ---- branch browsing (v0.19.3, IDEA-style) ----
12294
+ Double-clicking a tree branch no longer checks it out: it walks THAT
12295
+ branch's log (browseRev = '' keeps the HEAD view). The LAST browsed
12296
+ branch is remembered, so 2-then-1 lands 1's list on the merge commit
12297
+ that carried 2 into 1 ('merge-point') — 1's newer commits stay one
12298
+ wheel-scroll away, nothing is truncated. */
12299
+ const [browseRev, setBrowseRev] = useState('')
12300
+ const [mergeAnchor, setMergeAnchor] = useState(null)
12301
+ const lastBrowseRef = useRef(null)
11959
12302
  const loadCommits = useCallback(async (skip, overridePath) => {
11960
12303
  const activePath = overridePath === undefined ? pathFilter : overridePath
11961
12304
  const payload = Object.assign({}, base, { skip: skip === undefined ? 0 : skip, limit: 120 })
12305
+ if (browseRev !== '') payload.rev = browseRev
11962
12306
  if (activePath !== undefined && activePath !== '') payload.path = activePath
11963
12307
  const data = await request('log', payload)
11964
12308
  setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
11965
12309
  setHasMore(data.hasMore)
11966
12310
  return data
11967
- }, [base, pathFilter])
12311
+ }, [base, pathFilter, browseRev])
11968
12312
 
11969
12313
  /* `fetchRemote` turns the refresh into what the rail button means: "see
11970
12314
  whether the remote has anything new to pull" — a `git fetch --prune`
@@ -12007,13 +12351,40 @@ window.__ModuleLoader__.load({
12007
12351
  (a paged fetch never reaches this path), replaced on every
12008
12352
  refresh — the auto-refresh included — so a session switch through
12009
12353
  the same workspace seeds fresh data, not the mount-time
12010
- snapshot. */
12011
- writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
12354
+ snapshot. A branch-browsing page (browseRev) must not evict the
12355
+ HEAD page: the next mount opens on HEAD and would otherwise be
12356
+ seeded with another branch's log. */
12357
+ if (browseRev === '') writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
12012
12358
  })
12013
12359
  } finally {
12014
12360
  if (withFetch === true) setRefreshSpinning(false)
12015
12361
  }
12016
- }, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base])
12362
+ }, [guard, loadSummary, loadBranches, loadCommits, repoRoot, remoteReady, base, browseRev])
12363
+
12364
+ /* Double-click a branch (or the HEAD row): browse its history. The merge
12365
+ anchor comes from the PREVIOUSLY browsed branch — 'merge-point' finds
12366
+ the newest merge that carried it into the one now opened, and the
12367
+ history list scrolls there once (null = no relationship, top). */
12368
+ const browseBranch = useCallback(async (entry) => {
12369
+ const name = String(entry.name)
12370
+ const previous = lastBrowseRef.current
12371
+ if (previous === name) return
12372
+ lastBrowseRef.current = name
12373
+ setMergeAnchor(null)
12374
+ setBrowseRev(name)
12375
+ if (previous !== null) {
12376
+ try {
12377
+ const data = await request('merge-point', Object.assign({}, base, { from: previous, to: name }))
12378
+ if (data !== null && typeof data === 'object' && typeof data.hash === 'string' && data.hash !== '') setMergeAnchor(data.hash)
12379
+ } catch (error) { void error }
12380
+ }
12381
+ }, [base, request])
12382
+ /* A repository switch forgets the browsing context entirely. */
12383
+ useEffect(() => {
12384
+ lastBrowseRef.current = null
12385
+ setBrowseRev('')
12386
+ setMergeAnchor(null)
12387
+ }, [repoRoot])
12017
12388
 
12018
12389
  useEffect(() => {
12019
12390
  if (repoRoot === null) return undefined
@@ -12960,9 +13331,12 @@ window.__ModuleLoader__.load({
12960
13331
  onLoadMore: () => { void guard(() => loadCommits(commits.length)) },
12961
13332
  /* ---- multi-select (issue #13) ---- */
12962
13333
  pickedHashes: pickedCommits,
12963
- onPicksChange: setPickedCommits,
12964
- onSelectAll: (hashes) => setPickedCommits(new Set(hashes)),
13334
+ onPicksChange: onCommitPicks,
13335
+ onSelectAll: (hashes) => onCommitPicks(new Set(hashes)),
12965
13336
  onKeyDown: onHistoryKeyDown,
13337
+ mergeAnchor: mergeAnchor,
13338
+ base: base,
13339
+ request: request,
12966
13340
  })
12967
13341
 
12968
13342
  const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
@@ -13231,7 +13605,7 @@ window.__ModuleLoader__.load({
13231
13605
  onChangeMenu: changeMenu,
13232
13606
  /* ---- multi-select (issue #13) ---- */
13233
13607
  selection: changeSelection,
13234
- onSelectionChange: setChangeSelection,
13608
+ onSelectionChange: onChangeSelection,
13235
13609
  anchorRef: changeAnchor,
13236
13610
  onBulkStage: bulkStageChanges,
13237
13611
  onBulkUnstage: bulkUnstageChanges,
@@ -13512,19 +13886,19 @@ window.__ModuleLoader__.load({
13512
13886
  title: t('toolbar.tree'),
13513
13887
  onClick: () => foldTree(!treeOpen),
13514
13888
  }, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
13515
- treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
13889
+ treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
13516
13890
  treeOpen ? gutter('tree', 'prev') : null,
13517
13891
  view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
13518
13892
  } else if (columns) {
13519
13893
  body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
13520
- treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
13894
+ treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
13521
13895
  treeOpen ? gutter('tree', 'prev') : null,
13522
13896
  E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane),
13523
13897
  gutter('changes', 'next'),
13524
13898
  E('div', { className: 'dig-pane dig-pane-changes', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane))
13525
13899
  } else {
13526
13900
  body = E('div', { className: 'dig-body dig-body-stack', ref: bodyRef },
13527
- treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: setBranchPicks })) : null,
13901
+ treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
13528
13902
  treeOpen ? gutter('tree', 'prev') : null,
13529
13903
  E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
13530
13904
  gutter('changes', 'prev'),
@@ -14049,7 +14423,10 @@ window.__ModuleLoader__.load({
14049
14423
  '.dig-picker-name{flex:none;font-weight:600}',
14050
14424
  '.dig-picker-kind{color:var(--dsw-alias-label-tertiary);flex:none}',
14051
14425
  '.dig-picker-path{color:var(--dsw-alias-label-tertiary);margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}',
14052
- '.dig-tree{display:flex;flex-direction:column;min-height:0;flex:1}',
14426
+ /* position:relative: the marquee box paints against this wrapper (the
14427
+ scroll container is offset by the search field — same rule as
14428
+ .dig-history above). */
14429
+ '.dig-tree{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
14053
14430
  '.dig-tree-scroll{flex:1;overflow:auto;padding-bottom:6px}',
14054
14431
  '.dig-section{display:flex;flex-direction:column}',
14055
14432
  '.dig-section-head{display:flex;align-items:center;gap:4px;width:100%;padding:3px 8px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-weight:600;cursor:pointer;text-align:left}',
@@ -14072,8 +14449,19 @@ window.__ModuleLoader__.load({
14072
14449
  '.dig-chevron-open{transform:rotate(0deg)}',
14073
14450
  '.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
14074
14451
  '.dig-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
14452
+ /* v0.19.3: the HEAD section row behaves like a branch row (click
14453
+ selects, double-click browses, right-click menus) — same hover cue. */
14454
+ '.dig-head-branch{cursor:pointer;transition:background .12s}',
14455
+ '.dig-head-branch:hover{background:var(--dsw-alias-interactive-bg-hover)}',
14075
14456
  '.dig-row-head{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
14076
- '.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
14457
+ /* v0.19.4: the flat alias background read as "barely selected" over the
14458
+ translucency/frost surfaces the any-background plugin produces. Mixing
14459
+ the token with the row's own TEXT color keeps the theme hues (light
14460
+ theme darkens, dark theme lightens — both gain contrast), and because
14461
+ the text color is near-opaque the mix carries an alpha FLOOR (~42%):
14462
+ the token's own translucency still dominates the look (58%), so the
14463
+ selection follows the plugin's transparency while staying readable. */
14464
+ '.dig-row-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)))) 58%,currentColor 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
14077
14465
  '.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
14078
14466
  '.dig-row-tag .dig-row-label{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:500}',
14079
14467
  '.dig-row-head .dig-row-label{font-weight:600}',
@@ -14090,11 +14478,15 @@ window.__ModuleLoader__.load({
14090
14478
  '.dig-badge.dig-badge-ahead,.dig-badge.dig-badge-behind{padding:0 6px}',
14091
14479
  '.dig-badge-muted{opacity:.8}',
14092
14480
  '.dig-badge-worktree{padding:0 3px;display:inline-flex;align-items:center;height:14px}',
14093
- '.dig-history{display:flex;flex-direction:column;min-height:0;flex:1}',
14481
+ /* position:relative: the marquee box (rubber-band selection) paints
14482
+ against this wrapper — the scroll container below is offset by the
14483
+ filter bar, and painting from the wrong rect was the v0.19.2 drift. */
14484
+ '.dig-history{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
14094
14485
  '.dig-history-scroll{flex:1;overflow:auto}',
14095
14486
  '.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 0;cursor:pointer;height:26px;overflow:hidden}',
14096
14487
  '.dig-commit:hover{background:var(--dsw-alias-interactive-bg-hover)}',
14097
- '.dig-commit-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
14488
+ /* Same floor + tint mix as .dig-row-selected (see it). */
14489
+ '.dig-commit-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)))) 58%,currentColor 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
14098
14490
  // A favorited branch renders its star SOLID (BranchRow adds this class
14099
14491
  // and flips the tone to warn) — the hollow green HEAD star read as
14100
14492
  // "not favorited" (v0.18.5).