dsh-ide-git 0.23.0 → 0.25.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
@@ -434,6 +434,10 @@ window.__ModuleLoader__.load({
434
434
  'prompt.newTag': '新标签名',
435
435
  'prompt.fromHead': '当前 HEAD',
436
436
  'error.dismiss': '关闭',
437
+ 'error.pushRejected': '推送被拒绝:远程分支上有你本地没有的提交。请先「同步」(拉取并推送),或拉取合并后再推送。',
438
+ 'error.pullDiverged': '本地和远程各有新提交,无法快进。请用「拉取」合并,或改用「拉取(变基)」。',
439
+ 'error.pullConflict': '拉取时发生冲突:请在「变更」里解决冲突文件并提交,然后继续。',
440
+ 'error.pullDirty': '拉取会覆盖本地未提交的更改:请先提交或贮藏这些更改。',
437
441
  'status.loading': '正在读取...',
438
442
  'status.busy': '处理中...',
439
443
  'counts.staged': '已暂存 {n}',
@@ -802,6 +806,10 @@ window.__ModuleLoader__.load({
802
806
  'prompt.newTag': 'New tag name',
803
807
  'prompt.fromHead': 'current HEAD',
804
808
  'error.dismiss': 'Dismiss',
809
+ 'error.pushRejected': 'Push rejected: the remote branch has commits you do not have. Sync (pull and push), or pull (merge) and push again.',
810
+ 'error.pullDiverged': 'Local and remote have both moved, so a fast-forward is impossible. Pull with merge, or use Pull (Rebase).',
811
+ 'error.pullConflict': 'Pull stopped on conflicts: resolve the conflicted files under Changes, then commit.',
812
+ 'error.pullDirty': 'Pull would overwrite uncommitted local changes. Commit or stash them first.',
805
813
  'status.loading': 'Reading...',
806
814
  'status.busy': 'Working...',
807
815
  'counts.staged': 'Staged {n}',
@@ -1178,6 +1186,10 @@ window.__ModuleLoader__.load({
1178
1186
  'prompt.newTag': '新標籤名',
1179
1187
  'prompt.fromHead': '當前 HEAD',
1180
1188
  'error.dismiss': '關閉',
1189
+ 'error.pushRejected': '推送被拒絕:遠端分支有啲你本地冇嘅提交。請先同步(拉取並推送),或者拉取合併之後再推送。',
1190
+ 'error.pullDiverged': '本地同遠端各有新提交,唔可以快進。請用「拉取」合併,或者改用「拉取(變基)」。',
1191
+ 'error.pullConflict': '拉取時有衝突:喺「變更」入面解決衝突檔案並提交,然後繼續。',
1192
+ 'error.pullDirty': '拉取會覆蓋本地未提交嘅改動:請先提交或者貯藏呢啲改動。',
1181
1193
  'status.loading': '讀取中...',
1182
1194
  'status.busy': '處理中...',
1183
1195
  'counts.staged': '已暫存 {n}',
@@ -1546,6 +1558,10 @@ window.__ModuleLoader__.load({
1546
1558
  'prompt.newTag': '新標籤名稱',
1547
1559
  'prompt.fromHead': '目前 HEAD',
1548
1560
  'error.dismiss': '關閉',
1561
+ 'error.pushRejected': '推送被拒絕:遠端分支上有你本機沒有的提交。請先同步(拉取並推送),或拉取合併後再推送。',
1562
+ 'error.pullDiverged': '本機與遠端各有新提交,無法快進。請用「拉取」合併,或改用「拉取(變基)」。',
1563
+ 'error.pullConflict': '拉取時發生衝突:請在「變更」中解決衝突檔案並提交,然後繼續。',
1564
+ 'error.pullDirty': '拉取會覆蓋本機未提交的變更:請先提交或貯藏這些變更。',
1549
1565
  'status.loading': '讀取中...',
1550
1566
  'status.busy': '處理中...',
1551
1567
  'counts.staged': '已暫存 {n}',
@@ -1914,6 +1930,10 @@ window.__ModuleLoader__.load({
1914
1930
  'prompt.newTag': '新標籤名',
1915
1931
  'prompt.fromHead': '當前 HEAD',
1916
1932
  'error.dismiss': '關閉',
1933
+ 'error.pushRejected': '推送被拒絕:遠端分支有啲你本地冇嘅提交。請先同步(拉取並推送),或者拉取合併之後再推送。',
1934
+ 'error.pullDiverged': '本地同遠端各有新提交,唔可以快進。請用「拉取」合併,或者改用「拉取(變基)」。',
1935
+ 'error.pullConflict': '拉取時有衝突:喺「變更」入面解決衝突檔案並提交,然後繼續。',
1936
+ 'error.pullDirty': '拉取會覆蓋本地未提交嘅改動:請先提交或者貯藏呢啲改動。',
1917
1937
  'status.loading': '讀取中...',
1918
1938
  'status.busy': '處理中...',
1919
1939
  'counts.staged': '已暫存 {n}',
@@ -2282,6 +2302,10 @@ window.__ModuleLoader__.load({
2282
2302
  'prompt.newTag': '新しいタグ名',
2283
2303
  'prompt.fromHead': '現在の HEAD',
2284
2304
  'error.dismiss': '閉じる',
2305
+ 'error.pushRejected': 'プッシュが拒否されました:リモートブランチにローカルにないコミットがあります。先に同期(プルしてプッシュ)するか、プル(マージ)してから再プッシュしてください。',
2306
+ 'error.pullDiverged': 'ローカルとリモートの両方に新しいコミットがあり、fast-forward できません。「プル」(マージ)または「プル(リベース)」を使ってください。',
2307
+ 'error.pullConflict': 'プルで競合が発生しました。「変更」で競合ファイルを解決してコミットしてから続行してください。',
2308
+ 'error.pullDirty': 'プルするとローカルの未コミット変更が上書きされます。先にコミットするかスタッシュしてください。',
2285
2309
  'status.loading': '読み込み中...',
2286
2310
  'status.busy': '処理中...',
2287
2311
  'counts.staged': 'ステージ済み {n}',
@@ -2650,6 +2674,10 @@ window.__ModuleLoader__.load({
2650
2674
  'prompt.newTag': '새 태그 이름',
2651
2675
  'prompt.fromHead': '현재 HEAD',
2652
2676
  'error.dismiss': '닫기',
2677
+ 'error.pushRejected': '푸시가 거부되었습니다: 원격 브랜치에 로컬에 없는 커밋이 있습니다. 먼저 동기화(풀 후 푸시)하거나 풀(병합)한 뒤 다시 푸시하세요.',
2678
+ 'error.pullDiverged': '로컬과 원격에 각각 새 커밋이 있어 fast-forward할 수 없습니다. 풀(병합) 또는 풀(리베이스)을 사용하세요.',
2679
+ 'error.pullConflict': '풀 중 충돌이 발생했습니다. "변경"에서 충돌 파일을 해결하고 커밋한 뒤 계속하세요.',
2680
+ 'error.pullDirty': '풀을 하면 로컬의 커밋되지 않은 변경이 덮어써집니다. 먼저 커밋하거나 스태시하세요.',
2653
2681
  'status.loading': '읽는 중...',
2654
2682
  'status.busy': '처리 중...',
2655
2683
  'counts.staged': '스테이지됨 {n}',
@@ -3018,6 +3046,10 @@ window.__ModuleLoader__.load({
3018
3046
  'prompt.newTag': 'Name des neuen Tags',
3019
3047
  'prompt.fromHead': 'aktuelles HEAD',
3020
3048
  'error.dismiss': 'Schließen',
3049
+ 'error.pushRejected': 'Push abgelehnt: Der Remote-Branch enthält Commits, die lokal fehlen. Zuerst synchronisieren (Pull und Push) oder pullen (Merge) und dann erneut pushen.',
3050
+ 'error.pullDiverged': 'Lokal und remote sind beide weitergezogen; ein Fast-Forward ist unmöglich. Mit Merge pullen oder Pull (Rebase) verwenden.',
3051
+ 'error.pullConflict': 'Pull wurde wegen Konflikten gestoppt: Konfliktdateien unter „Änderungen“ auflösen, committen und dann fortfahren.',
3052
+ 'error.pullDirty': 'Pull würde lokale, nicht committete Änderungen überschreiben. Zuerst committen oder stashen.',
3021
3053
  'status.loading': 'Wird gelesen...',
3022
3054
  'status.busy': 'Arbeitet...',
3023
3055
  'counts.staged': 'Vorgemerkt {n}',
@@ -3386,6 +3418,10 @@ window.__ModuleLoader__.load({
3386
3418
  'prompt.newTag': 'Nom de la nouvelle étiquette',
3387
3419
  'prompt.fromHead': 'HEAD courant',
3388
3420
  'error.dismiss': 'Fermer',
3421
+ 'error.pushRejected': 'Push refusé : la branche distante contient des commits absents en local. Synchronisez (pull puis push) ou faites un pull (merge), puis poussez à nouveau.',
3422
+ 'error.pullDiverged': 'Local et distant ont tous deux avancé ; le fast-forward est impossible. Faites un pull (merge) ou un pull (rebase).',
3423
+ 'error.pullConflict': 'Le pull s\'est arrêté sur des conflits : résolvez les fichiers en conflit dans « Modifications », puis validez.',
3424
+ 'error.pullDirty': 'Le pull écraserait des modifications locales non validées. Validez-les ou remisez-les d\'abord.',
3389
3425
  'status.loading': 'Lecture...',
3390
3426
  'status.busy': 'En cours...',
3391
3427
  'counts.staged': 'Indexé {n}',
@@ -3754,6 +3790,10 @@ window.__ModuleLoader__.load({
3754
3790
  'prompt.newTag': 'Имя нового тега',
3755
3791
  'prompt.fromHead': 'текущий HEAD',
3756
3792
  'error.dismiss': 'Закрыть',
3793
+ 'error.pushRejected': 'Push отклонён: в удалённой ветке есть коммиты, которых нет локально. Сначала синхронизируйтесь (pull и push) или выполните pull (merge), затем повторите push.',
3794
+ 'error.pullDiverged': 'Локальная и удалённая ветки разошлись; fast-forward невозможен. Используйте pull (merge) или pull (rebase).',
3795
+ 'error.pullConflict': 'Pull остановился из-за конфликтов: разрешите конфликтные файлы в «Изменениях» и закоммитьте их.',
3796
+ 'error.pullDirty': 'Pull перезапишет незакоммиченные локальные изменения. Сначала закоммитьте или спрячьте их.',
3757
3797
  'status.loading': 'Чтение...',
3758
3798
  'status.busy': 'Работа...',
3759
3799
  'counts.staged': 'В индексе {n}',
@@ -4122,6 +4162,10 @@ window.__ModuleLoader__.load({
4122
4162
  'prompt.newTag': 'Nome da nova tag',
4123
4163
  'prompt.fromHead': 'HEAD atual',
4124
4164
  'error.dismiss': 'Fechar',
4165
+ 'error.pushRejected': 'Push rejeitado: o branch remoto tem commits que não existem localmente. Sincronize (pull e push) ou faça pull (merge) e envie novamente.',
4166
+ 'error.pullDiverged': 'Local e remoto avançaram; o fast-forward é impossível. Use pull (merge) ou pull (rebase).',
4167
+ 'error.pullConflict': 'O pull parou em conflitos: resolva os arquivos em conflito em "Alterações" e faça o commit.',
4168
+ 'error.pullDirty': 'O pull sobrescreveria alterações locais não commitadas. Faça commit ou stash primeiro.',
4125
4169
  'status.loading': 'Lendo...',
4126
4170
  'status.busy': 'Trabalhando...',
4127
4171
  'counts.staged': 'Preparado {n}',
@@ -4490,6 +4534,10 @@ window.__ModuleLoader__.load({
4490
4534
  'prompt.newTag': 'Nome del nuovo tag',
4491
4535
  'prompt.fromHead': 'HEAD corrente',
4492
4536
  'error.dismiss': 'Chiudi',
4537
+ 'error.pushRejected': 'Push rifiutato: il branch remoto contiene commit assenti in locale. Sincronizza (pull e push) oppure esegui pull (merge) e riprova.',
4538
+ 'error.pullDiverged': 'Locale e remoto sono avanzati entrambi; il fast-forward è impossibile. Usa pull (merge) o pull (rebase).',
4539
+ 'error.pullConflict': 'Il pull si è fermato per conflitti: risolvi i file in conflitto in "Modifiche", poi esegui il commit.',
4540
+ 'error.pullDirty': 'Il pull sovrascriverebbe modifiche locali non committate. Esegui prima commit o stash.',
4493
4541
  'status.loading': 'Lettura...',
4494
4542
  'status.busy': 'In corso...',
4495
4543
  'counts.staged': 'In stage {n}',
@@ -4858,6 +4906,10 @@ window.__ModuleLoader__.load({
4858
4906
  'prompt.newTag': 'Naam van de nieuwe tag',
4859
4907
  'prompt.fromHead': 'huidige HEAD',
4860
4908
  'error.dismiss': 'Sluiten',
4909
+ 'error.pushRejected': 'Push geweigerd: de remote branch heeft commits die lokaal ontbreken. Synchroniseer (pull en push) of pull (merge) en push daarna opnieuw.',
4910
+ 'error.pullDiverged': 'Lokaal en remote zijn beide verdergegaan; fast-forward is onmogelijk. Gebruik pull (merge) of pull (rebase).',
4911
+ 'error.pullConflict': 'Pull stopte door conflicten: los de conflictbestanden op onder "Wijzigingen" en commit daarna.',
4912
+ 'error.pullDirty': 'Pull zou niet-gecommitte lokale wijzigingen overschrijven. Commit of stash ze eerst.',
4861
4913
  'status.loading': 'Lezen...',
4862
4914
  'status.busy': 'Bezig...',
4863
4915
  'counts.staged': 'Gestaged {n}',
@@ -5226,6 +5278,10 @@ window.__ModuleLoader__.load({
5226
5278
  'prompt.newTag': 'Nazwa nowego tagu',
5227
5279
  'prompt.fromHead': 'bieżący HEAD',
5228
5280
  'error.dismiss': 'Zamknij',
5281
+ 'error.pushRejected': 'Push odrzucony: zdalna gałąź ma commity, których nie ma lokalnie. Najpierw zsynchronizuj (pull i push) albo wykonaj pull (merge) i ponów push.',
5282
+ 'error.pullDiverged': 'Lokalna i zdalna gałąź rozeszły się; fast-forward jest niemożliwy. Użyj pull (merge) lub pull (rebase).',
5283
+ 'error.pullConflict': 'Pull zatrzymał się na konfliktach: rozwiąż pliki konfliktów w „Zmianach” i zatwierdź zmiany.',
5284
+ 'error.pullDirty': 'Pull nadpisałby niezatwierdzone zmiany lokalne. Najpierw je zatwierdź lub schowaj.',
5229
5285
  'status.loading': 'Wczytywanie...',
5230
5286
  'status.busy': 'Praca...',
5231
5287
  'counts.staged': 'W indeksie {n}',
@@ -5594,6 +5650,10 @@ window.__ModuleLoader__.load({
5594
5650
  'prompt.newTag': 'Namn på ny tagg',
5595
5651
  'prompt.fromHead': 'aktuell HEAD',
5596
5652
  'error.dismiss': 'Stäng',
5653
+ 'error.pushRejected': 'Push avvisades: fjärrgrenen har commits som saknas lokalt. Synkronisera (pull och push) eller gör pull (merge) och pusha igen.',
5654
+ 'error.pullDiverged': 'Lokalt och fjärr har båda flyttats; fast-forward är omöjligt. Använd pull (merge) eller pull (rebase).',
5655
+ 'error.pullConflict': 'Pull stoppade på konflikter: lös konfliktfilerna under "Ändringar" och commit:a.',
5656
+ 'error.pullDirty': 'Pull skulle skriva över lokala ej commit:ade ändringar. Commit:a eller stash:a dem först.',
5597
5657
  'status.loading': 'Läser...',
5598
5658
  'status.busy': 'Arbetar...',
5599
5659
  'counts.staged': 'Klädda {n}',
@@ -5962,6 +6022,10 @@ window.__ModuleLoader__.load({
5962
6022
  'prompt.newTag': 'Yeni etiket adı',
5963
6023
  'prompt.fromHead': 'geçerli HEAD',
5964
6024
  'error.dismiss': 'Kapat',
6025
+ 'error.pushRejected': 'Push reddedildi: uzak dalda yerelde olmayan commit\'ler var. Önce senkronize edin (pull ve push) veya pull (merge) yapıp yeniden push\'layın.',
6026
+ 'error.pullDiverged': 'Yerel ve uzak dal ikisi de ilerledi; fast-forward mümkün değil. Pull (merge) veya pull (rebase) kullanın.',
6027
+ 'error.pullConflict': 'Pull çakışmalarda durdu: "Değişiklikler" altında çakışan dosyaları çözüp commit\'leyin.',
6028
+ 'error.pullDirty': 'Pull, commit\'lenmemiş yerel değişikliklerin üzerine yazar. Önce commit\'leyin veya stash\'leyin.',
5965
6029
  'status.loading': 'Okunuyor...',
5966
6030
  'status.busy': 'Çalışıyor...',
5967
6031
  'counts.staged': 'Hazırlanan {n}',
@@ -6330,6 +6394,10 @@ window.__ModuleLoader__.load({
6330
6394
  'prompt.newTag': 'Nama tag baru',
6331
6395
  'prompt.fromHead': 'HEAD saat ini',
6332
6396
  'error.dismiss': 'Tutup',
6397
+ 'error.pushRejected': 'Push ditolak: branch remote memiliki commit yang tidak ada di lokal. Sinkronkan (pull lalu push) atau pull (merge) lalu push lagi.',
6398
+ 'error.pullDiverged': 'Lokal dan remote sama-sama maju; fast-forward tidak mungkin. Gunakan pull (merge) atau pull (rebase).',
6399
+ 'error.pullConflict': 'Pull berhenti karena konflik: selesaikan file konflik di "Perubahan", lalu commit.',
6400
+ 'error.pullDirty': 'Pull akan menimpa perubahan lokal yang belum di-commit. Commit atau stash dulu.',
6333
6401
  'status.loading': 'Membaca...',
6334
6402
  'status.busy': 'Bekerja...',
6335
6403
  'counts.staged': 'Ter-stage {n}',
@@ -6698,6 +6766,10 @@ window.__ModuleLoader__.load({
6698
6766
  'prompt.newTag': 'Tên thẻ mới',
6699
6767
  'prompt.fromHead': 'HEAD hiện tại',
6700
6768
  'error.dismiss': 'Đóng',
6769
+ 'error.pushRejected': 'Push bị từ chối: nhánh remote có commit mà local chưa có. Hãy đồng bộ (pull rồi push) hoặc pull (merge) rồi push lại.',
6770
+ 'error.pullDiverged': 'Local và remote đều đã tiến lên; không thể fast-forward. Dùng pull (merge) hoặc pull (rebase).',
6771
+ 'error.pullConflict': 'Pull dừng do xung đột: giải quyết các tệp xung đột trong "Thay đổi" rồi commit.',
6772
+ 'error.pullDirty': 'Pull sẽ ghi đè thay đổi local chưa commit. Hãy commit hoặc stash trước.',
6701
6773
  'status.loading': 'Đang đọc...',
6702
6774
  'status.busy': 'Đang xử lý...',
6703
6775
  'counts.staged': 'Đã stage {n}',
@@ -7066,6 +7138,10 @@ window.__ModuleLoader__.load({
7066
7138
  'prompt.newTag': 'اسم الوسم الجديد',
7067
7139
  'prompt.fromHead': 'HEAD الحالي',
7068
7140
  'error.dismiss': 'إغلاق',
7141
+ 'error.pushRejected': 'تم رفض الدفع: الفرع البعيد يحتوي على commits غير موجودة محليًا. زامن أولًا (pull ثم push) أو اسحب (merge) ثم أعد الدفع.',
7142
+ 'error.pullDiverged': 'تقدّم الفرعان المحلي والبعيد؛ لا يمكن fast-forward. استخدم pull (merge) أو pull (rebase).',
7143
+ 'error.pullConflict': 'توقف pull بسبب تعارضات: حلّ ملفات التعارض في "التغييرات" ثم commit.',
7144
+ 'error.pullDirty': 'سيؤدي pull إلى الكتابة فوق تغييرات محلية غير مثبتة. ثبّتها أو خزّنها أولًا.',
7069
7145
  'status.loading': 'جارٍ القراءة...',
7070
7146
  'status.busy': 'جارٍ العمل...',
7071
7147
  'counts.staged': 'مُجهَّز {n}',
@@ -7434,6 +7510,10 @@ window.__ModuleLoader__.load({
7434
7510
  'prompt.newTag': 'नए टैग का नाम',
7435
7511
  'prompt.fromHead': 'वर्तमान HEAD',
7436
7512
  'error.dismiss': 'बंद करें',
7513
+ 'error.pushRejected': 'पुश अस्वीकृत: रिमोट ब्रांच में ऐसे commits हैं जो लोकल में नहीं हैं। पहले सिंक करें (pull फिर push) या pull (merge) करके फिर से push करें।',
7514
+ 'error.pullDiverged': 'लोकल और रिमोट दोनों आगे बढ़े हैं; fast-forward संभव नहीं। pull (merge) या pull (rebase) इस्तेमाल करें।',
7515
+ 'error.pullConflict': 'pull conflicts पर रुक गया: "परिवर्तन" में conflict फ़ाइलें हल करें, फिर commit करें।',
7516
+ 'error.pullDirty': 'pull लोकल की uncommitted changes को overwrite करेगा। पहले commit या stash करें।',
7437
7517
  'status.loading': 'पढ़ा जा रहा है...',
7438
7518
  'status.busy': 'कार्य जारी...',
7439
7519
  'counts.staged': 'स्टेज्ड {n}',
@@ -7802,6 +7882,10 @@ window.__ModuleLoader__.load({
7802
7882
  'prompt.newTag': 'ชื่อแท็กใหม่',
7803
7883
  'prompt.fromHead': 'HEAD ปัจจุบัน',
7804
7884
  'error.dismiss': 'ปิด',
7885
+ 'error.pushRejected': 'Push ถูกปฏิเสธ: สาขา remote มี commit ที่ยังไม่มีในเครื่องนี้ ให้ sync (pull แล้ว push) หรือ pull (merge) แล้ว push อีกครั้ง',
7886
+ 'error.pullDiverged': 'ทั้ง local และ remote มี commit ใหม่; fast-forward ไม่ได้ ใช้ pull (merge) หรือ pull (rebase)',
7887
+ 'error.pullConflict': 'Pull หยุดเพราะมี conflict: แก้ไฟล์ที่ขัดแย้งใน "การเปลี่ยนแปลง" แล้ว commit',
7888
+ 'error.pullDirty': 'Pull จะเขียนทับการเปลี่ยนแปลง local ที่ยังไม่ commit: ให้ commit หรือ stash ก่อน',
7805
7889
  'status.loading': 'กำลังอ่าน...',
7806
7890
  'status.busy': 'กำลังทำงาน...',
7807
7891
  'counts.staged': 'สเตจแล้ว {n}',
@@ -13055,6 +13139,9 @@ window.__ModuleLoader__.load({
13055
13139
  const [commits, setCommits] = useState([])
13056
13140
  const [hasMore, setHasMore] = useState(false)
13057
13141
  const [error, setError] = useState(null)
13142
+ /* Named failure code (push-rejected / pull-conflict / ...), kept beside
13143
+ the raw message so the banner can lead with an actionable sentence. */
13144
+ const [errorCode, setErrorCode] = useState(null)
13058
13145
  const [note, setNote] = useState(null)
13059
13146
  const [busy, setBusy] = useState(false)
13060
13147
  const [view, setView] = useState('history')
@@ -13398,10 +13485,13 @@ window.__ModuleLoader__.load({
13398
13485
  const guard = useCallback(async (work) => {
13399
13486
  setBusy(true)
13400
13487
  setError(null)
13488
+ setErrorCode(null)
13401
13489
  try {
13402
13490
  return await work()
13403
13491
  } catch (caught) {
13404
- setError(caught instanceof Error ? caught.message : String(caught))
13492
+ const failure = caught instanceof Error ? caught : new Error(String(caught))
13493
+ setError(failure.message)
13494
+ setErrorCode(typeof failure.code === 'string' ? failure.code : null)
13405
13495
  return undefined
13406
13496
  } finally {
13407
13497
  setBusy(false)
@@ -13446,7 +13536,7 @@ window.__ModuleLoader__.load({
13446
13536
  writePaintRepos(cwd, data)
13447
13537
  apply(data, readPaintRoot(cwd))
13448
13538
  } catch (caught) {
13449
- if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught))
13539
+ if (!cancelled) { setErrorCode(null); setError(caught instanceof Error ? caught.message : String(caught)) }
13450
13540
  }
13451
13541
  })()
13452
13542
  return () => { cancelled = true }
@@ -13474,6 +13564,7 @@ window.__ModuleLoader__.load({
13474
13564
  const data = await request('gitLogs', {})
13475
13565
  setGitOutput(Array.isArray(data.entries) ? data.entries : [])
13476
13566
  } catch (caught) {
13567
+ setErrorCode(null)
13477
13568
  setError(caught instanceof Error ? caught.message : String(caught))
13478
13569
  }
13479
13570
  }, [request])
@@ -13699,7 +13790,11 @@ window.__ModuleLoader__.load({
13699
13790
 
13700
13791
  const run = useCallback(async (method, payload) => {
13701
13792
  const result = await guard(() => request(method, Object.assign({}, base, payload)))
13702
- if (result !== undefined) {
13793
+ /* A failed pull/push still changes repository state (merge conflicts,
13794
+ or a merge that landed before the push was rejected), so refresh on
13795
+ those failures too — otherwise the Changes list hides the conflict
13796
+ files the banner is asking the user to resolve. */
13797
+ if (result !== undefined || method === 'pull' || method === 'push') {
13703
13798
  await refresh()
13704
13799
  /* v0.21.2: the composer pill is a separate tree; without this it only
13705
13800
  learned about an in-panel checkout/commit at the next 30s poll. The
@@ -13760,6 +13855,7 @@ window.__ModuleLoader__.load({
13760
13855
  const data = await request('diff', Object.assign({}, base, seed))
13761
13856
  setPatch(data.patch)
13762
13857
  } catch (caught) {
13858
+ setErrorCode(null)
13763
13859
  setError(caught instanceof Error ? caught.message : String(caught))
13764
13860
  } finally {
13765
13861
  setPatchLoading(false)
@@ -13862,6 +13958,7 @@ window.__ModuleLoader__.load({
13862
13958
  try {
13863
13959
  setDetail(await request('commitDetail', Object.assign({}, base, { hash: commit.hash })))
13864
13960
  } catch (caught) {
13961
+ setErrorCode(null)
13865
13962
  setError(caught instanceof Error ? caught.message : String(caught))
13866
13963
  }
13867
13964
  }, [base])
@@ -14034,6 +14131,7 @@ window.__ModuleLoader__.load({
14034
14131
  const createWorktree = async (draft) => {
14035
14132
  setBusy(true)
14036
14133
  setError(null)
14134
+ setErrorCode(null)
14037
14135
  try {
14038
14136
  await request('worktreeAdd', Object.assign({}, base, draft))
14039
14137
  setDialog(null)
@@ -14049,6 +14147,7 @@ window.__ModuleLoader__.load({
14049
14147
  const removeWorktree = async (tree, force) => {
14050
14148
  setBusy(true)
14051
14149
  setError(null)
14150
+ setErrorCode(null)
14052
14151
  try {
14053
14152
  await request('worktreeRemove', Object.assign({}, base, { path: tree.path, confirm: true, force: force === true }))
14054
14153
  setDialog(null)
@@ -14062,6 +14161,7 @@ window.__ModuleLoader__.load({
14062
14161
  return message
14063
14162
  }
14064
14163
  setDialog(null)
14164
+ setErrorCode(null)
14065
14165
  setError(message)
14066
14166
  return message
14067
14167
  } finally {
@@ -14071,6 +14171,7 @@ window.__ModuleLoader__.load({
14071
14171
  const pruneWorktrees = async () => {
14072
14172
  setBusy(true)
14073
14173
  setError(null)
14174
+ setErrorCode(null)
14074
14175
  try {
14075
14176
  const data = await request('worktreePrune', Object.assign({}, base, { confirm: true }))
14076
14177
  setDialog(null)
@@ -14079,6 +14180,7 @@ window.__ModuleLoader__.load({
14079
14180
  return data
14080
14181
  } catch (caught) {
14081
14182
  setDialog(null)
14183
+ setErrorCode(null)
14082
14184
  setError(caught instanceof Error ? caught.message : String(caught))
14083
14185
  return null
14084
14186
  } finally {
@@ -14259,7 +14361,26 @@ window.__ModuleLoader__.load({
14259
14361
  ].filter((item) => item !== null))
14260
14362
  return
14261
14363
  }
14262
- const updateItem = { id: 'update', icon: 'fetch', tone: 'cyan', label: t('action.update'), run: () => { void run('fetch', { prune: true }) } }
14364
+ /* v0.24.0: the label was always 拉取, but this only ran git fetch.
14365
+ The checked-out branch now takes a real pull (merge, the new
14366
+ default); a non-HEAD branch keeps the safe refspec update, because
14367
+ git pull would merge into HEAD instead of into that branch. */
14368
+ const updateItem = {
14369
+ id: 'update', icon: 'fetch', tone: 'cyan', label: t('action.update'),
14370
+ run: () => {
14371
+ if (entry.head === true) {
14372
+ const upstream = entry.upstream
14373
+ if (upstream === null || upstream === undefined || upstream === '') { void run('fetch', { prune: true }); return }
14374
+ void run('pull', { mode: 'merge' })
14375
+ return
14376
+ }
14377
+ const upstream = entry.upstream
14378
+ if (upstream === null || upstream === undefined || upstream === '') { void run('fetch', { prune: true }); return }
14379
+ const slash = upstream.indexOf('/')
14380
+ const remote = slash >= 0 ? upstream.slice(0, slash) : upstream
14381
+ void run('fetch', { remote: remote, branch: entry.name + ':' + entry.name })
14382
+ },
14383
+ }
14263
14384
  const pushItem = { id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) }
14264
14385
  const items = [
14265
14386
  { id: 'checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout'), disabled: entry.head === true || entry.tag === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void checkout(entry) } },
@@ -14662,9 +14783,29 @@ window.__ModuleLoader__.load({
14662
14783
  E('span', { className: 'dig-topbar-spacer' }),
14663
14784
  busy ? E('span', { className: 'dig-busy' }, t('status.busy')) : null)
14664
14785
 
14786
+ /* Named git failures get a sentence that says what to do; the raw git
14787
+ output stays one hover away in the title. The two recoverable ones
14788
+ (push rejected, ff-only divergence) also offer the exact next action:
14789
+ Sync = pull with merge, then push again. */
14790
+ const errorHint = errorCode === 'push-rejected' ? t('error.pushRejected')
14791
+ : errorCode === 'pull-diverged' ? t('error.pullDiverged')
14792
+ : errorCode === 'pull-conflict' ? t('error.pullConflict')
14793
+ : errorCode === 'pull-dirty' ? t('error.pullDirty')
14794
+ : null
14795
+ const syncFromBanner = () => {
14796
+ setError(null)
14797
+ setErrorCode(null)
14798
+ void (async () => {
14799
+ const pulled = await run('pull', { mode: 'merge' })
14800
+ if (pulled !== undefined) await run('push', { confirm: true })
14801
+ })()
14802
+ }
14665
14803
  const banner = error === null ? null : E('div', { className: 'dig-banner' },
14666
- E('span', { className: 'dig-banner-text' }, error),
14667
- E('button', { type: 'button', className: 'dig-link', onClick: () => setError(null) }, t('error.dismiss')))
14804
+ E('span', { className: 'dig-banner-text', title: errorHint === null ? undefined : error }, errorHint === null ? error : errorHint),
14805
+ errorHint !== null && (errorCode === 'push-rejected' || errorCode === 'pull-diverged')
14806
+ ? E('button', { type: 'button', className: 'dig-link', onClick: syncFromBanner }, t('more.sync'))
14807
+ : null,
14808
+ E('button', { type: 'button', className: 'dig-link', onClick: () => { setError(null); setErrorCode(null) } }, t('error.dismiss')))
14668
14809
 
14669
14810
  const noteBanner = note === null ? null : E('div', { className: 'dig-note' },
14670
14811
  E('span', { className: 'dig-banner-text' }, note),
@@ -14708,7 +14849,12 @@ window.__ModuleLoader__.load({
14708
14849
  const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
14709
14850
  E('div', { className: 'dig-diff-head' },
14710
14851
  E('span', { className: 'dig-mono dig-diff-path' }, selectedPath === null ? '' : selectedPath),
14711
- E('button', { type: 'button', className: 'dig-icon-btn', onClick: () => { setPatch(''); setSelectedPath(null); setDiffContext(null) } }, E(Icon, { name: 'close', size: 12 }))),
14852
+ E('button', {
14853
+ type: 'button',
14854
+ className: 'dig-icon-btn',
14855
+ title: t('toast.close'),
14856
+ onClick: () => { setPatch(''); setSelectedPath(null); setDiffContext(null) },
14857
+ }, E(Icon, { name: 'close', size: 12 }))),
14712
14858
  E(DiffBody, { patch: patch, loading: patchLoading, t: t }))
14713
14859
 
14714
14860
  /* ---------- rail (IDE-style left action strip) ---------- */
@@ -14856,7 +15002,7 @@ window.__ModuleLoader__.load({
14856
15002
  ] },
14857
15003
  null,
14858
15004
  /* —— top-level sync (VS Code header row) —— */
14859
- { 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' }) } },
15005
+ { id: 'mm-pull', icon: 'pull', tone: 'accent', label: t('toolbar.pull'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void run('pull', { mode: 'merge' }) } },
14860
15006
  { id: 'mm-push', icon: 'push', tone: 'success', label: t('toolbar.push'), disabled: remoteReady !== true || blocked, reason, run: () => setDialog({ kind: 'push' }) },
14861
15007
  { id: 'mm-clone', icon: 'plus', tone: 'secondary', label: t('more.clone'), run: () => setDialog({ kind: 'clone' }) },
14862
15008
  { id: 'mm-checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout') + '...', disabled: otherBranches.length === 0 || blocked, reason, run: (ev) => pickBranchMenu(ev, 'checkout') },
@@ -14885,12 +15031,12 @@ window.__ModuleLoader__.load({
14885
15031
  ] },
14886
15032
  /* —— SCM ▸ Pull, Push ▸ —— */
14887
15033
  { id: 'sub-pullpush', icon: 'pull', tone: 'accent', label: t('more.pullpushGroup'), items: [
14888
- { id: 'pp-sync', label: t('more.sync'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void (async () => { await run('pull', { mode: 'ff-only' }); await run('push', { confirm: true }) })() } },
15034
+ { id: 'pp-sync', label: t('more.sync'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void (async () => { await run('pull', { mode: 'merge' }); await run('push', { confirm: true }) })() } },
14889
15035
  { id: 'pp-sync-rebase', label: t('more.syncRebase'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void (async () => { await run('pull', { mode: 'rebase' }); await run('push', { confirm: true }) })() } },
14890
15036
  null,
14891
- { id: 'pp-pull', label: t('toolbar.pull'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void run('pull', { mode: 'ff-only' }) } },
15037
+ { id: 'pp-pull', label: t('toolbar.pull'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void run('pull', { mode: 'merge' }) } },
14892
15038
  { id: 'pp-pull-rebase', label: t('more.pullRebase'), disabled: remoteReady !== true || tracked !== true || blocked, reason, run: () => { void run('pull', { mode: 'rebase' }) } },
14893
- { id: 'pp-pull-from', label: t('more.pullFrom'), disabled: remoteNames.length === 0, reason, run: pickRemote((name) => { void run('pull', { mode: 'ff-only', remote: name }) }) },
15039
+ { id: 'pp-pull-from', label: t('more.pullFrom'), disabled: remoteNames.length === 0, reason, run: pickRemote((name) => { void run('pull', { mode: 'merge', remote: name }) }) },
14894
15040
  null,
14895
15041
  { id: 'pp-push', label: t('toolbar.push'), disabled: remoteReady !== true || blocked, reason, run: () => setDialog({ kind: 'push' }) },
14896
15042
  { id: 'pp-push-force', label: t('more.pushForce'), danger: true, disabled: remoteReady !== true || blocked, reason, run: () => setDialog({ kind: 'pushForce' }) },
@@ -15017,7 +15163,7 @@ window.__ModuleLoader__.load({
15017
15163
  const pullPickedBranches = async (picked) => {
15018
15164
  for (const item of picked) {
15019
15165
  if (item.kind !== 'local') continue
15020
- if (item.entry.head === true) { await run('pull', { mode: 'ff-only' }); continue }
15166
+ if (item.entry.head === true) { await run('pull', { mode: 'merge' }); continue }
15021
15167
  const upstream = item.entry.upstream
15022
15168
  if (upstream === null || upstream === undefined || upstream === '') continue
15023
15169
  const slash = upstream.indexOf('/')
@@ -15076,7 +15222,7 @@ window.__ModuleLoader__.load({
15076
15222
  }
15077
15223
  else if (spec.id === 'pull') {
15078
15224
  if (railPickedLocals.length > 0) { entry.label = fill(t('toolbar.pullPicked'), { n: railPickedLocals.length }); entry.disabled = blocked; entry.run = () => { void pullPickedBranches(railPickedLocals) } }
15079
- else { entry.disabled = remoteReady !== true || tracked !== true || blocked; entry.run = () => { void run('pull', { mode: 'ff-only' }) } }
15225
+ else { entry.disabled = remoteReady !== true || tracked !== true || blocked; entry.run = () => { void run('pull', { mode: 'merge' }) } }
15080
15226
  }
15081
15227
  else if (spec.id === 'push') {
15082
15228
  if (railPickedLocals.length > 0) { entry.label = fill(t('toolbar.pushPicked'), { n: railPickedLocals.length }); entry.disabled = blocked; entry.run = () => setDialog({ kind: 'pushMany', items: railPickedLocals.map((item) => ({ name: item.name, upstream: item.entry.upstream })) }) }
@@ -16281,6 +16427,13 @@ window.__ModuleLoader__.load({
16281
16427
  '.dig-wt-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500}',
16282
16428
  '.dig-wt-error{padding:6px 8px;margin-bottom:8px;border-radius:var(--dsw-radius-sm,8px);background:rgba(220,38,38,.12);color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;word-break:break-word}',
16283
16429
  'body[data-ds-dark-theme] .dig-wt-error{background:rgba(248,113,113,.16)}',
16430
+ /* ---- icon tooltip ---- */
16431
+ /* A body child, not a panel child: the bottom workbench declares
16432
+ `contain: layout style`, so a fixed element inside it would take the
16433
+ panel box as its containing block and land off-screen (invariant 13).
16434
+ Styled from the same menu tokens so it reads as part of the product. */
16435
+ '.dig-tip{position:fixed;z-index:9999;display:none;box-sizing:border-box;max-width:280px;padding:5px 8px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)));color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-prominent,0 8px 22px rgba(0,0,0,.32));font-family:var(--dsw-font-family,system-ui,sans-serif);font-size:calc(11px + var(--dsh-content-font-delta-secondary,0px));font-weight:500;line-height:1.45;text-align:left;white-space:normal;overflow-wrap:anywhere;pointer-events:none}',
16436
+ '.dig-tip[data-show="true"]{display:block}',
16284
16437
  ].concat(LANE_TRACE_RULES).join('\n')
16285
16438
 
16286
16439
  /* ============================== plugin ============================== */
@@ -17377,6 +17530,193 @@ window.__ModuleLoader__.load({
17377
17530
  }))))
17378
17531
  }
17379
17532
 
17533
+ /* ---- icon tooltips ----
17534
+ Every icon action already carries its name in `title` (the rail adds the
17535
+ disabled reason; the toolbar / mini / pill buttons carry the action
17536
+ verb). Native title bubbles are the wrong surface for this product: the
17537
+ bottom workbench is a dense horizontal strip, the bubble needs a full
17538
+ second of rest, it is drawn in the browser's own styling, and over a
17539
+ DISABLED rail button it never appears at all. Draw one themed card
17540
+ instead, shared by every seat (panel, composer pill, diff preview) and
17541
+ hung off <body> — see the `.dig-tip` note in CSS for why it can't live
17542
+ inside the panel. The card mirrors the anchor's own `title`, which is
17543
+ removed while the card is up so the native bubble never doubles it. */
17544
+ const TIP_ID = 'dsh-ide-git-tip'
17545
+ const TIP_DELAY_MS = 120
17546
+ const TIP_GAP = 6
17547
+ /* Only this plugin's own surfaces explain themselves. `.dig-root` is the
17548
+ panel (native and better-sidebar seats alike), `.dig-cs-root` the
17549
+ composer status pill, `.dig-preview-root` the external diff seat. */
17550
+ const TIP_ROOTS = '.dig-root, .dig-cs-root, .dig-preview-root'
17551
+
17552
+ function installHoverTips() {
17553
+ /* The SSR self-check drives apply() with a minimal document stub: it has
17554
+ no body, no id lookup and no event targets. Those environments get the
17555
+ no-op installer rather than a throw during plugin activation. */
17556
+ if (typeof document === 'undefined' || document.body == null
17557
+ || typeof document.createElement !== 'function'
17558
+ || typeof document.getElementById !== 'function'
17559
+ || typeof document.addEventListener !== 'function'
17560
+ || typeof window === 'undefined'
17561
+ || typeof window.addEventListener !== 'function') return () => {}
17562
+ /* HMR / re-apply: never stack two cards (or two listener sets on one). */
17563
+ const stale = document.getElementById(TIP_ID)
17564
+ if (stale !== null) stale.remove()
17565
+ const card = document.createElement('div')
17566
+ card.id = TIP_ID
17567
+ card.className = 'dig-tip'
17568
+ card.setAttribute('data-dsh-ide-git', '')
17569
+ card.setAttribute('role', 'tooltip')
17570
+ card.setAttribute('aria-hidden', 'true')
17571
+ document.body.appendChild(card)
17572
+
17573
+ /* `anchor` is the button the pointer is on (or about to rest on, while
17574
+ `timer` runs); `shown` is the one whose title the card has taken. */
17575
+ let anchor = null
17576
+ let shown = null
17577
+ let takenTitle = null
17578
+ let timer = null
17579
+
17580
+ const restore = () => {
17581
+ if (shown === null) return
17582
+ /* React may have rewritten the attribute while the card was up (toggle
17583
+ buttons swap their copy): only put the taken text back when nothing
17584
+ else has claimed the slot, so a fresh label is never clobbered. */
17585
+ try {
17586
+ if (shown.getAttribute('title') === null && takenTitle !== null) shown.setAttribute('title', takenTitle)
17587
+ } catch (error) { void error }
17588
+ shown = null
17589
+ takenTitle = null
17590
+ }
17591
+
17592
+ const hide = () => {
17593
+ if (timer !== null) { clearTimeout(timer); timer = null }
17594
+ anchor = null
17595
+ restore()
17596
+ card.removeAttribute('data-show')
17597
+ card.setAttribute('aria-hidden', 'true')
17598
+ card.textContent = ''
17599
+ }
17600
+
17601
+ /* Viewport coordinates on a body child: no panel containment to fight. */
17602
+ const place = () => {
17603
+ const rect = anchor.getBoundingClientRect()
17604
+ const width = card.offsetWidth
17605
+ const height = card.offsetHeight
17606
+ const viewportW = window.innerWidth || document.documentElement.clientWidth || 0
17607
+ const viewportH = window.innerHeight || document.documentElement.clientHeight || 0
17608
+ /* The vertical action rail is a 24px column on the panel's left edge:
17609
+ a card under an icon there would cover the icons below it, so this one
17610
+ sits to its right and follows the row. (The horizontal rail uses
17611
+ `.dig-rail-row`, so the `closest` query only matches the vertical
17612
+ seat — top rows keep the below-the-button placement.) */
17613
+ if (anchor.closest('.dig-rail') !== null) {
17614
+ let left = rect.left + rect.width + TIP_GAP
17615
+ if (left + width > viewportW - 4) left = rect.left - TIP_GAP - width
17616
+ if (left < 4) left = 4
17617
+ let top = rect.top + (rect.height - height) / 2
17618
+ if (top + height > viewportH - 4) top = viewportH - height - 4
17619
+ if (top < 4) top = 4
17620
+ card.style.left = Math.round(left) + 'px'
17621
+ card.style.top = Math.round(top) + 'px'
17622
+ return
17623
+ }
17624
+ /* Below the anchor; flip above when the bottom edge would clip. */
17625
+ let top = rect.bottom + TIP_GAP
17626
+ if (top + height > viewportH - 4) top = rect.top - height - TIP_GAP
17627
+ if (top < 4) top = 4
17628
+ let left = rect.left + (rect.width - width) / 2
17629
+ if (left + width > viewportW - 4) left = viewportW - width - 4
17630
+ if (left < 4) left = 4
17631
+ card.style.left = Math.round(left) + 'px'
17632
+ card.style.top = Math.round(top) + 'px'
17633
+ }
17634
+
17635
+ const reveal = () => {
17636
+ timer = null
17637
+ if (anchor === null) return
17638
+ const label = anchor.getAttribute('title')
17639
+ if (label === null || label === '') { hide(); return }
17640
+ takenTitle = label
17641
+ try { anchor.removeAttribute('title') } catch (error) { void error }
17642
+ shown = anchor
17643
+ card.textContent = label
17644
+ card.setAttribute('data-show', 'true')
17645
+ card.removeAttribute('aria-hidden')
17646
+ place()
17647
+ }
17648
+
17649
+ /* Whatever sits under the pointer, not whatever received the event: a
17650
+ disabled button still hit-tests (rail icons), and the SVG glyph inside
17651
+ the button is usually the element under the cursor. `closest` walks up
17652
+ to the button. The label is read here, but the anchor's own copy is
17653
+ already taken while its card is up — so the shown button answers from
17654
+ the saved text, otherwise it would stop counting as a candidate the
17655
+ moment the card opened. */
17656
+ const buttonAt = (x, y) => {
17657
+ if (typeof document.elementFromPoint !== 'function') return null
17658
+ let hit = null
17659
+ try { hit = document.elementFromPoint(x, y) } catch (error) { void error; return null }
17660
+ if (hit === null || hit === undefined || typeof hit.closest !== 'function') return null
17661
+ if (hit.closest(TIP_ROOTS) === null) return null
17662
+ return hit.closest('button, [role="button"]')
17663
+ }
17664
+
17665
+ const labelOf = (button) => {
17666
+ if (button === shown) return takenTitle
17667
+ const label = button.getAttribute('title')
17668
+ return label === null || label === '' ? null : label
17669
+ }
17670
+
17671
+ const onPointerMove = (event) => {
17672
+ if (event !== null && event !== undefined && event.pointerType === 'touch') return
17673
+ /* A drag is not a hover: reordering rail actions, dragging a divider or
17674
+ selecting rows all sweep across buttons with a pressed pointer, and
17675
+ no card should pop up under the drag. */
17676
+ if (event !== null && event !== undefined && typeof event.buttons === 'number' && event.buttons !== 0) {
17677
+ if (anchor !== null || timer !== null || shown !== null) hide()
17678
+ return
17679
+ }
17680
+ const button = buttonAt(event === null || event === undefined ? 0 : event.clientX, event === null || event === undefined ? 0 : event.clientY)
17681
+ if (button === null || labelOf(button) === null) {
17682
+ if (anchor !== null || timer !== null || shown !== null) hide()
17683
+ return
17684
+ }
17685
+ if (button === anchor) return
17686
+ /* Another button: the old card (and the title it took) goes now, the
17687
+ new one waits out the grace period — sweeping the rail must not
17688
+ flash a card per icon. */
17689
+ if (timer !== null) { clearTimeout(timer); timer = null }
17690
+ restore()
17691
+ card.removeAttribute('data-show')
17692
+ anchor = button
17693
+ timer = setTimeout(reveal, TIP_DELAY_MS)
17694
+ }
17695
+
17696
+ document.addEventListener('pointermove', onPointerMove, { passive: true })
17697
+ document.addEventListener('pointerdown', hide, true)
17698
+ document.addEventListener('pointerleave', hide)
17699
+ document.addEventListener('wheel', hide, { passive: true, capture: true })
17700
+ /* Scroll and resize move the anchor under a card that cannot follow it:
17701
+ the card would lie about where the button is. Drop it and let the next
17702
+ pointer move re-open it. */
17703
+ window.addEventListener('scroll', hide, true)
17704
+ window.addEventListener('resize', hide)
17705
+ window.addEventListener('blur', hide)
17706
+
17707
+ return () => {
17708
+ hide()
17709
+ document.removeEventListener('pointermove', onPointerMove)
17710
+ document.removeEventListener('pointerdown', hide, true)
17711
+ document.removeEventListener('pointerleave', hide)
17712
+ document.removeEventListener('wheel', hide, { capture: true })
17713
+ window.removeEventListener('scroll', hide, true)
17714
+ window.removeEventListener('resize', hide)
17715
+ window.removeEventListener('blur', hide)
17716
+ card.remove()
17717
+ }
17718
+ }
17719
+
17380
17720
  function apply(ctx) {
17381
17721
  const style = document.createElement('style')
17382
17722
  style.setAttribute('data-dsh-ide-git', '')
@@ -17384,6 +17724,11 @@ window.__ModuleLoader__.load({
17384
17724
  document.head.appendChild(style)
17385
17725
  ctx.effect(() => () => { style.remove() }, 'dsh-ide-git: panel styles')
17386
17726
 
17727
+ /* Icon affordances explain themselves on hover wherever this plugin
17728
+ draws one — the panel in any seat, the composer pill, the diff
17729
+ preview. One document-level card covers them all. */
17730
+ ctx.effect(() => installHoverTips(), 'dsh-ide-git: icon tooltips')
17731
+
17387
17732
  const t = translatorOf(ctx)
17388
17733
 
17389
17734
  /* DSH's own locale registry: bind this plugin's namespace so host-side