dsh-ide-git 0.14.5 → 0.15.0

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
@@ -21,6 +21,7 @@ window.__ModuleLoader__.load({
21
21
  factory: (require) => {
22
22
  const React = require('react')
23
23
  const E = React.createElement
24
+ const Fragment = React.Fragment
24
25
  const useState = React.useState
25
26
  const useEffect = React.useEffect
26
27
  const useMemo = React.useMemo
@@ -186,6 +187,22 @@ window.__ModuleLoader__.load({
186
187
  'diff.copy': '复制',
187
188
  'diff.copied': '已复制',
188
189
  'toolbar.refresh': '刷新',
190
+ 'toolbar.more': '更多操作',
191
+ 'more.sortGit': 'Git 顺序',
192
+ 'more.sortPath': '按路径',
193
+ 'more.sortName': '按名称',
194
+ 'more.sortStatus': '按状态',
195
+ 'more.clone': '克隆...',
196
+ 'more.stashPop': '弹出最近贮藏',
197
+ 'more.deleteTag': '删除标签...',
198
+ 'more.gitOutput': '显示 Git 输出',
199
+ 'clone.title': '克隆仓库',
200
+ 'clone.url': '仓库 URL',
201
+ 'clone.dir': '目标目录(绝对路径)',
202
+ 'clone.run': '克隆',
203
+ 'prompt.deleteTag': '要删除的标签名',
204
+ 'gitLog.title': 'Git 输出',
205
+ 'gitLog.empty': '暂无 Git 运行记录',
189
206
  'toolbar.newBranch': '新建分支',
190
207
  'toolbar.fetch': '抓取(Fetch)',
191
208
  'toolbar.pull': '拉取(Pull)',
@@ -411,6 +428,22 @@ window.__ModuleLoader__.load({
411
428
  'diff.copy': 'Copy',
412
429
  'diff.copied': 'Copied',
413
430
  'toolbar.refresh': 'Refresh',
431
+ 'toolbar.more': 'More actions',
432
+ 'more.sortGit': 'Git order',
433
+ 'more.sortPath': 'By path',
434
+ 'more.sortName': 'By name',
435
+ 'more.sortStatus': 'By status',
436
+ 'more.clone': 'Clone...',
437
+ 'more.stashPop': 'Pop latest stash',
438
+ 'more.deleteTag': 'Delete tag...',
439
+ 'more.gitOutput': 'Show Git output',
440
+ 'clone.title': 'Clone repository',
441
+ 'clone.url': 'Repository URL',
442
+ 'clone.dir': 'Target directory (absolute path)',
443
+ 'clone.run': 'Clone',
444
+ 'prompt.deleteTag': 'Tag name to delete',
445
+ 'gitLog.title': 'Git output',
446
+ 'gitLog.empty': 'No git runs yet',
414
447
  'toolbar.newBranch': 'New branch',
415
448
  'toolbar.fetch': 'Fetch',
416
449
  'toolbar.pull': 'Pull',
@@ -644,6 +677,22 @@ window.__ModuleLoader__.load({
644
677
  'diff.copy': '複製',
645
678
  'diff.copied': '已複製',
646
679
  'toolbar.refresh': '重新整理',
680
+ 'toolbar.more': '更多操作',
681
+ 'more.sortGit': 'Git 順序',
682
+ 'more.sortPath': '按路徑',
683
+ 'more.sortName': '按名稱',
684
+ 'more.sortStatus': '按狀態',
685
+ 'more.clone': '複製存放庫...',
686
+ 'more.stashPop': '彈出最近貯藏',
687
+ 'more.deleteTag': '刪除標籤...',
688
+ 'more.gitOutput': '顯示 Git 輸出',
689
+ 'clone.title': '複製存放庫',
690
+ 'clone.url': '存放庫 URL',
691
+ 'clone.dir': '目標目錄(絕對路徑)',
692
+ 'clone.run': '複製',
693
+ 'prompt.deleteTag': '要刪除的標籤名',
694
+ 'gitLog.title': 'Git 輸出',
695
+ 'gitLog.empty': '暫無 Git 執行記錄',
647
696
  'toolbar.newBranch': '新建分支',
648
697
  'toolbar.fetch': '抓取',
649
698
  'toolbar.pull': '拉取',
@@ -869,6 +918,22 @@ window.__ModuleLoader__.load({
869
918
  'diff.copy': '複製',
870
919
  'diff.copied': '已複製',
871
920
  'toolbar.refresh': '重新整理',
921
+ 'toolbar.more': '更多操作',
922
+ 'more.sortGit': 'Git 順序',
923
+ 'more.sortPath': '按路徑',
924
+ 'more.sortName': '按名稱',
925
+ 'more.sortStatus': '按狀態',
926
+ 'more.clone': '複製存放庫...',
927
+ 'more.stashPop': '取出最近擱置',
928
+ 'more.deleteTag': '刪除標籤...',
929
+ 'more.gitOutput': '顯示 Git 輸出',
930
+ 'clone.title': '複製存放庫',
931
+ 'clone.url': '存放庫 URL',
932
+ 'clone.dir': '目標目錄(絕對路徑)',
933
+ 'clone.run': '複製',
934
+ 'prompt.deleteTag': '要刪除的標籤名',
935
+ 'gitLog.title': 'Git 輸出',
936
+ 'gitLog.empty': '暫無 Git 執行記錄',
872
937
  'toolbar.newBranch': '新建分支',
873
938
  'toolbar.fetch': '抓取',
874
939
  'toolbar.pull': '拉取',
@@ -1094,6 +1159,22 @@ window.__ModuleLoader__.load({
1094
1159
  'diff.copy': '複製',
1095
1160
  'diff.copied': '已複製',
1096
1161
  'toolbar.refresh': '重新整理',
1162
+ 'toolbar.more': '更多操作',
1163
+ 'more.sortGit': 'Git 順序',
1164
+ 'more.sortPath': '按路徑',
1165
+ 'more.sortName': '按名稱',
1166
+ 'more.sortStatus': '按狀態',
1167
+ 'more.clone': '複製存放庫...',
1168
+ 'more.stashPop': '彈出最近貯藏',
1169
+ 'more.deleteTag': '刪除標籤...',
1170
+ 'more.gitOutput': '顯示 Git 輸出',
1171
+ 'clone.title': '複製存放庫',
1172
+ 'clone.url': '存放庫 URL',
1173
+ 'clone.dir': '目標目錄(絕對路徑)',
1174
+ 'clone.run': '複製',
1175
+ 'prompt.deleteTag': '要刪除的標籤名',
1176
+ 'gitLog.title': 'Git 輸出',
1177
+ 'gitLog.empty': '暫無 Git 執行記錄',
1097
1178
  'toolbar.newBranch': '新建分支',
1098
1179
  'toolbar.fetch': '抓取',
1099
1180
  'toolbar.pull': '拉取',
@@ -1319,6 +1400,22 @@ window.__ModuleLoader__.load({
1319
1400
  'diff.copy': 'コピー',
1320
1401
  'diff.copied': 'コピーしました',
1321
1402
  'toolbar.refresh': '更新',
1403
+ 'toolbar.more': 'その他の操作',
1404
+ 'more.sortGit': 'Git の順序',
1405
+ 'more.sortPath': 'パス順',
1406
+ 'more.sortName': '名前順',
1407
+ 'more.sortStatus': '状態順',
1408
+ 'more.clone': 'リポジトリを複製...',
1409
+ 'more.stashPop': '最新のスタッシュをポップ',
1410
+ 'more.deleteTag': 'タグを削除...',
1411
+ 'more.gitOutput': 'Git 出力を表示',
1412
+ 'clone.title': 'リポジトリを複製',
1413
+ 'clone.url': 'リポジトリ URL',
1414
+ 'clone.dir': '複製先ディレクトリ(絶対パス)',
1415
+ 'clone.run': '複製',
1416
+ 'prompt.deleteTag': '削除するタグ名',
1417
+ 'gitLog.title': 'Git 出力',
1418
+ 'gitLog.empty': 'Git 実行履歴はまだありません',
1322
1419
  'toolbar.newBranch': '新しいブランチ',
1323
1420
  'toolbar.fetch': 'フェッチ',
1324
1421
  'toolbar.pull': 'プル',
@@ -1544,6 +1641,22 @@ window.__ModuleLoader__.load({
1544
1641
  'diff.copy': '복사',
1545
1642
  'diff.copied': '복사됨',
1546
1643
  'toolbar.refresh': '새로 고침',
1644
+ 'toolbar.more': '더 보기',
1645
+ 'more.sortGit': 'Git 순서',
1646
+ 'more.sortPath': '경로순',
1647
+ 'more.sortName': '이름순',
1648
+ 'more.sortStatus': '상태순',
1649
+ 'more.clone': '복제...',
1650
+ 'more.stashPop': '최신 스태시 팝',
1651
+ 'more.deleteTag': '태그 삭제...',
1652
+ 'more.gitOutput': 'Git 출력 표시',
1653
+ 'clone.title': '리포지토리 복제',
1654
+ 'clone.url': '리포지토리 URL',
1655
+ 'clone.dir': '대상 디렉터리(절대 경로)',
1656
+ 'clone.run': '복제',
1657
+ 'prompt.deleteTag': '삭제할 태그 이름',
1658
+ 'gitLog.title': 'Git 출력',
1659
+ 'gitLog.empty': '아직 Git 실행 기록이 없습니다',
1547
1660
  'toolbar.newBranch': '새 브랜치',
1548
1661
  'toolbar.fetch': '페치',
1549
1662
  'toolbar.pull': '풀',
@@ -1769,6 +1882,22 @@ window.__ModuleLoader__.load({
1769
1882
  'diff.copy': 'Kopieren',
1770
1883
  'diff.copied': 'Kopiert',
1771
1884
  'toolbar.refresh': 'Aktualisieren',
1885
+ 'toolbar.more': 'Weitere Aktionen',
1886
+ 'more.sortGit': 'Git-Reihenfolge',
1887
+ 'more.sortPath': 'Nach Pfad',
1888
+ 'more.sortName': 'Nach Name',
1889
+ 'more.sortStatus': 'Nach Status',
1890
+ 'more.clone': 'Repository klonen...',
1891
+ 'more.stashPop': 'Letzten Stash poppen',
1892
+ 'more.deleteTag': 'Tag löschen...',
1893
+ 'more.gitOutput': 'Git-Ausgabe anzeigen',
1894
+ 'clone.title': 'Repository klonen',
1895
+ 'clone.url': 'Repository-URL',
1896
+ 'clone.dir': 'Zielverzeichnis (absoluter Pfad)',
1897
+ 'clone.run': 'Klonen',
1898
+ 'prompt.deleteTag': 'Zu löschender Tag-Name',
1899
+ 'gitLog.title': 'Git-Ausgabe',
1900
+ 'gitLog.empty': 'Noch keine Git-Ausführungen',
1772
1901
  'toolbar.newBranch': 'Neuer Branch',
1773
1902
  'toolbar.fetch': 'Fetch',
1774
1903
  'toolbar.pull': 'Pull',
@@ -1994,6 +2123,22 @@ window.__ModuleLoader__.load({
1994
2123
  'diff.copy': 'Copier',
1995
2124
  'diff.copied': 'Copié',
1996
2125
  'toolbar.refresh': 'Rafraîchir',
2126
+ 'toolbar.more': 'Actions supplémentaires',
2127
+ 'more.sortGit': 'Ordre Git',
2128
+ 'more.sortPath': 'Par chemin',
2129
+ 'more.sortName': 'Par nom',
2130
+ 'more.sortStatus': 'Par statut',
2131
+ 'more.clone': 'Cloner...',
2132
+ 'more.stashPop': 'Pop du dernier stash',
2133
+ 'more.deleteTag': 'Supprimer l\'étiquette...',
2134
+ 'more.gitOutput': 'Afficher la sortie Git',
2135
+ 'clone.title': 'Cloner le dépôt',
2136
+ 'clone.url': 'URL du dépôt',
2137
+ 'clone.dir': 'Répertoire cible (chemin absolu)',
2138
+ 'clone.run': 'Cloner',
2139
+ 'prompt.deleteTag': 'Nom de l\'étiquette à supprimer',
2140
+ 'gitLog.title': 'Sortie Git',
2141
+ 'gitLog.empty': 'Aucune exécution Git pour l\'instant',
1997
2142
  'toolbar.newBranch': 'Nouvelle branche',
1998
2143
  'toolbar.fetch': 'Récupérer',
1999
2144
  'toolbar.pull': 'Tirer',
@@ -2219,6 +2364,22 @@ window.__ModuleLoader__.load({
2219
2364
  'diff.copy': 'Копировать',
2220
2365
  'diff.copied': 'Скопировано',
2221
2366
  'toolbar.refresh': 'Обновить',
2367
+ 'toolbar.more': 'Дополнительные действия',
2368
+ 'more.sortGit': 'Порядок Git',
2369
+ 'more.sortPath': 'По пути',
2370
+ 'more.sortName': 'По имени',
2371
+ 'more.sortStatus': 'По состоянию',
2372
+ 'more.clone': 'Клонировать...',
2373
+ 'more.stashPop': 'Pop последнего stash',
2374
+ 'more.deleteTag': 'Удалить тег...',
2375
+ 'more.gitOutput': 'Показать вывод Git',
2376
+ 'clone.title': 'Клонировать репозиторий',
2377
+ 'clone.url': 'URL репозитория',
2378
+ 'clone.dir': 'Целевой каталог (абсолютный путь)',
2379
+ 'clone.run': 'Клонировать',
2380
+ 'prompt.deleteTag': 'Имя тега для удаления',
2381
+ 'gitLog.title': 'Вывод Git',
2382
+ 'gitLog.empty': 'Запусков Git пока нет',
2222
2383
  'toolbar.newBranch': 'Новая ветка',
2223
2384
  'toolbar.fetch': 'Получить',
2224
2385
  'toolbar.pull': 'Забрать',
@@ -2444,6 +2605,22 @@ window.__ModuleLoader__.load({
2444
2605
  'diff.copy': 'Copiar',
2445
2606
  'diff.copied': 'Copiado',
2446
2607
  'toolbar.refresh': 'Atualizar',
2608
+ 'toolbar.more': 'Mais ações',
2609
+ 'more.sortGit': 'Ordem do Git',
2610
+ 'more.sortPath': 'Por caminho',
2611
+ 'more.sortName': 'Por nome',
2612
+ 'more.sortStatus': 'Por status',
2613
+ 'more.clone': 'Clonar...',
2614
+ 'more.stashPop': 'Pop do último stash',
2615
+ 'more.deleteTag': 'Excluir tag...',
2616
+ 'more.gitOutput': 'Mostrar saída do Git',
2617
+ 'clone.title': 'Clonar repositório',
2618
+ 'clone.url': 'URL do repositório',
2619
+ 'clone.dir': 'Diretório de destino (caminho absoluto)',
2620
+ 'clone.run': 'Clonar',
2621
+ 'prompt.deleteTag': 'Nome da tag a excluir',
2622
+ 'gitLog.title': 'Saída do Git',
2623
+ 'gitLog.empty': 'Ainda não há execuções do Git',
2447
2624
  'toolbar.newBranch': 'Nova branch',
2448
2625
  'toolbar.fetch': 'Buscar',
2449
2626
  'toolbar.pull': 'Puxar',
@@ -2669,6 +2846,22 @@ window.__ModuleLoader__.load({
2669
2846
  'diff.copy': 'Copia',
2670
2847
  'diff.copied': 'Copiato',
2671
2848
  'toolbar.refresh': 'Aggiorna',
2849
+ 'toolbar.more': 'Altre azioni',
2850
+ 'more.sortGit': 'Ordine Git',
2851
+ 'more.sortPath': 'Per percorso',
2852
+ 'more.sortName': 'Per nome',
2853
+ 'more.sortStatus': 'Per stato',
2854
+ 'more.clone': 'Clona...',
2855
+ 'more.stashPop': 'Pop dell\'ultimo stash',
2856
+ 'more.deleteTag': 'Elimina tag...',
2857
+ 'more.gitOutput': 'Mostra output Git',
2858
+ 'clone.title': 'Clona repository',
2859
+ 'clone.url': 'URL del repository',
2860
+ 'clone.dir': 'Directory di destinazione (percorso assoluto)',
2861
+ 'clone.run': 'Clona',
2862
+ 'prompt.deleteTag': 'Nome del tag da eliminare',
2863
+ 'gitLog.title': 'Output Git',
2864
+ 'gitLog.empty': 'Nessuna esecuzione Git registrata',
2672
2865
  'toolbar.newBranch': 'Nuovo branch',
2673
2866
  'toolbar.fetch': 'Fetch',
2674
2867
  'toolbar.pull': 'Pull',
@@ -2894,6 +3087,22 @@ window.__ModuleLoader__.load({
2894
3087
  'diff.copy': 'Kopiëren',
2895
3088
  'diff.copied': 'Gekopieerd',
2896
3089
  'toolbar.refresh': 'Vernieuwen',
3090
+ 'toolbar.more': 'Meer acties',
3091
+ 'more.sortGit': 'Git-volgorde',
3092
+ 'more.sortPath': 'Op pad',
3093
+ 'more.sortName': 'Op naam',
3094
+ 'more.sortStatus': 'Op status',
3095
+ 'more.clone': 'Klonen...',
3096
+ 'more.stashPop': 'Pop laatste stash',
3097
+ 'more.deleteTag': 'Tag verwijderen...',
3098
+ 'more.gitOutput': 'Git-uitvoer tonen',
3099
+ 'clone.title': 'Repository klonen',
3100
+ 'clone.url': 'Repository-URL',
3101
+ 'clone.dir': 'Doelmap (absoluut pad)',
3102
+ 'clone.run': 'Klonen',
3103
+ 'prompt.deleteTag': 'Naam van de te verwijderen tag',
3104
+ 'gitLog.title': 'Git-uitvoer',
3105
+ 'gitLog.empty': 'Nog geen Git-uitvoeringen',
2897
3106
  'toolbar.newBranch': 'Nieuwe branch',
2898
3107
  'toolbar.fetch': 'Fetch',
2899
3108
  'toolbar.pull': 'Pull',
@@ -3119,6 +3328,22 @@ window.__ModuleLoader__.load({
3119
3328
  'diff.copy': 'Kopiuj',
3120
3329
  'diff.copied': 'Skopiowano',
3121
3330
  'toolbar.refresh': 'Odśwież',
3331
+ 'toolbar.more': 'Więcej akcji',
3332
+ 'more.sortGit': 'Kolejność Gita',
3333
+ 'more.sortPath': 'Według ścieżki',
3334
+ 'more.sortName': 'Według nazwy',
3335
+ 'more.sortStatus': 'Według stanu',
3336
+ 'more.clone': 'Klonuj...',
3337
+ 'more.stashPop': 'Pop ostatniego stash',
3338
+ 'more.deleteTag': 'Usuń tag...',
3339
+ 'more.gitOutput': 'Pokaż dane wyjściowe Git',
3340
+ 'clone.title': 'Klonuj repozytorium',
3341
+ 'clone.url': 'URL repozytorium',
3342
+ 'clone.dir': 'Katalog docelowy (ścieżka bezwzględna)',
3343
+ 'clone.run': 'Klonuj',
3344
+ 'prompt.deleteTag': 'Nazwa tagu do usunięcia',
3345
+ 'gitLog.title': 'Dane wyjściowe Git',
3346
+ 'gitLog.empty': 'Brak zapisanych wykonań Git',
3122
3347
  'toolbar.newBranch': 'Nowa gałąź',
3123
3348
  'toolbar.fetch': 'Pobierz',
3124
3349
  'toolbar.pull': 'Ściągnij',
@@ -3344,6 +3569,22 @@ window.__ModuleLoader__.load({
3344
3569
  'diff.copy': 'Kopiera',
3345
3570
  'diff.copied': 'Kopierat',
3346
3571
  'toolbar.refresh': 'Uppdatera',
3572
+ 'toolbar.more': 'Fler åtgärder',
3573
+ 'more.sortGit': 'Git-ordning',
3574
+ 'more.sortPath': 'Efter sökväg',
3575
+ 'more.sortName': 'Efter namn',
3576
+ 'more.sortStatus': 'Efter status',
3577
+ 'more.clone': 'Klona...',
3578
+ 'more.stashPop': 'Pop:a senaste stash',
3579
+ 'more.deleteTag': 'Ta bort tagg...',
3580
+ 'more.gitOutput': 'Visa Git-utdata',
3581
+ 'clone.title': 'Klona lagringsplats',
3582
+ 'clone.url': 'URL till lagringsplats',
3583
+ 'clone.dir': 'Målkatalog (absolut sökväg)',
3584
+ 'clone.run': 'Klona',
3585
+ 'prompt.deleteTag': 'Tagnamn som ska tas bort',
3586
+ 'gitLog.title': 'Git-utdata',
3587
+ 'gitLog.empty': 'Inga Git-körningar ännu',
3347
3588
  'toolbar.newBranch': 'Ny gren',
3348
3589
  'toolbar.fetch': 'Fetch',
3349
3590
  'toolbar.pull': 'Pull',
@@ -3569,6 +3810,22 @@ window.__ModuleLoader__.load({
3569
3810
  'diff.copy': 'Kopyala',
3570
3811
  'diff.copied': 'Kopyalandı',
3571
3812
  'toolbar.refresh': 'Yenile',
3813
+ 'toolbar.more': 'Diğer eylemler',
3814
+ 'more.sortGit': 'Git sırası',
3815
+ 'more.sortPath': 'Yola göre',
3816
+ 'more.sortName': 'Ada göre',
3817
+ 'more.sortStatus': 'Duruma göre',
3818
+ 'more.clone': 'Klonla...',
3819
+ 'more.stashPop': 'Son stash\'i aç',
3820
+ 'more.deleteTag': 'Etiket sil...',
3821
+ 'more.gitOutput': 'Git çıktısını göster',
3822
+ 'clone.title': 'Depoyu klonla',
3823
+ 'clone.url': 'Depo URL\'si',
3824
+ 'clone.dir': 'Hedef dizin (mutlak yol)',
3825
+ 'clone.run': 'Klonla',
3826
+ 'prompt.deleteTag': 'Silinecek etiket adı',
3827
+ 'gitLog.title': 'Git çıktısı',
3828
+ 'gitLog.empty': 'Henüz Git çalıştırması yok',
3572
3829
  'toolbar.newBranch': 'Yeni dal',
3573
3830
  'toolbar.fetch': 'Fetch',
3574
3831
  'toolbar.pull': 'Pull',
@@ -3794,6 +4051,22 @@ window.__ModuleLoader__.load({
3794
4051
  'diff.copy': 'Salin',
3795
4052
  'diff.copied': 'Tersalin',
3796
4053
  'toolbar.refresh': 'Segarkan',
4054
+ 'toolbar.more': 'Tindakan lainnya',
4055
+ 'more.sortGit': 'Urutan Git',
4056
+ 'more.sortPath': 'Menurut jalur',
4057
+ 'more.sortName': 'Menurut nama',
4058
+ 'more.sortStatus': 'Menurut status',
4059
+ 'more.clone': 'Kloning...',
4060
+ 'more.stashPop': 'Pop stash terbaru',
4061
+ 'more.deleteTag': 'Hapus tag...',
4062
+ 'more.gitOutput': 'Tampilkan keluaran Git',
4063
+ 'clone.title': 'Kloning repositori',
4064
+ 'clone.url': 'URL repositori',
4065
+ 'clone.dir': 'Direktori target (jalur absolut)',
4066
+ 'clone.run': 'Kloning',
4067
+ 'prompt.deleteTag': 'Nama tag yang akan dihapus',
4068
+ 'gitLog.title': 'Keluaran Git',
4069
+ 'gitLog.empty': 'Belum ada eksekusi Git',
3797
4070
  'toolbar.newBranch': 'Branch baru',
3798
4071
  'toolbar.fetch': 'Fetch',
3799
4072
  'toolbar.pull': 'Pull',
@@ -4019,6 +4292,22 @@ window.__ModuleLoader__.load({
4019
4292
  'diff.copy': 'Sao chép',
4020
4293
  'diff.copied': 'Đã sao chép',
4021
4294
  'toolbar.refresh': 'Làm mới',
4295
+ 'toolbar.more': 'Thao tác khác',
4296
+ 'more.sortGit': 'Thứ tự Git',
4297
+ 'more.sortPath': 'Theo đường dẫn',
4298
+ 'more.sortName': 'Theo tên',
4299
+ 'more.sortStatus': 'Theo trạng thái',
4300
+ 'more.clone': 'Clone...',
4301
+ 'more.stashPop': 'Pop stash gần nhất',
4302
+ 'more.deleteTag': 'Xóa thẻ...',
4303
+ 'more.gitOutput': 'Hiện đầu ra Git',
4304
+ 'clone.title': 'Clone kho',
4305
+ 'clone.url': 'URL kho',
4306
+ 'clone.dir': 'Thư mục đích (đường dẫn tuyệt đối)',
4307
+ 'clone.run': 'Clone',
4308
+ 'prompt.deleteTag': 'Tên thẻ cần xóa',
4309
+ 'gitLog.title': 'Đầu ra Git',
4310
+ 'gitLog.empty': 'Chưa có lần chạy Git nào',
4022
4311
  'toolbar.newBranch': 'Nhánh mới',
4023
4312
  'toolbar.fetch': 'Fetch',
4024
4313
  'toolbar.pull': 'Pull',
@@ -4244,6 +4533,22 @@ window.__ModuleLoader__.load({
4244
4533
  'diff.copy': 'نسخ',
4245
4534
  'diff.copied': 'تم النسخ',
4246
4535
  'toolbar.refresh': 'تحديث',
4536
+ 'toolbar.more': 'مزيد من الإجراءات',
4537
+ 'more.sortGit': 'ترتيب Git',
4538
+ 'more.sortPath': 'حسب المسار',
4539
+ 'more.sortName': 'حسب الاسم',
4540
+ 'more.sortStatus': 'حسب الحالة',
4541
+ 'more.clone': 'استنساخ...',
4542
+ 'more.stashPop': 'تفريغ آخر stash',
4543
+ 'more.deleteTag': 'حذف الوسم...',
4544
+ 'more.gitOutput': 'إظهار مخرجات Git',
4545
+ 'clone.title': 'استنساخ المستودع',
4546
+ 'clone.url': 'رابط المستودع',
4547
+ 'clone.dir': 'المجلد الهدف (مسار مطلق)',
4548
+ 'clone.run': 'استنساخ',
4549
+ 'prompt.deleteTag': 'اسم الوسم المطلوب حذفه',
4550
+ 'gitLog.title': 'مخرجات Git',
4551
+ 'gitLog.empty': 'لا توجد عمليات Git بعد',
4247
4552
  'toolbar.newBranch': 'فرع جديد',
4248
4553
  'toolbar.fetch': 'جلب',
4249
4554
  'toolbar.pull': 'سحب',
@@ -4469,6 +4774,22 @@ window.__ModuleLoader__.load({
4469
4774
  'diff.copy': 'कॉपी करें',
4470
4775
  'diff.copied': 'कॉपी हो गया',
4471
4776
  'toolbar.refresh': 'ताज़ा करें',
4777
+ 'toolbar.more': 'अधिक क्रियाएँ',
4778
+ 'more.sortGit': 'Git क्रम',
4779
+ 'more.sortPath': 'पथ से',
4780
+ 'more.sortName': 'नाम से',
4781
+ 'more.sortStatus': 'स्थिति से',
4782
+ 'more.clone': 'क्लोन करें...',
4783
+ 'more.stashPop': 'नवीनतम स्टैश पॉप करें',
4784
+ 'more.deleteTag': 'टैग हटाएँ...',
4785
+ 'more.gitOutput': 'Git आउटपुट दिखाएँ',
4786
+ 'clone.title': 'रिपॉज़िटरी क्लोन करें',
4787
+ 'clone.url': 'रिपॉज़िटरी URL',
4788
+ 'clone.dir': 'लक्ष्य डायरेक्टरी (पूर्ण पथ)',
4789
+ 'clone.run': 'क्लोन करें',
4790
+ 'prompt.deleteTag': 'हटाने के लिए टैग नाम',
4791
+ 'gitLog.title': 'Git आउटपुट',
4792
+ 'gitLog.empty': 'अभी तक कोई Git रन नहीं',
4472
4793
  'toolbar.newBranch': 'नई शाखा',
4473
4794
  'toolbar.fetch': 'फ़ेच',
4474
4795
  'toolbar.pull': 'पुल',
@@ -4694,6 +5015,22 @@ window.__ModuleLoader__.load({
4694
5015
  'diff.copy': 'คัดลอก',
4695
5016
  'diff.copied': 'คัดลอกแล้ว',
4696
5017
  'toolbar.refresh': 'รีเฟรช',
5018
+ 'toolbar.more': 'การกระทำเพิ่มเติม',
5019
+ 'more.sortGit': 'ลำดับ Git',
5020
+ 'more.sortPath': 'ตามเส้นทาง',
5021
+ 'more.sortName': 'ตามชื่อ',
5022
+ 'more.sortStatus': 'ตามสถานะ',
5023
+ 'more.clone': 'โคลน...',
5024
+ 'more.stashPop': 'ป๊อป stash ล่าสุด',
5025
+ 'more.deleteTag': 'ลบแท็ก...',
5026
+ 'more.gitOutput': 'แสดงเอาต์พุต Git',
5027
+ 'clone.title': 'โคลนรีโพซิทอรี',
5028
+ 'clone.url': 'URL ของรีโพซิทอรี',
5029
+ 'clone.dir': 'ไดเรกทอรีเป้าหมาย (พาธสัมบูรณ์)',
5030
+ 'clone.run': 'โคลน',
5031
+ 'prompt.deleteTag': 'ชื่อแท็กที่จะลบ',
5032
+ 'gitLog.title': 'เอาต์พุต Git',
5033
+ 'gitLog.empty': 'ยังไม่มีการรัน Git',
4697
5034
  'toolbar.newBranch': 'แบรนช์ใหม่',
4698
5035
  'toolbar.fetch': 'Fetch',
4699
5036
  'toolbar.pull': 'Pull',
@@ -5247,6 +5584,10 @@ window.__ModuleLoader__.load({
5247
5584
  { id: 'fetch', icon: 'fetch', key: 'toolbar.fetch', tone: 'cyan' },
5248
5585
  { id: 'pull', icon: 'pull', key: 'toolbar.pull', tone: 'accent' },
5249
5586
  { id: 'push', icon: 'push', key: 'toolbar.push', tone: 'success' },
5587
+ /* VS Code-style "..." menu: view mode, sort, and every repository action
5588
+ that has no rail button of its own. A rail action (not a chrome
5589
+ button) so the user can hide/move it like any other, per invariant 14. */
5590
+ { id: 'more', icon: 'more', key: 'toolbar.more', tone: 'secondary' },
5250
5591
  ]
5251
5592
  const RAIL_IDS = RAIL_SPECS.map((spec) => spec.id)
5252
5593
  /* Actions that arrange the view rather than touch the repository, so a running
@@ -5933,7 +6274,10 @@ window.__ModuleLoader__.load({
5933
6274
  return segments
5934
6275
  }
5935
6276
 
5936
- function GraphCell(props) {
6277
+ /* Memoised: rows come from a useMemo, so a row object is referentially
6278
+ stable across re-renders unless the commit list actually changed — the
6279
+ 26px graph cells then skip re-rendering on every filter keystroke. */
6280
+ const GraphCell = memo(function GraphCell(props) {
5937
6281
  const row = props.row
5938
6282
  const height = props.height
5939
6283
  const width = Math.min(Math.max(1, row.width), GRAPH_MAX_LANES) * LANE_WIDTH + 8
@@ -5946,6 +6290,7 @@ window.__ModuleLoader__.load({
5946
6290
  for (const segment of laneSegments(row, height)) {
5947
6291
  children.push(E('line', {
5948
6292
  key: 'lane-' + segment.lane,
6293
+ 'data-lane': String(segment.lane),
5949
6294
  x1: x(segment.lane), y1: segment.y1, x2: x(segment.lane), y2: segment.y2,
5950
6295
  stroke: color(segment.lane), strokeWidth: 2, strokeLinecap: 'round',
5951
6296
  }))
@@ -5954,6 +6299,7 @@ window.__ModuleLoader__.load({
5954
6299
  if (parentLane === row.lane) continue
5955
6300
  children.push(E('path', {
5956
6301
  key: 'edge-' + parentLane,
6302
+ 'data-lane': String(parentLane),
5957
6303
  d: 'M ' + x(row.lane) + ' ' + middle + ' C ' + x(row.lane) + ' ' + height + ', ' + x(parentLane) + ' ' + middle + ', ' + x(parentLane) + ' ' + height,
5958
6304
  fill: 'none',
5959
6305
  stroke: color(parentLane),
@@ -5963,10 +6309,10 @@ window.__ModuleLoader__.load({
5963
6309
  }
5964
6310
  const isHead = Array.isArray(row.commit.refs) && row.commit.refs.some((ref) => String(ref).indexOf('HEAD') >= 0)
5965
6311
  children.push(isHead
5966
- ? E('circle', { key: 'dot', cx: x(row.lane), cy: middle, r: 3.8, fill: 'none', stroke: color(row.lane), strokeWidth: 2 })
5967
- : E('circle', { key: 'dot', cx: x(row.lane), cy: middle, r: 3.6, fill: color(row.lane) }))
6312
+ ? E('circle', { key: 'dot', 'data-lane': String(row.lane), cx: x(row.lane), cy: middle, r: 3.8, fill: 'none', stroke: color(row.lane), strokeWidth: 2 })
6313
+ : E('circle', { key: 'dot', 'data-lane': String(row.lane), cx: x(row.lane), cy: middle, r: 3.6, fill: color(row.lane) }))
5968
6314
  return E('svg', { className: 'dig-graph', width: width, height: height, viewBox: '0 0 ' + width + ' ' + height }, children)
5969
- }
6315
+ }, (prev, next) => prev.row === next.row && prev.height === next.height)
5970
6316
 
5971
6317
  /* ============================== composer send ============================== */
5972
6318
  /* "Send to chat": drop a reference chip / short text into the CURRENT
@@ -7596,6 +7942,68 @@ window.__ModuleLoader__.load({
7596
7942
  E('button', { type: 'button', className: 'dig-btn dig-btn-primary', onClick: submit }, props.okLabel))))
7597
7943
  }
7598
7944
 
7945
+ /* VS Code's "Clone...": a URL plus an absolute target path. Two inputs in
7946
+ one dialog — chaining two single prompts would forget the URL by the
7947
+ time the directory is typed. */
7948
+ function CloneDialog(props) {
7949
+ const [url, setUrl] = useState('')
7950
+ const [dir, setDir] = useState('')
7951
+ const ref = useRef(null)
7952
+ useEffect(() => { if (ref.current !== null) ref.current.focus() }, [])
7953
+ const ready = url.trim() !== '' && dir.trim() !== ''
7954
+ const submit = () => { if (ready === true) props.onSubmit(url.trim(), dir.trim()) }
7955
+ const onKey = (event) => {
7956
+ if (event.key === 'Enter') { event.preventDefault(); submit() }
7957
+ if (event.key === 'Escape') { event.preventDefault(); props.onCancel() }
7958
+ }
7959
+ return E('div', { className: 'dig-overlay' },
7960
+ E('div', { className: 'dig-dialog' },
7961
+ E('div', { className: 'dig-dialog-title' }, props.title),
7962
+ E('input', {
7963
+ ref: ref, className: 'dig-input', value: url, spellCheck: false,
7964
+ placeholder: props.urlPlaceholder === undefined ? '' : props.urlPlaceholder,
7965
+ onChange: (event) => setUrl(event.target.value), onKeyDown: onKey,
7966
+ }),
7967
+ E('input', {
7968
+ className: 'dig-input', value: dir, spellCheck: false,
7969
+ placeholder: props.dirPlaceholder === undefined ? '' : props.dirPlaceholder,
7970
+ onChange: (event) => setDir(event.target.value), onKeyDown: onKey,
7971
+ }),
7972
+ E('div', { className: 'dig-dialog-actions' },
7973
+ E('button', { type: 'button', className: 'dig-btn', onClick: props.onCancel }, props.cancelLabel),
7974
+ E('button', { type: 'button', className: 'dig-btn dig-btn-primary', disabled: ready === false, onClick: submit }, props.okLabel))))
7975
+ }
7976
+
7977
+ /* VS Code's "Git output" view (showOutput): the tail of the host's git-run
7978
+ ring buffer, fetched through the gitLogs method. A debugging view —
7979
+ opened on demand, refreshed by hand; newest land at the bottom and the
7980
+ body auto-scrolls to them. Panel-internal absolute positioning (13). */
7981
+ function GitOutputDrawer(props) {
7982
+ const t = props.t
7983
+ const ref = useRef(null)
7984
+ useEffect(() => { if (ref.current !== null) ref.current.scrollTop = ref.current.scrollHeight }, [props.entries])
7985
+ return E('div', { className: 'dig-gitlog' },
7986
+ E('div', { className: 'dig-gitlog-head' },
7987
+ E('span', { className: 'dig-gitlog-title' }, t('gitLog.title')),
7988
+ E('span', { className: 'dig-topbar-spacer' }),
7989
+ E('button', { type: 'button', className: 'dig-icon-btn dig-icon-btn-small', title: t('toolbar.refresh'), onClick: props.onRefresh }, E(Icon, { name: 'refresh', size: 13 })),
7990
+ E('button', { type: 'button', className: 'dig-icon-btn dig-icon-btn-small', title: t('confirm.cancel'), onClick: props.onClose }, E(Icon, { name: 'collapse', size: 13 }))),
7991
+ props.entries.length === 0
7992
+ ? E('div', { className: 'dig-gitlog-empty' }, t('gitLog.empty'))
7993
+ : E('div', { className: 'dig-gitlog-body', ref: ref },
7994
+ props.entries.map((entry, index) => E('div', {
7995
+ key: index,
7996
+ className: 'dig-gitlog-row' + (String(entry.code) === '0' ? '' : ' dig-gitlog-row-bad'),
7997
+ },
7998
+ E('div', { className: 'dig-gitlog-meta' },
7999
+ E('span', { className: 'dig-gitlog-code' }, String(entry.code)),
8000
+ E('span', { className: 'dig-gitlog-ms' }, String(entry.ms) + 'ms'),
8001
+ E('span', { className: 'dig-gitlog-at' }, String(entry.at)),
8002
+ E('span', { className: 'dig-gitlog-argv' }, String(entry.argv))),
8003
+ entry.out === '' && entry.err === '' ? null
8004
+ : E('pre', { className: 'dig-gitlog-pre' }, String(entry.err) === '' ? String(entry.out) : String(entry.out) + '\n' + String(entry.err))))))
8005
+ }
8006
+
7599
8007
  /* A destructive confirm: focus starts on Cancel (never on the red button), Enter
7600
8008
  only submits when the dialog asked for a typed confirmation, and passing
7601
8009
  requireText turns the dialog into a type-the-name gate. */
@@ -7726,7 +8134,7 @@ window.__ModuleLoader__.load({
7726
8134
  system colours over a dark translucent panel) and was unreadable. */
7727
8135
  let current = null
7728
8136
  for (const repo of props.repos) { if (repo.path === props.value) { current = repo; break } }
7729
- return E('span', { className: 'dig-select-wrap', title: t('repo.switch') },
8137
+ return E('span', { className: 'dig-select-wrap', title: current === null ? t('repo.switch') : current.path },
7730
8138
  E(Icon, { name: 'folder', size: 12 }),
7731
8139
  E('button', {
7732
8140
  type: 'button',
@@ -7913,7 +8321,15 @@ window.__ModuleLoader__.load({
7913
8321
 
7914
8322
  /* ============================== changes ============================== */
7915
8323
 
7916
- function ChangeRow(props) {
8324
+ /* Memoised with a hand-rolled comparison (issue #11): the callbacks are
8325
+ inline arrows and change identity on every parent render, so default
8326
+ shallow memo would never hit. Rows re-render only when their `item`
8327
+ reference changes — which is exactly what the two-phase stats merge
8328
+ produces (untouched rows keep their object). Stale-callback safety:
8329
+ every callback closes over `run`/`base`, and those only change together
8330
+ with a repo switch, which lands as a brand-new summary array — i.e.
8331
+ new `item` references, so the rows do re-render with fresh closures. */
8332
+ const ChangeRow = memo(function ChangeRow(props) {
7917
8333
  const t = props.t
7918
8334
  const item = props.item
7919
8335
  const raw = item.index === '?' ? '?' : (item.index + item.worktree).trim()
@@ -7939,7 +8355,7 @@ window.__ModuleLoader__.load({
7939
8355
  ? E('button', { type: 'button', className: 'dig-mini', title: t('action.unstage'), onClick: (event) => { event.stopPropagation(); props.onUnstage(item) } }, E(Icon, { name: 'minus', size: 12 }))
7940
8356
  : E('button', { type: 'button', className: 'dig-mini', title: t('action.stage'), onClick: (event) => { event.stopPropagation(); props.onStage(item) } }, E(Icon, { name: 'plus', size: 12 }))),
7941
8357
  readOnly ? null : E('button', { type: 'button', className: 'dig-mini', title: t('action.discard'), onClick: (event) => { event.stopPropagation(); props.onDiscard(item, props.group) } }, E(Icon, { name: 'undo', size: 12 })))
7942
- }
8358
+ }, (prev, next) => prev.item === next.item && prev.group === next.group && prev.t === next.t)
7943
8359
 
7944
8360
  function ChangesPanel(props) {
7945
8361
  const t = props.t
@@ -7965,7 +8381,12 @@ window.__ModuleLoader__.load({
7965
8381
  // clusters each group's files under their folder, the way "Group by:
7966
8382
  // Directory" does in an IDE. Folders are addressed by <group>|<dir>, so a
7967
8383
  // refresh never loses which ones the user folded away.
7968
- const [groupBy, setGroupBy] = useState('flat')
8384
+ // Lifted to the panel (v0.15): the "..." menu drives the same state, so
8385
+ // the header toggle and the menu can never disagree. The alias pair keeps
8386
+ // the body reads below untouched.
8387
+ const groupBy = props.groupBy === 'dir' ? 'dir' : 'flat'
8388
+ const setGroupBy = props.onGroupBy
8389
+ const sortKey = props.sortKey === undefined ? 'git' : props.sortKey
7969
8390
  const [foldedDirs, setFoldedDirs] = useState({})
7970
8391
  const changes = summary === null ? null : summary.changes
7971
8392
  const conflicted = changes === null ? [] : changes.conflicted
@@ -8021,29 +8442,79 @@ window.__ModuleLoader__.load({
8021
8442
  const dir = dirName(item.path)
8022
8443
  return dir === '' ? t('changes.rootDir') : dir
8023
8444
  }
8024
- const rowsOf = (key, entries) => {
8025
- if (groupBy !== 'dir') return entries.map((item) => entryRow(key, item))
8026
- const folders = new Map()
8027
- for (const item of entries) {
8028
- const name = folderOf(item)
8029
- if (folders.has(name) === false) folders.set(name, [])
8030
- folders.get(name).push(item)
8031
- }
8032
- return Array.from(folders.keys()).sort().map((name) => {
8033
- const id = key + '|' + name
8034
- const open = foldedDirs[id] !== true
8035
- const list = folders.get(name)
8036
- return E('div', { className: 'dig-folder', key: id },
8037
- E('button', {
8038
- type: 'button', className: 'dig-folder-head',
8039
- onClick: () => setFoldedDirs((previous) => Object.assign({}, previous, { [id]: previous[id] !== true })),
8040
- },
8041
- E('span', { className: 'dig-chevron' + (open ? ' dig-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
8042
- E('span', { className: 'dig-folder-name' }, name),
8043
- E('span', { className: 'dig-count' }, String(list.length))),
8044
- open ? list.map((item) => entryRow(key, item)) : null)
8045
- })
8445
+ /* The "..." menu's sort orders (v0.15). 'git' keeps git's own status
8446
+ order; the others are pure client-side sorts of already-loaded
8447
+ entries — no extra git calls, no re-fetch. */
8448
+ const sortedOf = (entries) => {
8449
+ if (sortKey === 'path') return entries.slice().sort((a, b) => a.path.localeCompare(b.path))
8450
+ if (sortKey === 'name') return entries.slice().sort((a, b) => baseName(a.path).localeCompare(baseName(b.path)) || a.path.localeCompare(b.path))
8451
+ if (sortKey === 'status') return entries.slice().sort((a, b) => (a.index + a.worktree).localeCompare(b.index + b.worktree) || a.path.localeCompare(b.path))
8452
+ return entries
8046
8453
  }
8454
+ /* Windowing: the change list is flattened into (group-head | folder-head
8455
+ | row) items with FIXED heights (see ITEM_HEIGHTS + the CSS pins), a
8456
+ prefix-sum index locates the visible window, and only that slice is
8457
+ mounted. A tree with thousands of files scrolls at frame rate. */
8458
+ const changesScrollRef = useRef(null)
8459
+ const changesViewport = useViewportBox(changesScrollRef)
8460
+ const changeItems = useMemo(() => {
8461
+ const out = []
8462
+ const groupHead = (key, title, entries) => E('div', { className: 'dig-group-head' },
8463
+ E('span', null, title),
8464
+ key === 'staged' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onUnstageAll() }, t('changes.unstageAll')) : null,
8465
+ key === 'unstaged' || key === 'untracked' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onStageAll(entries) }, t('changes.stageAll')) : null)
8466
+ const emitRows = (key, entries) => {
8467
+ const sorted = sortedOf(entries)
8468
+ if (groupBy !== 'dir') {
8469
+ for (const item of sorted) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item) })
8470
+ return
8471
+ }
8472
+ const folders = new Map()
8473
+ for (const item of sorted) {
8474
+ const name = folderOf(item)
8475
+ if (folders.has(name) === false) folders.set(name, [])
8476
+ folders.get(name).push(item)
8477
+ }
8478
+ for (const name of Array.from(folders.keys()).sort()) {
8479
+ const id = key + '|' + name
8480
+ const open = foldedDirs[id] !== true
8481
+ const list = folders.get(name)
8482
+ out.push({
8483
+ kind: 'folder', key: 'folder:' + id, h: ITEM_HEIGHTS.folder,
8484
+ node: E('button', {
8485
+ type: 'button', className: 'dig-folder-head',
8486
+ onClick: () => setFoldedDirs((previous) => Object.assign({}, previous, { [id]: previous[id] !== true })),
8487
+ },
8488
+ E('span', { className: 'dig-chevron' + (open ? ' dig-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
8489
+ E('span', { className: 'dig-folder-name' }, name),
8490
+ E('span', { className: 'dig-count' }, String(list.length))),
8491
+ })
8492
+ if (open === true) for (const item of list) out.push({ kind: 'row', key: key + ':' + item.path, h: ITEM_HEIGHTS.row, node: entryRow(key, item) })
8493
+ }
8494
+ }
8495
+ const emitGroup = (key, title, entries) => {
8496
+ if (entries.length === 0) return
8497
+ out.push({ kind: 'group', key: 'group:' + key, h: ITEM_HEIGHTS.group, node: groupHead(key, title, entries) })
8498
+ emitRows(key, entries)
8499
+ }
8500
+ emitGroup('conflicted', fill(t('counts.conflicted'), { n: conflicted.length }), conflicted)
8501
+ emitGroup('staged', fill(t('counts.staged'), { n: staged.length }), staged)
8502
+ emitGroup('unstaged', fill(t('counts.unstaged'), { n: unstaged.length }), unstaged)
8503
+ emitGroup('untracked', fill(t('counts.untracked'), { n: untracked.length }), untracked)
8504
+ emitGroup('ignored', fill(t('counts.ignored'), { n: ignored.length }), ignored)
8505
+ return out
8506
+ }, [conflicted, staged, unstaged, untracked, ignored, groupBy, foldedDirs, sortKey, t, props.onUnstageAll, props.onStageAll])
8507
+ const changeOffsets = useMemo(() => {
8508
+ const sums = [0]
8509
+ for (const item of changeItems) sums.push(sums[sums.length - 1] + item.h)
8510
+ return sums
8511
+ }, [changeItems])
8512
+ const winFirst = Math.max(0, windowStart(changeOffsets, changesViewport.scrollTop) - WINDOW_OVERSCAN)
8513
+ const winLimit = Math.ceil(changesViewport.viewH / ITEM_HEIGHTS.row) + WINDOW_OVERSCAN * 3
8514
+ let winLast = winFirst
8515
+ let used = 0
8516
+ while (winLast < changeItems.length && used < winLimit) { used += changeItems[winLast].h; winLast += 1 }
8517
+ const visibleItems = changeItems.slice(winFirst, winLast)
8047
8518
  const foldAll = (value) => {
8048
8519
  const next = {}
8049
8520
  if (value === true) {
@@ -8052,12 +8523,6 @@ window.__ModuleLoader__.load({
8052
8523
  }
8053
8524
  setFoldedDirs(next)
8054
8525
  }
8055
- const group = (key, title, entries) => entries.length === 0 ? null : E('div', { className: 'dig-group', key: key },
8056
- E('div', { className: 'dig-group-head' },
8057
- E('span', null, title),
8058
- key === 'staged' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onUnstageAll() }, t('changes.unstageAll')) : null,
8059
- key === 'unstaged' || key === 'untracked' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onStageAll(entries) }, t('changes.stageAll')) : null),
8060
- rowsOf(key, entries))
8061
8526
  const aiConfirmDialog = aiAsk !== true ? null : E(ConfirmDialog, {
8062
8527
  t: t,
8063
8528
  title: t('changes.aiOverwriteTitle'),
@@ -8162,14 +8627,14 @@ window.__ModuleLoader__.load({
8162
8627
  aiConfirmDialog,
8163
8628
  head,
8164
8629
  E('div', { className: 'dig-changes-body' },
8165
- E('div', { className: 'dig-changes-list' },
8630
+ E('div', { className: 'dig-changes-list', ref: changesScrollRef },
8166
8631
  collapsed ? null : [
8167
8632
  total === 0 && ignored.length === 0 ? E('div', { className: 'dig-empty', key: 'empty' }, t('changes.empty')) : null,
8168
- group('conflicted', fill(t('counts.conflicted'), { n: conflicted.length }), conflicted),
8169
- group('staged', fill(t('counts.staged'), { n: staged.length }), staged),
8170
- group('unstaged', fill(t('counts.unstaged'), { n: unstaged.length }), unstaged),
8171
- group('untracked', fill(t('counts.untracked'), { n: untracked.length }), untracked),
8172
- group('ignored', fill(t('counts.ignored'), { n: ignored.length }), ignored),
8633
+ /* Windowed slice: spacers above/below stand in for the unmounted
8634
+ rows (fixed heights, see ITEM_HEIGHTS). */
8635
+ winFirst > 0 ? E('div', { key: 'pad-top', style: { height: String(changeOffsets[winFirst]) + 'px' } }) : null,
8636
+ visibleItems.map((item) => E(Fragment, { key: item.key }, item.node)),
8637
+ winLast < changeItems.length ? E('div', { key: 'pad-bottom', style: { height: String(changeOffsets[changeItems.length] - changeOffsets[winLast]) + 'px' } }) : null,
8173
8638
  ]),
8174
8639
  composer))
8175
8640
  }
@@ -8193,6 +8658,144 @@ window.__ModuleLoader__.load({
8193
8658
  return { name: value, kind: value.indexOf('/') >= 0 ? 'remote' : 'local' }
8194
8659
  }
8195
8660
 
8661
+ /* Display form of a decoration: a HEAD decoration names the branch it points
8662
+ at, so the badge says "HEAD → main" — where HEAD sits must be readable at
8663
+ a glance (GitLens-style), not only inferable from the hollow dot. */
8664
+ function refLabel(decoration) {
8665
+ if (decoration.kind !== 'head') return decoration.name
8666
+ return decoration.name === 'HEAD' ? 'HEAD' : 'HEAD → ' + decoration.name
8667
+ }
8668
+
8669
+ /* ---- list windowing (issue #11 follow-up: hundreds of rows stay smooth) ----
8670
+ Both long lists (history rows, change rows) render only what the viewport
8671
+ can see plus an overscan skirt; spacer divs keep the scrollbar honest.
8672
+ Row heights are FIXED constants — the CSS pins each row kind to an exact
8673
+ height, so a prefix-sum index plus binary search locates the first
8674
+ visible item without measuring. */
8675
+ const HISTORY_ROW_H = 28 /* .dig-commit: height 26 + 2×1px padding */
8676
+ const ITEM_HEIGHTS = { group: 21, folder: 16, row: 24 }
8677
+ const WINDOW_OVERSCAN = 6
8678
+
8679
+ /* Tracks a scroll container's scrollTop (rAF-throttled) and clientHeight
8680
+ (ResizeObserver). SSR-safe: the effect never runs on the server. */
8681
+ function useViewportBox(scrollRef) {
8682
+ const [scrollTop, setScrollTop] = useState(0)
8683
+ const [viewH, setViewH] = useState(360)
8684
+ useEffect(() => {
8685
+ const el = scrollRef.current
8686
+ if (el === null || typeof ResizeObserver !== 'function') return undefined
8687
+ const observer = new ResizeObserver(() => setViewH(el.clientHeight))
8688
+ observer.observe(el)
8689
+ setViewH(el.clientHeight)
8690
+ let frame = 0
8691
+ const onScroll = () => {
8692
+ if (frame !== 0) return
8693
+ frame = requestAnimationFrame(() => { frame = 0; setScrollTop(el.scrollTop) })
8694
+ }
8695
+ el.addEventListener('scroll', onScroll, { passive: true })
8696
+ return () => {
8697
+ observer.disconnect()
8698
+ el.removeEventListener('scroll', onScroll)
8699
+ if (frame !== 0) cancelAnimationFrame(frame)
8700
+ }
8701
+ }, [scrollRef])
8702
+ return { scrollTop: scrollTop, viewH: viewH }
8703
+ }
8704
+
8705
+ /* First index whose cumulative offset exceeds `at` (binary search over the
8706
+ prefix-sum array; offsets.length === items.length + 1). */
8707
+ function windowStart(offsets, at) {
8708
+ let low = 0
8709
+ let high = offsets.length - 1
8710
+ while (low < high) {
8711
+ const mid = (low + high) >> 1
8712
+ if (offsets[mid] <= at) low = mid + 1
8713
+ else high = mid
8714
+ }
8715
+ return Math.max(0, low - 1)
8716
+ }
8717
+
8718
+ /* One history row, memoised on its data (commit + graph row + selection):
8719
+ filter keystrokes re-render the list shell without touching unchanged
8720
+ rows, and the +N overflow badge opens the hidden refs in a menu instead
8721
+ of hiding them behind a bare count. */
8722
+ const REF_ICONS = { head: 'star', local: 'branch', remote: 'fetch', tag: 'tag' }
8723
+ const REF_TONES = { head: 'success', local: 'accent', remote: 'violet', tag: 'warn' }
8724
+ const CommitRow = memo(function CommitRow(props) {
8725
+ const commit = props.commit
8726
+ const decorations = (Array.isArray(commit.refs) ? commit.refs : []).map(refInfo)
8727
+ /* Remote folding (GitLens-style dedupe): a remote whose short name
8728
+ (origin/main → main) matches a LOCAL or HEAD badge on the same commit
8729
+ stops taking a badge of its own — the local badge's title gains a
8730
+ "⇅ origin/main" line and the overflow menu still lists it. Nothing is
8731
+ lost, the row just stops saying "main" and "origin/main" twice. */
8732
+ const localNames = new Set(decorations.filter((d) => d.kind === 'local' || d.kind === 'head').map((d) => d.name))
8733
+ const foldedBy = new Map()
8734
+ const kept = []
8735
+ for (const decoration of decorations) {
8736
+ if (decoration.kind === 'remote') {
8737
+ const slash = decoration.name.indexOf('/')
8738
+ const short = slash >= 0 ? decoration.name.slice(slash + 1) : decoration.name
8739
+ if (localNames.has(short) === true) {
8740
+ if (foldedBy.has(short) === false) foldedBy.set(short, [])
8741
+ foldedBy.get(short).push(decoration.name)
8742
+ continue
8743
+ }
8744
+ }
8745
+ kept.push(decoration)
8746
+ }
8747
+ const folded = []
8748
+ for (const list of foldedBy.values()) folded.push(...list)
8749
+ const shown = kept.slice(0, 3)
8750
+ const hidden = kept.length - shown.length
8751
+ const badgeTitle = (decoration) => {
8752
+ const label = refLabel(decoration)
8753
+ if (decoration.kind !== 'local' && decoration.kind !== 'head') return label
8754
+ const folds = foldedBy.get(decoration.name)
8755
+ return folds === undefined ? label : label + '\n⇅ ' + folds.join(', ')
8756
+ }
8757
+ return E('div', {
8758
+ /* data-lane feeds the hover highlight: the scroll container copies the
8759
+ hovered row's lane into data-hover-lane, and generated CSS dims every
8760
+ graph stroke whose data-lane differs — zero React re-renders. */
8761
+ 'data-lane': String(props.row.lane),
8762
+ className: 'dig-commit' + (props.selected === true ? ' dig-commit-selected' : ''),
8763
+ onClick: () => props.onSelect(commit),
8764
+ onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, commit) },
8765
+ },
8766
+ E('span', { className: 'dig-commit-date', title: shortDate(commit.date) }, relativeTime(commit.date)),
8767
+ E('span', { className: 'dig-commit-author', title: commit.author }, commit.author),
8768
+ E(GraphCell, { row: props.row, height: 26 }),
8769
+ shown.map((decoration, index) => E('span', {
8770
+ key: decoration.kind + ':' + decoration.name + ':' + index,
8771
+ className: 'dig-ref dig-ref-' + decoration.kind,
8772
+ title: badgeTitle(decoration),
8773
+ }, refLabel(decoration))),
8774
+ hidden > 0 || folded.length > 0 ? E('span', {
8775
+ className: 'dig-ref dig-ref-more',
8776
+ title: kept.slice(3).map(refLabel).concat(folded.map((name) => '⇅ ' + name)).join('\n'),
8777
+ onClick: (event) => {
8778
+ event.stopPropagation()
8779
+ if (props.openMenuAt === undefined) return
8780
+ const entries = kept.slice(3).map((decoration, at) => ({
8781
+ id: 'ref:' + at,
8782
+ icon: REF_ICONS[decoration.kind] === undefined ? 'branch' : REF_ICONS[decoration.kind],
8783
+ tone: REF_TONES[decoration.kind] === undefined ? 'secondary' : REF_TONES[decoration.kind],
8784
+ label: refLabel(decoration),
8785
+ run: () => { copyText(refLabel(decoration)) },
8786
+ })).concat(folded.map((name, at) => ({
8787
+ id: 'fold:' + at,
8788
+ icon: 'fetch', tone: 'violet',
8789
+ label: '⇅ ' + name,
8790
+ run: () => { copyText(name) },
8791
+ })))
8792
+ props.openMenuAt(event, entries)
8793
+ },
8794
+ }, '+' + String(hidden + folded.length)) : null,
8795
+ E('span', { className: 'dig-commit-subject', title: commit.subject }, commit.subject),
8796
+ E('span', { className: 'dig-commit-fill' }))
8797
+ }, (prev, next) => prev.commit === next.commit && prev.row === next.row && prev.selected === next.selected && prev.t === next.t)
8798
+
8196
8799
  function sinceThreshold(key) {
8197
8800
  if (key === 'today') { const start = new Date(); start.setHours(0, 0, 0, 0); return start.getTime() }
8198
8801
  if (key === 'week') return Date.now() - 7 * DAY_MS
@@ -8204,6 +8807,8 @@ window.__ModuleLoader__.load({
8204
8807
  function HistoryList(props) {
8205
8808
  const t = props.t
8206
8809
  const commits = props.commits
8810
+ const historyScrollRef = useRef(null)
8811
+ const viewport = useViewportBox(historyScrollRef)
8207
8812
  const [text, setText] = useState('')
8208
8813
  const [author, setAuthor] = useState('')
8209
8814
  const [refName, setRefName] = useState('')
@@ -8326,32 +8931,40 @@ window.__ModuleLoader__.load({
8326
8931
  onClick: clearAll,
8327
8932
  }, E(Icon, { name: 'close', size: 12 })) : null)
8328
8933
 
8934
+ /* Windowed rendering: only the visible slice (± overscan) is mounted —
8935
+ "load more" into the hundreds still scrolls at frame rate. Spacers
8936
+ keep the scrollbar and the load-more button at honest positions. */
8937
+ const winStart = Math.max(0, Math.floor(viewport.scrollTop / HISTORY_ROW_H) - WINDOW_OVERSCAN)
8938
+ const winEnd = Math.min(rows.length, winStart + Math.ceil(viewport.viewH / HISTORY_ROW_H) + WINDOW_OVERSCAN * 2)
8939
+ const visibleRows = rows.slice(winStart, winEnd)
8940
+
8329
8941
  return E('div', { className: 'dig-history' }, filterBar,
8330
- E('div', { className: 'dig-history-scroll' },
8942
+ E('div', {
8943
+ className: 'dig-history-scroll',
8944
+ ref: historyScrollRef,
8945
+ /* Lane hover highlight (GitLens-style trace): copy the hovered row's
8946
+ lane into data-hover-lane; generated CSS dims every stroke whose
8947
+ data-lane differs. Direct DOM dataset writes on purpose — the
8948
+ alternative is re-rendering every row on each hover change. */
8949
+ onMouseOver: (event) => {
8950
+ const host = event.currentTarget
8951
+ const row = event.target instanceof Element ? event.target.closest('.dig-commit') : null
8952
+ const lane = row === null ? null : row.getAttribute('data-lane')
8953
+ if (lane === host.getAttribute('data-hover-lane')) return
8954
+ if (lane === null) host.removeAttribute('data-hover-lane')
8955
+ else host.setAttribute('data-hover-lane', lane)
8956
+ },
8957
+ onMouseLeave: (event) => event.currentTarget.removeAttribute('data-hover-lane'),
8958
+ },
8331
8959
  rows.length === 0 ? E('div', { className: 'dig-empty' }, dirtyFilter ? t('filter.none') : t('history.empty')) : null,
8332
- rows.map((row) => {
8333
- const commit = row.commit
8334
- const decorations = (Array.isArray(commit.refs) ? commit.refs : []).map(refInfo)
8335
- const shown = decorations.slice(0, 3)
8336
- const hidden = decorations.length - shown.length
8337
- return E('div', {
8338
- key: commit.hash,
8339
- className: 'dig-commit' + (props.selectedHash === commit.hash ? ' dig-commit-selected' : ''),
8340
- onClick: () => props.onSelect(commit),
8341
- onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, commit) },
8342
- },
8343
- E('span', { className: 'dig-commit-date', title: shortDate(commit.date) }, relativeTime(commit.date)),
8344
- E('span', { className: 'dig-commit-author', title: commit.author }, commit.author),
8345
- E(GraphCell, { row: row, height: 26 }),
8346
- shown.map((decoration, index) => E('span', {
8347
- key: decoration.kind + ':' + decoration.name + ':' + index,
8348
- className: 'dig-ref dig-ref-' + decoration.kind,
8349
- title: decoration.name,
8350
- }, decoration.name)),
8351
- hidden > 0 ? E('span', { className: 'dig-ref dig-ref-more' }, '+' + hidden) : null,
8352
- E('span', { className: 'dig-commit-subject', title: commit.subject }, commit.subject),
8353
- E('span', { className: 'dig-commit-fill' }))
8354
- }),
8960
+ winStart > 0 ? E('div', { key: 'pad-top', style: { height: String(winStart * HISTORY_ROW_H) + 'px' } }) : null,
8961
+ visibleRows.map((row) => E(CommitRow, {
8962
+ key: row.commit.hash,
8963
+ commit: row.commit, row: row, t: t,
8964
+ selected: props.selectedHash === row.commit.hash,
8965
+ onSelect: props.onSelect, onMenu: props.onMenu, openMenuAt: props.openMenuAt,
8966
+ })),
8967
+ winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
8355
8968
  props.hasMore === true
8356
8969
  ? E('button', { type: 'button', className: 'dig-load-more', disabled: props.busy === true, onClick: props.onLoadMore }, t('history.loadMore'))
8357
8970
  : null))
@@ -8734,9 +9347,78 @@ window.__ModuleLoader__.load({
8734
9347
  return () => { cancelled = true }
8735
9348
  }, [cwd, sessionId, tick])
8736
9349
 
9350
+ /* Changes view state, lifted out of ChangesPanel so the "..." menu (a
9351
+ rail action, invariant 14) can drive the same groupBy and the sort
9352
+ order it introduces — one source of truth, wherever the click came
9353
+ from. sortKey 'git' keeps git's own status order. */
9354
+ const [groupBy, setGroupBy] = useState('flat')
9355
+ const [sortKey, setSortKey] = useState('git')
9356
+ /* The Git-output drawer (VS Code showOutput): null = closed, an array =
9357
+ the entries it currently shows. Opened on demand, refreshed by hand or
9358
+ by reopening — it is a debugging view, not a live stream. */
9359
+ const [gitOutput, setGitOutput] = useState(null)
9360
+ const openGitOutput = useCallback(async () => {
9361
+ try {
9362
+ const data = await request('gitLogs', {})
9363
+ setGitOutput(Array.isArray(data.entries) ? data.entries : [])
9364
+ } catch (caught) {
9365
+ setError(caught instanceof Error ? caught.message : String(caught))
9366
+ }
9367
+ }, [request])
9368
+
9369
+
9370
+ /* Phase-two merge (issue #11): fold the [path, additions, deletions,
9371
+ binary] rows from the `stats` method into a deferred summary. Pure and
9372
+ immutable — untouched rows keep their object reference so the memoised
9373
+ ChangeRow skips them, and a group with no numeric change keeps its
9374
+ array reference so React reconciles nothing. Declared before use
9375
+ (hooks dependency TDZ rule). */
9376
+ const applyStats = (data, statsPayload) => {
9377
+ if (data === null || typeof data !== 'object' || statsPayload === null || typeof statsPayload !== 'object') return data
9378
+ const build = (entries, rows) => {
9379
+ if (Array.isArray(entries) !== true || entries.length === 0 || Array.isArray(rows) !== true) return entries
9380
+ const map = new Map()
9381
+ for (const row of rows) {
9382
+ if (Array.isArray(row) === true && row.length >= 4 && typeof row[0] === 'string') {
9383
+ map.set(row[0], { additions: row[1] | 0, deletions: row[2] | 0, binary: row[3] === true })
9384
+ }
9385
+ }
9386
+ let touched = false
9387
+ const next = entries.map((entry) => {
9388
+ const stat = map.get(entry.path)
9389
+ if (stat === undefined) {
9390
+ if (entry.additions === 0 && entry.deletions === 0 && entry.binary !== true) return entry
9391
+ touched = true
9392
+ return Object.assign({}, entry, { additions: 0, deletions: 0, binary: false })
9393
+ }
9394
+ if (entry.additions === stat.additions && entry.deletions === stat.deletions && entry.binary === stat.binary) return entry
9395
+ touched = true
9396
+ return Object.assign({}, entry, stat)
9397
+ })
9398
+ return touched ? next : entries
9399
+ }
9400
+ const changes = data.changes
9401
+ const staged = build(changes.staged, statsPayload.staged)
9402
+ const unstaged = build(changes.unstaged, statsPayload.unstaged)
9403
+ if (staged === changes.staged && unstaged === changes.unstaged) return data
9404
+ return Object.assign({}, data, {
9405
+ statsDeferred: false,
9406
+ changes: Object.assign({}, changes, { staged: staged, unstaged: unstaged }),
9407
+ })
9408
+ }
9409
+
8737
9410
  const loadSummary = useCallback(async () => {
8738
- const data = await request('summary', Object.assign({}, base, { ignored: showIgnored }))
9411
+ /* Two-phase (issue #11): the rows land first (`stats: false` skips the
9412
+ two numstat passes), then the +/- numbers merge in behind them. The
9413
+ panel is usable the moment the rows arrive; the numbers are exact
9414
+ when they arrive — nothing is approximated or dropped. */
9415
+ const data = await request('summary', Object.assign({}, base, { ignored: showIgnored, stats: false }))
8739
9416
  setSummary(data)
9417
+ if (data.statsDeferred === true) {
9418
+ const merged = applyStats(data, await request('stats', base))
9419
+ setSummary(merged)
9420
+ return merged
9421
+ }
8740
9422
  return data
8741
9423
  }, [base, showIgnored])
8742
9424
 
@@ -8790,9 +9472,18 @@ window.__ModuleLoader__.load({
8790
9472
 
8791
9473
  useEffect(() => {
8792
9474
  if (props.visible !== true || repoRoot === null) return undefined
9475
+ /* In-flight gate (issue #11): a summary over a tree with thousands of
9476
+ changes can outlast AUTO_REFRESH_MS, and re-entering just piles
9477
+ queued scans behind the repo lock (the interval deliberately does
9478
+ not run through `guard` — a background refresh must not flash the
9479
+ busy UI). Skip while the previous round is still running; the next
9480
+ tick re-checks. */
9481
+ let inFlight = false
8793
9482
  const timer = setInterval(() => {
9483
+ if (inFlight === true) return
9484
+ inFlight = true
8794
9485
  void (async () => {
8795
- try { await loadSummary() } catch (error) { void error }
9486
+ try { await loadSummary() } catch (error) { void error } finally { inFlight = false }
8796
9487
  })()
8797
9488
  }, AUTO_REFRESH_MS)
8798
9489
  return () => clearInterval(timer)
@@ -9222,6 +9913,7 @@ window.__ModuleLoader__.load({
9222
9913
  if (state.kind === 'newBranch') { await run('checkout', { branch: value, create: true, startPoint: state.from }); return }
9223
9914
  if (state.kind === 'renameBranch') { await run('branchRename', { from: state.from, to: value }); return }
9224
9915
  if (state.kind === 'newTag') { await run('tagCreate', { name: value, hash: state.hash }); return }
9916
+ if (state.kind === 'deleteTag') { await run('tagDelete', { name: value }); return }
9225
9917
  }, [run])
9226
9918
 
9227
9919
  /* ---------- toasts + undo ---------- */
@@ -9343,7 +10035,7 @@ window.__ModuleLoader__.load({
9343
10035
  loading: repoState === null,
9344
10036
  onOpenMenu: openRepoMenu,
9345
10037
  }),
9346
- branches === null || branches.local.length === 0 ? null : E('span', { className: 'dig-select-wrap', title: t('branch.switch') },
10038
+ branches === null || branches.local.length === 0 ? null : E('span', { className: 'dig-select-wrap', title: branches.branch },
9347
10039
  E(Icon, { name: 'branch', size: 12 }),
9348
10040
  E('button', {
9349
10041
  type: 'button',
@@ -9374,6 +10066,7 @@ window.__ModuleLoader__.load({
9374
10066
  const commitSettings = readCommitSettings()
9375
10067
  const changesPane = E(ChangesPanel, {
9376
10068
  t: t, summary: summary, busy: busy, compact: compact, hideHeader: compact,
10069
+ groupBy: groupBy, sortKey: sortKey, onGroupBy: setGroupBy,
9377
10070
  request: request, cwd: cwd, sessionId: sessionId, commitSettings: commitSettings, base: base,
9378
10071
  showIgnored: showIgnored,
9379
10072
  onToggleIgnored: () => setShowIgnored((value) => !value),
@@ -9476,6 +10169,48 @@ window.__ModuleLoader__.load({
9476
10169
  const stashCount = summary === null ? 0 : summary.stashCount
9477
10170
  const reason = t('action.unavailable')
9478
10171
 
10172
+ /* VS Code-style "..." menu — the SCM title menu: view mode, sort, sync,
10173
+ changes, branch/stash/tag shortcuts, and the Git-output view at the
10174
+ bottom. Every entry reuses the very handlers the rail rows and group
10175
+ heads already run, so the menu can never disagree with them. */
10176
+ const moreMenu = (event) => {
10177
+ const tagCount = branches === null || Array.isArray(branches.tags) !== true ? 0 : branches.tags.length
10178
+ const stageable = summary === null ? [] : summary.changes.unstaged.concat(summary.changes.untracked)
10179
+ const stagedPaths = summary === null ? [] : summary.changes.staged.map((entry) => entry.path)
10180
+ const sort = (key) => () => setSortKey(key)
10181
+ openMenuAt(event, [
10182
+ { id: 'view-flat', icon: 'commit', tone: 'secondary', label: t('changes.groupFlat'), active: groupBy === 'flat', run: () => setGroupBy('flat') },
10183
+ { id: 'view-dir', icon: 'folder', tone: 'secondary', label: t('changes.groupDir'), active: groupBy === 'dir', run: () => setGroupBy('dir') },
10184
+ null,
10185
+ { id: 'sort-git', icon: 'commit', tone: 'secondary', label: t('more.sortGit'), active: sortKey === 'git', run: sort('git') },
10186
+ { id: 'sort-path', icon: 'commit', tone: 'secondary', label: t('more.sortPath'), active: sortKey === 'path', run: sort('path') },
10187
+ { id: 'sort-name', icon: 'commit', tone: 'secondary', label: t('more.sortName'), active: sortKey === 'name', run: sort('name') },
10188
+ { id: 'sort-status', icon: 'commit', tone: 'secondary', label: t('more.sortStatus'), active: sortKey === 'status', run: sort('status') },
10189
+ null,
10190
+ { id: 'mm-pull', icon: 'pull', tone: 'accent', label: t('toolbar.pull'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void run('pull', { mode: 'ff-only' }) } },
10191
+ { id: 'mm-push', icon: 'push', tone: 'success', label: t('toolbar.push'), disabled: remoteReady !== true || blocked, reason, run: () => setDialog({ kind: 'push' }) },
10192
+ { id: 'mm-fetch', icon: 'fetch', tone: 'cyan', label: t('toolbar.fetch'), disabled: remoteReady !== true, reason, run: () => { void run('fetch', { prune: true }) } },
10193
+ { id: 'mm-checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout') + '...', disabled: otherBranches.length === 0 || blocked, reason, run: (ev) => pickBranchMenu(ev, 'checkout') },
10194
+ { id: 'mm-clone', icon: 'plus', tone: 'secondary', label: t('more.clone'), run: () => setDialog({ kind: 'clone' }) },
10195
+ null,
10196
+ { id: 'mm-stage-all', icon: 'plus', tone: 'success', label: t('changes.stageAll'), disabled: stageable.length === 0, reason, run: () => { void run('stage', { paths: stageable.map((entry) => entry.path) }) } },
10197
+ { id: 'mm-unstage-all', icon: 'minus', tone: 'warn', label: t('changes.unstageAll'), disabled: stagedPaths.length === 0, reason, run: () => { void run('unstage', { paths: stagedPaths }) } },
10198
+ null,
10199
+ { id: 'mm-new-branch', icon: 'plus', tone: 'success', label: t('toolbar.newBranch'), disabled: blocked, reason, run: () => setDialog({ kind: 'newBranch' }) },
10200
+ { id: 'mm-delete-branch', icon: 'trash', tone: 'danger', danger: true, label: t('action.delete'), disabled: otherBranches.length === 0, reason, run: (ev) => pickBranchMenu(ev, 'delete') },
10201
+ null,
10202
+ { id: 'mm-stash-push', icon: 'stash', tone: 'violet', label: t('stash.push'), disabled: dirty === 0, reason, run: () => { void run('stashPush', { includeUntracked: true }) } },
10203
+ { id: 'mm-stash-apply', icon: 'checkout', tone: 'accent', label: t('stash.apply'), disabled: stashCount === 0, reason, run: () => { void run('stashApply', {}) } },
10204
+ { id: 'mm-stash-pop', icon: 'stash', tone: 'accent', label: t('more.stashPop'), disabled: stashCount === 0, reason, run: () => { void run('stashApply', { pop: true }) } },
10205
+ { id: 'mm-stash-drop', icon: 'trash', tone: 'danger', danger: true, label: t('stash.drop'), disabled: stashCount === 0, reason, run: () => setDialog({ kind: 'stashDrop', ref: 'stash@{0}' }) },
10206
+ null,
10207
+ { id: 'mm-new-tag', icon: 'tag', tone: 'warn', label: t('action.newTagHere'), disabled: commits.length === 0, reason, run: () => setDialog({ kind: 'newTag' }) },
10208
+ { id: 'mm-delete-tag', icon: 'trash', tone: 'danger', danger: true, label: t('more.deleteTag'), disabled: tagCount === 0, reason, run: () => setDialog({ kind: 'deleteTag' }) },
10209
+ null,
10210
+ { id: 'mm-git-output', icon: 'commit', tone: 'secondary', label: t('more.gitOutput'), run: () => { void openGitOutput() } },
10211
+ ])
10212
+ }
10213
+
9479
10214
  /* One descriptor per action, with the availability IDEA would use: delete /
9480
10215
  checkout / compare need another local branch, fetch and push need a remote,
9481
10216
  pull needs an upstream, diff needs pending changes, tag needs a commit.
@@ -9494,6 +10229,7 @@ window.__ModuleLoader__.load({
9494
10229
  else if (spec.id === 'fetch') { entry.disabled = remoteReady !== true; entry.run = () => { void run('fetch', { prune: true }) } }
9495
10230
  else if (spec.id === 'pull') { entry.disabled = remoteReady !== true || tracked !== true || blocked; entry.run = () => { void run('pull', { mode: 'ff-only' }) } }
9496
10231
  else if (spec.id === 'push') { entry.disabled = remoteReady !== true || blocked; entry.run = () => setDialog({ kind: 'push' }) }
10232
+ else if (spec.id === 'more') { entry.run = (event) => moreMenu(event) }
9497
10233
  else if (spec.id === 'tree') { entry.active = treeOpen; entry.run = () => foldTree(!treeOpen) }
9498
10234
  else if (spec.id === 'float') {
9499
10235
  // Label AND icon follow where the panel actually is: floating panels get
@@ -9725,8 +10461,8 @@ window.__ModuleLoader__.load({
9725
10461
  onClose: () => setRailSettings(false),
9726
10462
  }))
9727
10463
  }
9728
- if (dialog !== null && (dialog.kind === 'newBranch' || dialog.kind === 'renameBranch' || dialog.kind === 'newTag')) {
9729
- const titles = { newBranch: t('prompt.newBranch'), renameBranch: t('prompt.renameBranch'), newTag: t('prompt.newTag') }
10464
+ if (dialog !== null && (dialog.kind === 'newBranch' || dialog.kind === 'renameBranch' || dialog.kind === 'newTag' || dialog.kind === 'deleteTag')) {
10465
+ const titles = { newBranch: t('prompt.newBranch'), renameBranch: t('prompt.renameBranch'), newTag: t('prompt.newTag'), deleteTag: t('prompt.deleteTag') }
9730
10466
  overlays.push(E(PromptDialog, {
9731
10467
  key: 'prompt',
9732
10468
  title: titles[dialog.kind],
@@ -9737,6 +10473,17 @@ window.__ModuleLoader__.load({
9737
10473
  onSubmit: (value) => { void submitDialog(dialog, value) },
9738
10474
  }))
9739
10475
  }
10476
+ if (dialog !== null && dialog.kind === 'clone') {
10477
+ overlays.push(E(CloneDialog, {
10478
+ key: 'clone',
10479
+ title: t('clone.title'),
10480
+ urlPlaceholder: t('clone.url'),
10481
+ dirPlaceholder: t('clone.dir'),
10482
+ okLabel: t('clone.run'), cancelLabel: t('confirm.cancel'),
10483
+ onCancel: () => setDialog(null),
10484
+ onSubmit: (url, dir) => { setDialog(null); void run('clone', { url: url, dir: dir }) },
10485
+ }))
10486
+ }
9740
10487
  if (dialog !== null && dialog.kind === 'deleteBranch') {
9741
10488
  const guarded = isProtectedBranch(dialog.name)
9742
10489
  overlays.push(E(ConfirmDialog, {
@@ -9830,6 +10577,11 @@ window.__ModuleLoader__.load({
9830
10577
  E('div', { className: 'dig-shell' },
9831
10578
  columns === true && repoRoot !== null ? renderRail(true) : null,
9832
10579
  body),
10580
+ gitOutput === null ? null : E(GitOutputDrawer, {
10581
+ t: t, entries: gitOutput,
10582
+ onRefresh: () => { void openGitOutput() },
10583
+ onClose: () => setGitOutput(null),
10584
+ }),
9833
10585
  overlays,
9834
10586
  E(ToastStack, { t: t, toasts: toasts }))
9835
10587
  }
@@ -9844,23 +10596,46 @@ window.__ModuleLoader__.load({
9844
10596
  /* Tokens only (--dsw-alias-*): transparent themes and background plugins
9845
10597
  (dsh-any-background) keep showing through, like the host's own panels. */
9846
10598
 
10599
+ /* Lane-hover trace, generated: one rule per lane (lanes are capped at
10600
+ GRAPH_MAX_LANES, so this is a fixed handful). When the scroll container
10601
+ carries data-hover-lane="N", every graph stroke whose data-lane differs
10602
+ fades — the hovered lane reads as one continuous thread down the graph
10603
+ with zero React involvement. */
10604
+ const LANE_TRACE_RULES = []
10605
+ for (let lane = 0; lane < GRAPH_MAX_LANES; lane += 1) {
10606
+ LANE_TRACE_RULES.push(
10607
+ '.dig-history-scroll[data-hover-lane="' + lane + '"] .dig-graph line:not([data-lane="' + lane + '"])'
10608
+ + ',.dig-history-scroll[data-hover-lane="' + lane + '"] .dig-graph path:not([data-lane="' + lane + '"])'
10609
+ + ',.dig-history-scroll[data-hover-lane="' + lane + '"] .dig-graph circle:not([data-lane="' + lane + '"])'
10610
+ + '{stroke-opacity:.18;fill-opacity:.18}',
10611
+ )
10612
+ }
10613
+ LANE_TRACE_RULES.push('.dig-graph line,.dig-graph path,.dig-graph circle{transition:stroke-opacity .15s,fill-opacity .15s}')
10614
+
9847
10615
  const CSS = [
9848
10616
  '.dig-root{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;background:transparent;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui,sans-serif);font-size:13px;line-height:1.5;font-weight:600;overflow:hidden}',
9849
10617
  '.dig-topbar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none;min-width:0;border-bottom:1px solid var(--dsw-alias-border-l2);overflow:hidden}',
9850
10618
  '.dig-topbar-spacer{flex:1;min-width:4px}',
9851
10619
  '.dig-busy{color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}',
9852
10620
  '.dig-track{color:var(--dsw-alias-label-secondary);flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}',
9853
- '.dig-select-wrap{display:inline-flex;align-items:center;gap:4px;min-width:0;color:var(--dsw-alias-label-secondary)}',
9854
- '.dig-select{appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);color:var(--dsw-alias-label-primary);font:inherit;font-weight:500;padding:1px 6px;height:22px;max-width:190px;min-width:0;cursor:pointer;text-overflow:ellipsis}',
10621
+ '.dig-select-wrap{display:inline-flex;align-items:center;gap:4px;min-width:0;flex:0 1 auto;max-width:90%;color:var(--dsw-alias-label-secondary)}',
10622
+ '.dig-select{appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);color:var(--dsw-alias-label-primary);font:inherit;font-weight:500;padding:1px 6px;height:22px;max-width:min(280px,42%);min-width:0;cursor:pointer;text-overflow:ellipsis}',
9855
10623
  '.dig-select:hover{background:var(--dsw-alias-interactive-bg-hover)}',
9856
10624
  '.dig-select:focus-visible{outline:1px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:-1px}',
9857
10625
  '.dig-select-btn{display:inline-flex;align-items:center;gap:4px;text-align:left;white-space:nowrap;overflow:hidden}',
9858
10626
  '.dig-select-btn .dig-repo-name{flex:1;min-width:0;max-width:none}',
9859
- '.dig-select-repo{max-width:200px}',
10627
+ '.dig-select-repo{max-width:none}',
9860
10628
  '.dig-select option,.dig-filter-select option{background:var(--dsw-alias-bg-layer-1,#22262e);color:var(--dsw-alias-label-primary,#e6e8ee)}',
9861
- '.dig-select-branch{max-width:150px}',
10629
+ /* Long branch names (the topbar picker): the cap lives on the WRAP (a
10630
+ direct topbar child, so the percentage resolves against the bar). The
10631
+ button grows a SECOND LINE instead of ellipsising — a sidebar is
10632
+ ~400px wide, and a 600px branch name simply cannot fit on one line,
10633
+ but two lines hold it (the user-facing rule: show the WHOLE name).
10634
+ The full name also rides on the wrapper's title. */
10635
+ '.dig-select-branch{max-width:none;height:auto;min-height:22px;align-items:center}',
10636
+ '.dig-select-branch .dig-repo-name{white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.3}',
9862
10637
  '.dig-repo-static{display:inline-flex;align-items:center;gap:5px;min-width:0;color:var(--dsw-alias-label-secondary)}',
9863
- '.dig-repo-name{color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:200px}',
10638
+ '.dig-repo-name{color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:none}',
9864
10639
  '.dig-icon-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;flex:none}',
9865
10640
  '.dig-icon-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
9866
10641
  '.dig-icon-btn:disabled{opacity:.4;cursor:default}',
@@ -10009,7 +10784,7 @@ window.__ModuleLoader__.load({
10009
10784
  '.dig-folder{display:flex;flex-direction:column}',
10010
10785
  '.dig-branch-folder{display:flex;flex-direction:column}',
10011
10786
  '.dig-row-picked{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary)}',
10012
- '.dig-folder-head{display:flex;align-items:center;gap:6px;width:100%;padding:1px 8px 1px 4px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;text-align:left}',
10787
+ '.dig-folder-head{display:flex;align-items:center;gap:6px;width:100%;padding:1px 8px 1px 4px;height:14px;border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;cursor:pointer;text-align:left}',
10013
10788
  '.dig-folder-head:hover{color:var(--dsw-alias-label-primary)}',
10014
10789
  '.dig-folder-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
10015
10790
  '.dig-count{margin-left:auto;opacity:.6;font-weight:400}',
@@ -10044,7 +10819,7 @@ window.__ModuleLoader__.load({
10044
10819
  '.dig-ref-local{color:var(--dsw-alias-brand-primary)}',
10045
10820
  '.dig-ref-remote{color:#b083f0}',
10046
10821
  '.dig-ref-tag{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
10047
- '.dig-ref-more{color:var(--dsw-alias-label-tertiary)}',
10822
+ '.dig-ref-more{color:var(--dsw-alias-label-tertiary);cursor:pointer}',
10048
10823
  '.dig-filters{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;min-width:0;overflow-x:auto;overflow-y:hidden;border-bottom:1px solid var(--dsw-alias-border-l2)}',
10049
10824
  '.dig-filter-text{flex:1 1 90px;min-width:80px;width:auto}',
10050
10825
  '.dig-filter-select{flex:none;max-width:118px;appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);color:var(--dsw-alias-label-secondary);font:inherit;font-weight:500;height:22px;padding:0 4px;cursor:pointer;text-overflow:ellipsis}',
@@ -10060,10 +10835,10 @@ window.__ModuleLoader__.load({
10060
10835
  '.dig-changes-body{display:flex;flex-direction:column;min-height:0;flex:1}',
10061
10836
  '.dig-changes-list{flex:1;overflow:auto;min-height:0}',
10062
10837
  '.dig-group{display:flex;flex-direction:column}',
10063
- '.dig-group-head{display:flex;align-items:center;gap:8px;padding:2px 8px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}',
10838
+ '.dig-group-head{display:flex;align-items:center;gap:8px;padding:2px 8px;height:17px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}',
10064
10839
  '.dig-link{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;padding:0;flex:none}',
10065
10840
  '.dig-link:hover{text-decoration:underline}',
10066
- '.dig-row-file{gap:6px}',
10841
+ '.dig-row-file{gap:6px;height:20px}',
10067
10842
  '.dig-file-status{flex:none;width:14px;text-align:center;font-weight:700;font-size:10px;color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-label-secondary))}',
10068
10843
  '.dig-file-status-A{color:var(--dsw-alias-state-success-primary)}',
10069
10844
  '.dig-file-status-D{color:var(--dsw-alias-state-error-primary)}',
@@ -10118,6 +10893,21 @@ window.__ModuleLoader__.load({
10118
10893
  '.dig-file-icon{flex:none;opacity:.95}',
10119
10894
  '.dig-overlay{position:absolute;inset:0;padding:8px;box-sizing:border-box;background:var(--dsw-alias-bg-mask-1,rgba(0,0,0,.42));-webkit-backdrop-filter:var(--dsw-mask-blur,none);backdrop-filter:var(--dsw-mask-blur,none);display:flex;align-items:center;justify-content:center;z-index:60}',
10120
10895
  '.dig-dialog{box-sizing:border-box;min-width:min(240px,100%);max-width:min(420px,94%);max-height:100%;overflow:auto;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));border:0;border-radius:var(--dsw-radius-panel,28px);padding:14px;display:flex;flex-direction:column;gap:10px;box-shadow:var(--dsw-elevation-prominent,0 12px 32px rgba(0,0,0,.35))}',
10896
+ /* Git-output drawer (VS Code showOutput): panel-internal absolute bottom
10897
+ sheet (invariant 13). */
10898
+ '.dig-gitlog{position:absolute;left:0;right:0;bottom:0;z-index:55;height:46%;min-height:132px;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));border-radius:var(--dsw-radius-panel,28px) var(--dsw-radius-panel,28px) 0 0;box-shadow:var(--dsw-elevation-prominent,0 -8px 28px rgba(0,0,0,.3))}',
10899
+ '.dig-gitlog-head{display:flex;align-items:center;gap:4px;padding:8px 14px 6px;flex:none}',
10900
+ '.dig-gitlog-title{font-weight:600}',
10901
+ '.dig-gitlog-empty{padding:10px 14px;color:var(--dsw-alias-label-tertiary)}',
10902
+ '.dig-gitlog-body{flex:1;min-height:0;overflow:auto;padding:0 14px 10px;font-weight:400}',
10903
+ '.dig-gitlog-row{padding:6px 0;border-top:1px solid var(--dsw-alias-border-l1,transparent)}',
10904
+ '.dig-gitlog-row-bad .dig-gitlog-code{color:var(--dsw-alias-state-danger,#e5484d);font-weight:700}',
10905
+ '.dig-gitlog-meta{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}',
10906
+ '.dig-gitlog-code{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
10907
+ '.dig-gitlog-ms{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}',
10908
+ '.dig-gitlog-at{color:var(--dsw-alias-label-tertiary);font-size:11px}',
10909
+ '.dig-gitlog-argv{font-weight:600;word-break:break-all}',
10910
+ '.dig-gitlog-pre{margin:4px 0 0;padding:6px 8px;border-radius:var(--dsw-radius-md,12px);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-bg-layer-2));overflow:auto;max-height:160px;white-space:pre-wrap;word-break:break-all;font-size:11px}',
10121
10911
  '.dig-dialog-wide{min-width:min(360px,92%)}',
10122
10912
  '.dig-dialog-title{font-weight:600}',
10123
10913
  '.dig-dialog-text{color:var(--dsw-alias-label-secondary);white-space:pre-wrap}',
@@ -10219,7 +11009,7 @@ window.__ModuleLoader__.load({
10219
11009
  '.dig-lf-tool svg{width:15px;height:15px}',
10220
11010
  '.dig-lf-tool:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}',
10221
11011
  '.dig-lf-tool[aria-pressed=true] .dig-lf-compareIcon{transform:rotate(90deg)}',
10222
- ].join('\n')
11012
+ ].concat(LANE_TRACE_RULES).join('\n')
10223
11013
 
10224
11014
  /* ============================== plugin ============================== */
10225
11015