dsh-ide-git 0.13.2 → 0.13.3

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
@@ -34,6 +34,12 @@ window.__ModuleLoader__.load({
34
34
  without a frame loop (the SSR self-check's shims) get the plain effect —
35
35
  there is no paint to sync with there, and no server warning either. */
36
36
  const useMeasureLayoutEffect = typeof requestAnimationFrame === 'function' ? useLayoutEffect : useEffect
37
+ /* Host-rendering reuse (v0.14): the file-type glyphs and the diff block come
38
+ from the platform seed table, so a plain require always hits — but a host
39
+ that predates the seed entry must degrade to the hand-drawn rows instead
40
+ of throwing at load time. Every consumer checks for null first. */
41
+ let UIPrimitives = null
42
+ try { UIPrimitives = require('@deepseek-ai/dsh-client-ui-primitives') } catch (error) { void error }
37
43
 
38
44
  /* API base is MOUNT-RELATIVE (no leading slash). dsh 0.1.7 serves the
39
45
  shell with <base href="./">, so document.baseURI is the mount the page
@@ -146,6 +152,8 @@ window.__ModuleLoader__.load({
146
152
  'action.resetHard': '重置到此(丢弃更改)',
147
153
  'action.copyHash': '复制修订号',
148
154
  'action.details': '提交详情',
155
+ 'menu.sendToChat': '发送到对话',
156
+ 'menu.sendFilePathToChat': '发送文件路径到对话',
149
157
  'history.empty': '这个仓库还没有提交',
150
158
  'history.loadMore': '加载更多',
151
159
  'history.filter': '文本或哈希',
@@ -157,6 +165,11 @@ window.__ModuleLoader__.load({
157
165
  'pane.resize': '拖动调整{name}尺寸,双击复位,方向键微调',
158
166
  'diff.binary': '二进制文件',
159
167
  'diff.loading': '正在加载差异...',
168
+ 'diff.codeLabel': '差异',
169
+ 'diff.wrapLabel': '自动换行',
170
+ 'diff.unwrapLabel': '按源列宽显示',
171
+ 'diff.copy': '复制',
172
+ 'diff.copied': '已复制',
160
173
  'toolbar.refresh': '刷新',
161
174
  'toolbar.newBranch': '新建分支',
162
175
  'toolbar.fetch': '抓取(Fetch)',
@@ -223,6 +236,8 @@ window.__ModuleLoader__.load({
223
236
  'toast.discarded': '已丢弃 {path} 的更改',
224
237
  'toast.discardNoUndo': '已丢弃 {path} 的更改(文件过大或过多,未保留撤回)',
225
238
  'toast.restored': '已恢复',
239
+ 'toast.sentToChat': '已发送到对话框',
240
+ 'toast.sendCopied': '无法发送,已复制到剪贴板',
226
241
  'undo.menu': '最近可撤回的操作',
227
242
  'undo.empty': '现在没有可撤回的操作',
228
243
  'undo.branch': '分支',
@@ -337,6 +352,8 @@ window.__ModuleLoader__.load({
337
352
  'action.resetHard': 'Reset here (discard changes)',
338
353
  'action.copyHash': 'Copy revision number',
339
354
  'action.details': 'Commit details',
355
+ 'menu.sendToChat': 'Send to chat',
356
+ 'menu.sendFilePathToChat': 'Send file path to chat',
340
357
  'history.empty': 'This repository has no commits yet',
341
358
  'history.loadMore': 'Load more',
342
359
  'history.filter': 'Text or hash',
@@ -348,6 +365,11 @@ window.__ModuleLoader__.load({
348
365
  'pane.resize': 'Drag to resize {name}: double-click resets, arrow keys nudge',
349
366
  'diff.binary': 'Binary file',
350
367
  'diff.loading': 'Loading diff...',
368
+ 'diff.codeLabel': 'Diff',
369
+ 'diff.wrapLabel': 'Wrap lines',
370
+ 'diff.unwrapLabel': 'Keep source columns',
371
+ 'diff.copy': 'Copy',
372
+ 'diff.copied': 'Copied',
351
373
  'toolbar.refresh': 'Refresh',
352
374
  'toolbar.newBranch': 'New branch',
353
375
  'toolbar.fetch': 'Fetch',
@@ -414,6 +436,8 @@ window.__ModuleLoader__.load({
414
436
  'toast.discarded': 'Discarded changes in {path}',
415
437
  'toast.discardNoUndo': 'Discarded changes in {path} (too large to keep an undo)',
416
438
  'toast.restored': 'Restored',
439
+ 'toast.sentToChat': 'Sent to the chat box',
440
+ 'toast.sendCopied': 'Could not send — copied to the clipboard instead',
417
441
  'undo.menu': 'Recently deleted (undo)',
418
442
  'undo.empty': 'Nothing to undo right now',
419
443
  'undo.branch': 'Branch',
@@ -536,6 +560,8 @@ window.__ModuleLoader__.load({
536
560
  'action.resetHard': '重置到此(丟棄變更)',
537
561
  'action.copyHash': '複製修訂號',
538
562
  'action.details': '提交詳情',
563
+ 'menu.sendToChat': '傳送到對話',
564
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
539
565
  'history.empty': '呢個存放庫仲未有提交',
540
566
  'history.loadMore': '載入更多',
541
567
  'history.filter': '文字或雜湊',
@@ -547,6 +573,11 @@ window.__ModuleLoader__.load({
547
573
  'pane.resize': '拖動調整{name}尺寸,雙擊復位,方向鍵微調',
548
574
  'diff.binary': '二進位檔案',
549
575
  'diff.loading': '正在載入差異...',
576
+ 'diff.codeLabel': '差異',
577
+ 'diff.wrapLabel': '自動換行',
578
+ 'diff.unwrapLabel': '按原始欄寬顯示',
579
+ 'diff.copy': '複製',
580
+ 'diff.copied': '已複製',
550
581
  'toolbar.refresh': '重新整理',
551
582
  'toolbar.newBranch': '新建分支',
552
583
  'toolbar.fetch': '抓取',
@@ -613,6 +644,8 @@ window.__ModuleLoader__.load({
613
644
  'toast.discarded': '已丟棄 {path} 嘅變更',
614
645
  'toast.discardNoUndo': '已丟棄 {path} 嘅變更(太大,冇保留撤回)',
615
646
  'toast.restored': '已還原',
647
+ 'toast.sentToChat': '已傳送到對話框',
648
+ 'toast.sendCopied': '無法傳送,已複製到剪貼板',
616
649
  'undo.menu': '最近刪除(撤回)',
617
650
  'undo.empty': '暫時冇嘢可以撤回',
618
651
  'undo.branch': '分支',
@@ -727,6 +760,8 @@ window.__ModuleLoader__.load({
727
760
  'action.resetHard': '重置到此(丟棄變更)',
728
761
  'action.copyHash': '複製修訂編號',
729
762
  'action.details': '提交詳情',
763
+ 'menu.sendToChat': '傳送至對話',
764
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
730
765
  'history.empty': '這個版本庫還沒有提交',
731
766
  'history.loadMore': '載入更多',
732
767
  'history.filter': '文字或雜湊',
@@ -738,6 +773,11 @@ window.__ModuleLoader__.load({
738
773
  'pane.resize': '拖曳調整{name}尺寸,雙擊重設,方向鍵微調',
739
774
  'diff.binary': '二進位檔案',
740
775
  'diff.loading': '正在載入差異...',
776
+ 'diff.codeLabel': '差異',
777
+ 'diff.wrapLabel': '自動換行',
778
+ 'diff.unwrapLabel': '依原始欄寬顯示',
779
+ 'diff.copy': '複製',
780
+ 'diff.copied': '已複製',
741
781
  'toolbar.refresh': '重新整理',
742
782
  'toolbar.newBranch': '新建分支',
743
783
  'toolbar.fetch': '抓取',
@@ -804,6 +844,8 @@ window.__ModuleLoader__.load({
804
844
  'toast.discarded': '已丟棄 {path} 的變更',
805
845
  'toast.discardNoUndo': '已丟棄 {path} 的變更(太大,未保留撤回)',
806
846
  'toast.restored': '已還原',
847
+ 'toast.sentToChat': '已傳送至對話框',
848
+ 'toast.sendCopied': '無法傳送,已複製到剪貼簿',
807
849
  'undo.menu': '最近刪除(撤回)',
808
850
  'undo.empty': '目前沒有可撤回的項目',
809
851
  'undo.branch': '分支',
@@ -918,6 +960,8 @@ window.__ModuleLoader__.load({
918
960
  'action.resetHard': '重置到此(丟棄變更)',
919
961
  'action.copyHash': '複製修訂號',
920
962
  'action.details': '提交詳情',
963
+ 'menu.sendToChat': '傳送至對話',
964
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
921
965
  'history.empty': '呢個存放庫仲未有提交',
922
966
  'history.loadMore': '載入更多',
923
967
  'history.filter': '文字或雜湊',
@@ -929,6 +973,11 @@ window.__ModuleLoader__.load({
929
973
  'pane.resize': '拖動調整{name}尺寸,雙擊復位,方向鍵微調',
930
974
  'diff.binary': '二進位檔案',
931
975
  'diff.loading': '正在載入差異...',
976
+ 'diff.codeLabel': '差異',
977
+ 'diff.wrapLabel': '自動換行',
978
+ 'diff.unwrapLabel': '依原始欄寬顯示',
979
+ 'diff.copy': '複製',
980
+ 'diff.copied': '已複製',
932
981
  'toolbar.refresh': '重新整理',
933
982
  'toolbar.newBranch': '新建分支',
934
983
  'toolbar.fetch': '抓取',
@@ -995,6 +1044,8 @@ window.__ModuleLoader__.load({
995
1044
  'toast.discarded': '已丟棄 {path} 嘅變更',
996
1045
  'toast.discardNoUndo': '已丟棄 {path} 嘅變更(太大,冇保留撤回)',
997
1046
  'toast.restored': '已還原',
1047
+ 'toast.sentToChat': '已傳送至對話框',
1048
+ 'toast.sendCopied': '無法傳送,已複製到剪貼簿',
998
1049
  'undo.menu': '最近刪除(撤回)',
999
1050
  'undo.empty': '暫時冇嘢可以撤回',
1000
1051
  'undo.branch': '分支',
@@ -1109,6 +1160,8 @@ window.__ModuleLoader__.load({
1109
1160
  'action.resetHard': 'ここにリセット(変更を破棄)',
1110
1161
  'action.copyHash': 'リビジョン番号をコピー',
1111
1162
  'action.details': 'コミットの詳細',
1163
+ 'menu.sendToChat': 'チャットへ送る',
1164
+ 'menu.sendFilePathToChat': 'ファイルパスをチャットへ送る',
1112
1165
  'history.empty': 'このリポジトリにはまだコミットがありません',
1113
1166
  'history.loadMore': 'さらに読み込む',
1114
1167
  'history.filter': 'テキストまたはハッシュ',
@@ -1120,6 +1173,11 @@ window.__ModuleLoader__.load({
1120
1173
  'pane.resize': '{name}のサイズをドラッグで調整(ダブルクリックでリセット、矢印キーで微調整)',
1121
1174
  'diff.binary': 'バイナリファイル',
1122
1175
  'diff.loading': '差分を読み込み中...',
1176
+ 'diff.codeLabel': '差分',
1177
+ 'diff.wrapLabel': '折り返し',
1178
+ 'diff.unwrapLabel': 'ソースの列幅を維持',
1179
+ 'diff.copy': 'コピー',
1180
+ 'diff.copied': 'コピーしました',
1123
1181
  'toolbar.refresh': '更新',
1124
1182
  'toolbar.newBranch': '新しいブランチ',
1125
1183
  'toolbar.fetch': 'フェッチ',
@@ -1186,6 +1244,8 @@ window.__ModuleLoader__.load({
1186
1244
  'toast.discarded': '{path} の変更を破棄しました',
1187
1245
  'toast.discardNoUndo': '{path} の変更を破棄しました(大きすぎるため取り消しは保持していません)',
1188
1246
  'toast.restored': '復元しました',
1247
+ 'toast.sentToChat': '入力欄に送りました',
1248
+ 'toast.sendCopied': '送信できなかったためクリップボードにコピーしました',
1189
1249
  'undo.menu': '最近の削除(元に戻す)',
1190
1250
  'undo.empty': '今すぐ元に戻せるものはありません',
1191
1251
  'undo.branch': 'ブランチ',
@@ -1300,6 +1360,8 @@ window.__ModuleLoader__.load({
1300
1360
  'action.resetHard': '여기로 리셋(변경 버림)',
1301
1361
  'action.copyHash': '리비전 번호 복사',
1302
1362
  'action.details': '커밋 상세',
1363
+ 'menu.sendToChat': '대화로 보내기',
1364
+ 'menu.sendFilePathToChat': '파일 경로를 대화로 보내기',
1303
1365
  'history.empty': '이 저장소에는 아직 커밋이 없습니다',
1304
1366
  'history.loadMore': '더 불러오기',
1305
1367
  'history.filter': '텍스트 또는 해시',
@@ -1311,6 +1373,11 @@ window.__ModuleLoader__.load({
1311
1373
  'pane.resize': '{name} 크기를 드래그해 조절(더블클릭 초기화, 화살표 키 미세 조정)',
1312
1374
  'diff.binary': '바이너리 파일',
1313
1375
  'diff.loading': '차이 불러오는 중...',
1376
+ 'diff.codeLabel': '차이',
1377
+ 'diff.wrapLabel': '줄 바꿈',
1378
+ 'diff.unwrapLabel': '원본 열 너비 유지',
1379
+ 'diff.copy': '복사',
1380
+ 'diff.copied': '복사됨',
1314
1381
  'toolbar.refresh': '새로 고침',
1315
1382
  'toolbar.newBranch': '새 브랜치',
1316
1383
  'toolbar.fetch': '페치',
@@ -1377,6 +1444,8 @@ window.__ModuleLoader__.load({
1377
1444
  'toast.discarded': '{path}의 변경 사항을 버렸습니다',
1378
1445
  'toast.discardNoUndo': '{path}의 변경 사항을 버렸습니다(너무 커서 되돌리기를 보관하지 않음)',
1379
1446
  'toast.restored': '복원했습니다',
1447
+ 'toast.sentToChat': '입력창에 보냈습니다',
1448
+ 'toast.sendCopied': '보낼 수 없어 클립보드에 복사했습니다',
1380
1449
  'undo.menu': '최근 삭제(되돌리기)',
1381
1450
  'undo.empty': '지금 되돌릴 항목이 없습니다',
1382
1451
  'undo.branch': '브랜치',
@@ -1491,6 +1560,8 @@ window.__ModuleLoader__.load({
1491
1560
  'action.resetHard': 'Hierher zurücksetzen (Änderungen verwerfen)',
1492
1561
  'action.copyHash': 'Revisionsnummer kopieren',
1493
1562
  'action.details': 'Commit-Details',
1563
+ 'menu.sendToChat': 'Im Chat senden',
1564
+ 'menu.sendFilePathToChat': 'Dateipfad im Chat senden',
1494
1565
  'history.empty': 'Dieses Repository hat noch keine Commits',
1495
1566
  'history.loadMore': 'Mehr laden',
1496
1567
  'history.filter': 'Text oder Hash',
@@ -1502,6 +1573,11 @@ window.__ModuleLoader__.load({
1502
1573
  'pane.resize': 'Größe von {name} per Ziehen ändern (Doppelklick setzt zurück, Pfeiltasten justieren)',
1503
1574
  'diff.binary': 'Binärdatei',
1504
1575
  'diff.loading': 'Diff wird geladen...',
1576
+ 'diff.codeLabel': 'Diff',
1577
+ 'diff.wrapLabel': 'Zeilen umbrechen',
1578
+ 'diff.unwrapLabel': 'Quellspalten beibehalten',
1579
+ 'diff.copy': 'Kopieren',
1580
+ 'diff.copied': 'Kopiert',
1505
1581
  'toolbar.refresh': 'Aktualisieren',
1506
1582
  'toolbar.newBranch': 'Neuer Branch',
1507
1583
  'toolbar.fetch': 'Fetch',
@@ -1568,6 +1644,8 @@ window.__ModuleLoader__.load({
1568
1644
  'toast.discarded': 'Änderungen in {path} verworfen',
1569
1645
  'toast.discardNoUndo': 'Änderungen in {path} verworfen (zu groß für ein Rückgängig)',
1570
1646
  'toast.restored': 'Wiederhergestellt',
1647
+ 'toast.sentToChat': 'In das Eingabefeld eingefügt',
1648
+ 'toast.sendCopied': 'Senden nicht möglich – stattdessen in die Zwischenablage kopiert',
1571
1649
  'undo.menu': 'Zuletzt gelöscht (rückgängig)',
1572
1650
  'undo.empty': 'Derzeit nichts rückgängig zu machen',
1573
1651
  'undo.branch': 'Branch',
@@ -1682,6 +1760,8 @@ window.__ModuleLoader__.load({
1682
1760
  'action.resetHard': 'Réinitialiser ici (abandonner les modifications)',
1683
1761
  'action.copyHash': 'Copier le numéro de révision',
1684
1762
  'action.details': 'Détails du commit',
1763
+ 'menu.sendToChat': 'Envoyer dans la conversation',
1764
+ 'menu.sendFilePathToChat': 'Envoyer le chemin du fichier dans la conversation',
1685
1765
  'history.empty': 'Ce dépôt n\'a pas encore de commit',
1686
1766
  'history.loadMore': 'Charger plus',
1687
1767
  'history.filter': 'Texte ou empreinte',
@@ -1693,6 +1773,11 @@ window.__ModuleLoader__.load({
1693
1773
  'pane.resize': 'Glisser pour redimensionner {name} (double-clic pour réinitialiser, flèches pour ajuster)',
1694
1774
  'diff.binary': 'Fichier binaire',
1695
1775
  'diff.loading': 'Chargement du diff...',
1776
+ 'diff.codeLabel': 'Diff',
1777
+ 'diff.wrapLabel': 'Renvoyer à la ligne',
1778
+ 'diff.unwrapLabel': 'Conserver les colonnes de la source',
1779
+ 'diff.copy': 'Copier',
1780
+ 'diff.copied': 'Copié',
1696
1781
  'toolbar.refresh': 'Rafraîchir',
1697
1782
  'toolbar.newBranch': 'Nouvelle branche',
1698
1783
  'toolbar.fetch': 'Récupérer',
@@ -1759,6 +1844,8 @@ window.__ModuleLoader__.load({
1759
1844
  'toast.discarded': 'Modifications de {path} abandonnées',
1760
1845
  'toast.discardNoUndo': 'Modifications de {path} abandonnées (trop volumineuses pour être annulées)',
1761
1846
  'toast.restored': 'Restauré',
1847
+ 'toast.sentToChat': 'Envoyé dans la zone de saisie',
1848
+ 'toast.sendCopied': 'Envoi impossible — copié dans le presse-papiers à la place',
1762
1849
  'undo.menu': 'Supprimés récemment (annuler)',
1763
1850
  'undo.empty': 'Rien à annuler pour le moment',
1764
1851
  'undo.branch': 'Branche',
@@ -1873,6 +1960,8 @@ window.__ModuleLoader__.load({
1873
1960
  'action.resetHard': 'Сбросить сюда (отбросить изменения)',
1874
1961
  'action.copyHash': 'Скопировать номер ревизии',
1875
1962
  'action.details': 'Сведения о коммите',
1963
+ 'menu.sendToChat': 'Отправить в чат',
1964
+ 'menu.sendFilePathToChat': 'Отправить путь к файлу в чат',
1876
1965
  'history.empty': 'В этом репозитории ещё нет коммитов',
1877
1966
  'history.loadMore': 'Загрузить ещё',
1878
1967
  'history.filter': 'Текст или хеш',
@@ -1884,6 +1973,11 @@ window.__ModuleLoader__.load({
1884
1973
  'pane.resize': 'Перетащите, чтобы изменить размер {name} (двойной клик — сброс, стрелки — точная настройка)',
1885
1974
  'diff.binary': 'Двоичный файл',
1886
1975
  'diff.loading': 'Загрузка различий...',
1976
+ 'diff.codeLabel': 'Различия',
1977
+ 'diff.wrapLabel': 'Переносить строки',
1978
+ 'diff.unwrapLabel': 'Сохранять исходные колонки',
1979
+ 'diff.copy': 'Копировать',
1980
+ 'diff.copied': 'Скопировано',
1887
1981
  'toolbar.refresh': 'Обновить',
1888
1982
  'toolbar.newBranch': 'Новая ветка',
1889
1983
  'toolbar.fetch': 'Получить',
@@ -1950,6 +2044,8 @@ window.__ModuleLoader__.load({
1950
2044
  'toast.discarded': 'Изменения в {path} отменены',
1951
2045
  'toast.discardNoUndo': 'Изменения в {path} отменены (слишком велики, отмена не сохранена)',
1952
2046
  'toast.restored': 'Восстановлено',
2047
+ 'toast.sentToChat': 'Отправлено в поле ввода',
2048
+ 'toast.sendCopied': 'Не удалось отправить — скопировано в буфер обмена',
1953
2049
  'undo.menu': 'Недавно удалённое (отменить)',
1954
2050
  'undo.empty': 'Сейчас нечего отменять',
1955
2051
  'undo.branch': 'Ветка',
@@ -2064,6 +2160,8 @@ window.__ModuleLoader__.load({
2064
2160
  'action.resetHard': 'Redefinir aqui (descartar alterações)',
2065
2161
  'action.copyHash': 'Copiar número da revisão',
2066
2162
  'action.details': 'Detalhes do commit',
2163
+ 'menu.sendToChat': 'Enviar para a conversa',
2164
+ 'menu.sendFilePathToChat': 'Enviar caminho do arquivo para a conversa',
2067
2165
  'history.empty': 'Este repositório ainda não tem commits',
2068
2166
  'history.loadMore': 'Carregar mais',
2069
2167
  'history.filter': 'Texto ou hash',
@@ -2075,6 +2173,11 @@ window.__ModuleLoader__.load({
2075
2173
  'pane.resize': 'Arraste para redimensionar {name} (duplo clique restaura, setas ajustam)',
2076
2174
  'diff.binary': 'Arquivo binário',
2077
2175
  'diff.loading': 'Carregando diff...',
2176
+ 'diff.codeLabel': 'Diff',
2177
+ 'diff.wrapLabel': 'Quebrar linhas',
2178
+ 'diff.unwrapLabel': 'Manter colunas do código',
2179
+ 'diff.copy': 'Copiar',
2180
+ 'diff.copied': 'Copiado',
2078
2181
  'toolbar.refresh': 'Atualizar',
2079
2182
  'toolbar.newBranch': 'Nova branch',
2080
2183
  'toolbar.fetch': 'Buscar',
@@ -2141,6 +2244,8 @@ window.__ModuleLoader__.load({
2141
2244
  'toast.discarded': 'Alterações em {path} descartadas',
2142
2245
  'toast.discardNoUndo': 'Alterações em {path} descartadas (grandes demais para desfazer)',
2143
2246
  'toast.restored': 'Restaurado',
2247
+ 'toast.sentToChat': 'Enviado para a caixa de entrada',
2248
+ 'toast.sendCopied': 'Não foi possível enviar — copiado para a área de transferência',
2144
2249
  'undo.menu': 'Excluídos recentemente (desfazer)',
2145
2250
  'undo.empty': 'Nada para desfazer agora',
2146
2251
  'undo.branch': 'Branch',
@@ -2255,6 +2360,8 @@ window.__ModuleLoader__.load({
2255
2360
  'action.resetHard': 'Ripristina qui (scarta le modifiche)',
2256
2361
  'action.copyHash': 'Copia il numero di revisione',
2257
2362
  'action.details': 'Dettagli del commit',
2363
+ 'menu.sendToChat': 'Invia alla conversazione',
2364
+ 'menu.sendFilePathToChat': 'Invia il percorso del file alla conversazione',
2258
2365
  'history.empty': 'Questo repository non ha ancora commit',
2259
2366
  'history.loadMore': 'Carica altro',
2260
2367
  'history.filter': 'Testo o hash',
@@ -2266,6 +2373,11 @@ window.__ModuleLoader__.load({
2266
2373
  'pane.resize': 'Trascina per ridimensionare {name} (doppio clic ripristina, frecce per regolare)',
2267
2374
  'diff.binary': 'File binario',
2268
2375
  'diff.loading': 'Caricamento del diff...',
2376
+ 'diff.codeLabel': 'Diff',
2377
+ 'diff.wrapLabel': 'A capo automatico',
2378
+ 'diff.unwrapLabel': 'Mantieni le colonne originali',
2379
+ 'diff.copy': 'Copia',
2380
+ 'diff.copied': 'Copiato',
2269
2381
  'toolbar.refresh': 'Aggiorna',
2270
2382
  'toolbar.newBranch': 'Nuovo branch',
2271
2383
  'toolbar.fetch': 'Fetch',
@@ -2332,6 +2444,8 @@ window.__ModuleLoader__.load({
2332
2444
  'toast.discarded': 'Modifiche in {path} scartate',
2333
2445
  'toast.discardNoUndo': 'Modifiche in {path} scartate (troppo grandi per essere annullate)',
2334
2446
  'toast.restored': 'Ripristinato',
2447
+ 'toast.sentToChat': 'Inviato nella casella di input',
2448
+ 'toast.sendCopied': 'Impossibile inviare — copiato negli appunti',
2335
2449
  'undo.menu': 'Eliminati di recente (annulla)',
2336
2450
  'undo.empty': 'Niente da annullare al momento',
2337
2451
  'undo.branch': 'Branch',
@@ -2446,6 +2560,8 @@ window.__ModuleLoader__.load({
2446
2560
  'action.resetHard': 'Hierheen resetten (wijzigingen weggooien)',
2447
2561
  'action.copyHash': 'Revisienummer kopiëren',
2448
2562
  'action.details': 'Commit-details',
2563
+ 'menu.sendToChat': 'Naar het gesprek verzenden',
2564
+ 'menu.sendFilePathToChat': 'Bestandspad naar het gesprek verzenden',
2449
2565
  'history.empty': 'Deze repository heeft nog geen commits',
2450
2566
  'history.loadMore': 'Meer laden',
2451
2567
  'history.filter': 'Tekst of hash',
@@ -2457,6 +2573,11 @@ window.__ModuleLoader__.load({
2457
2573
  'pane.resize': 'Sleep om {name} te vergroten (dubbelklik herstelt, pijltjes stellen fijn af)',
2458
2574
  'diff.binary': 'Binair bestand',
2459
2575
  'diff.loading': 'Diff laden...',
2576
+ 'diff.codeLabel': 'Diff',
2577
+ 'diff.wrapLabel': 'Regels afbreken',
2578
+ 'diff.unwrapLabel': 'Bronkolommen behouden',
2579
+ 'diff.copy': 'Kopiëren',
2580
+ 'diff.copied': 'Gekopieerd',
2460
2581
  'toolbar.refresh': 'Vernieuwen',
2461
2582
  'toolbar.newBranch': 'Nieuwe branch',
2462
2583
  'toolbar.fetch': 'Fetch',
@@ -2523,6 +2644,8 @@ window.__ModuleLoader__.load({
2523
2644
  'toast.discarded': 'Wijzigingen in {path} weggegooid',
2524
2645
  'toast.discardNoUndo': 'Wijzigingen in {path} weggegooid (te groot om ongedaan te maken)',
2525
2646
  'toast.restored': 'Hersteld',
2647
+ 'toast.sentToChat': 'In het invoerveld geplaatst',
2648
+ 'toast.sendCopied': 'Kon niet worden verzonden — in plaats daarvan naar het klembord gekopieerd',
2526
2649
  'undo.menu': 'Onlangs verwijderd (ongedaan maken)',
2527
2650
  'undo.empty': 'Nu niets om ongedaan te maken',
2528
2651
  'undo.branch': 'Branch',
@@ -2637,6 +2760,8 @@ window.__ModuleLoader__.load({
2637
2760
  'action.resetHard': 'Przywróć tutaj (odrzuć zmiany)',
2638
2761
  'action.copyHash': 'Kopiuj numer wersji',
2639
2762
  'action.details': 'Szczegóły commita',
2763
+ 'menu.sendToChat': 'Wyślij do rozmowy',
2764
+ 'menu.sendFilePathToChat': 'Wyślij ścieżkę pliku do rozmowy',
2640
2765
  'history.empty': 'To repozytorium nie ma jeszcze commitów',
2641
2766
  'history.loadMore': 'Wczytaj więcej',
2642
2767
  'history.filter': 'Tekst lub hash',
@@ -2648,6 +2773,11 @@ window.__ModuleLoader__.load({
2648
2773
  'pane.resize': 'Przeciągnij, aby zmienić rozmiar {name} (podwójne kliknięcie przywraca, strzałki korygują)',
2649
2774
  'diff.binary': 'Plik binarny',
2650
2775
  'diff.loading': 'Wczytywanie różnic...',
2776
+ 'diff.codeLabel': 'Różnice',
2777
+ 'diff.wrapLabel': 'Zawijaj wiersze',
2778
+ 'diff.unwrapLabel': 'Zachowaj kolumny źródła',
2779
+ 'diff.copy': 'Kopiuj',
2780
+ 'diff.copied': 'Skopiowano',
2651
2781
  'toolbar.refresh': 'Odśwież',
2652
2782
  'toolbar.newBranch': 'Nowa gałąź',
2653
2783
  'toolbar.fetch': 'Pobierz',
@@ -2714,6 +2844,8 @@ window.__ModuleLoader__.load({
2714
2844
  'toast.discarded': 'Odrzucono zmiany w {path}',
2715
2845
  'toast.discardNoUndo': 'Odrzucono zmiany w {path} (zbyt duże, aby zachować cofnięcie)',
2716
2846
  'toast.restored': 'Przywrócono',
2847
+ 'toast.sentToChat': 'Wysłano do pola wprowadzania',
2848
+ 'toast.sendCopied': 'Nie udało się wysłać — skopiowano do schowka',
2717
2849
  'undo.menu': 'Ostatnio usunięte (cofnij)',
2718
2850
  'undo.empty': 'Teraz nie ma czego cofać',
2719
2851
  'undo.branch': 'Gałąź',
@@ -2828,6 +2960,8 @@ window.__ModuleLoader__.load({
2828
2960
  'action.resetHard': 'Återställ hit (förkasta ändringar)',
2829
2961
  'action.copyHash': 'Kopiera revisionsnummer',
2830
2962
  'action.details': 'Commit-detaljer',
2963
+ 'menu.sendToChat': 'Skicka till samtalet',
2964
+ 'menu.sendFilePathToChat': 'Skicka filsökvägen till samtalet',
2831
2965
  'history.empty': 'Det här arkivet har inga commits ännu',
2832
2966
  'history.loadMore': 'Ladda mer',
2833
2967
  'history.filter': 'Text eller hash',
@@ -2839,6 +2973,11 @@ window.__ModuleLoader__.load({
2839
2973
  'pane.resize': 'Dra för att ändra storleken på {name} (dubbelklick återställer, piltangenter finjusterar)',
2840
2974
  'diff.binary': 'Binär fil',
2841
2975
  'diff.loading': 'Laddar diff...',
2976
+ 'diff.codeLabel': 'Skillnad',
2977
+ 'diff.wrapLabel': 'Radbryt rader',
2978
+ 'diff.unwrapLabel': 'Behåll kolumner från källan',
2979
+ 'diff.copy': 'Kopiera',
2980
+ 'diff.copied': 'Kopierat',
2842
2981
  'toolbar.refresh': 'Uppdatera',
2843
2982
  'toolbar.newBranch': 'Ny gren',
2844
2983
  'toolbar.fetch': 'Fetch',
@@ -2905,6 +3044,8 @@ window.__ModuleLoader__.load({
2905
3044
  'toast.discarded': 'Ändringar i {path} förkastade',
2906
3045
  'toast.discardNoUndo': 'Ändringar i {path} förkastade (för stora för att ångra)',
2907
3046
  'toast.restored': 'Återställd',
3047
+ 'toast.sentToChat': 'Skickat till inmatningsrutan',
3048
+ 'toast.sendCopied': 'Det gick inte att skicka – kopierade till urklipp istället',
2908
3049
  'undo.menu': 'Nyligen borttaget (ångra)',
2909
3050
  'undo.empty': 'Inget att ångra just nu',
2910
3051
  'undo.branch': 'Gren',
@@ -3019,6 +3160,8 @@ window.__ModuleLoader__.load({
3019
3160
  'action.resetHard': 'Buraya sıfırla (değişiklikleri at)',
3020
3161
  'action.copyHash': 'Revizyon numarasını kopyala',
3021
3162
  'action.details': 'Commit ayrıntıları',
3163
+ 'menu.sendToChat': 'Sohbete gönder',
3164
+ 'menu.sendFilePathToChat': 'Dosya yolunu sohbete gönder',
3022
3165
  'history.empty': 'Bu depoda henüz commit yok',
3023
3166
  'history.loadMore': 'Daha fazla yükle',
3024
3167
  'history.filter': 'Metin veya hash',
@@ -3030,6 +3173,11 @@ window.__ModuleLoader__.load({
3030
3173
  'pane.resize': '{name} boyutunu sürükleyerek ayarlayın (çift tıklama sıfırlar, ok tuşları ince ayar yapar)',
3031
3174
  'diff.binary': 'İkili dosya',
3032
3175
  'diff.loading': 'Fark yükleniyor...',
3176
+ 'diff.codeLabel': 'Fark',
3177
+ 'diff.wrapLabel': 'Satırları kaydır',
3178
+ 'diff.unwrapLabel': 'Kaynak sütunlarını koru',
3179
+ 'diff.copy': 'Kopyala',
3180
+ 'diff.copied': 'Kopyalandı',
3033
3181
  'toolbar.refresh': 'Yenile',
3034
3182
  'toolbar.newBranch': 'Yeni dal',
3035
3183
  'toolbar.fetch': 'Fetch',
@@ -3096,6 +3244,8 @@ window.__ModuleLoader__.load({
3096
3244
  'toast.discarded': '{path} içindeki değişiklikler atıldı',
3097
3245
  'toast.discardNoUndo': '{path} içindeki değişiklikler atıldı (geri alma için fazla büyük)',
3098
3246
  'toast.restored': 'Geri yüklendi',
3247
+ 'toast.sentToChat': 'Giriş kutusuna gönderildi',
3248
+ 'toast.sendCopied': 'Gönderilemedi – bunun yerine panoya kopyalandı',
3099
3249
  'undo.menu': 'Son silinenler (geri al)',
3100
3250
  'undo.empty': 'Şu anda geri alınacak bir şey yok',
3101
3251
  'undo.branch': 'Dal',
@@ -3210,6 +3360,8 @@ window.__ModuleLoader__.load({
3210
3360
  'action.resetHard': 'Reset ke sini (buang perubahan)',
3211
3361
  'action.copyHash': 'Salin nomor revisi',
3212
3362
  'action.details': 'Detail commit',
3363
+ 'menu.sendToChat': 'Kirim ke obrolan',
3364
+ 'menu.sendFilePathToChat': 'Kirim jalur berkas ke obrolan',
3213
3365
  'history.empty': 'Repositori ini belum punya commit',
3214
3366
  'history.loadMore': 'Muat lebih banyak',
3215
3367
  'history.filter': 'Teks atau hash',
@@ -3221,6 +3373,11 @@ window.__ModuleLoader__.load({
3221
3373
  'pane.resize': 'Seret untuk mengubah ukuran {name} (klik ganda mengatur ulang, tombol panah menyesuaikan)',
3222
3374
  'diff.binary': 'Berkas biner',
3223
3375
  'diff.loading': 'Memuat diff...',
3376
+ 'diff.codeLabel': 'Perbedaan',
3377
+ 'diff.wrapLabel': 'Lipat baris',
3378
+ 'diff.unwrapLabel': 'Pertahankan kolom sumber',
3379
+ 'diff.copy': 'Salin',
3380
+ 'diff.copied': 'Tersalin',
3224
3381
  'toolbar.refresh': 'Segarkan',
3225
3382
  'toolbar.newBranch': 'Branch baru',
3226
3383
  'toolbar.fetch': 'Fetch',
@@ -3287,6 +3444,8 @@ window.__ModuleLoader__.load({
3287
3444
  'toast.discarded': 'Perubahan di {path} dibuang',
3288
3445
  'toast.discardNoUndo': 'Perubahan di {path} dibuang (terlalu besar untuk diurungkan)',
3289
3446
  'toast.restored': 'Dipulihkan',
3447
+ 'toast.sentToChat': 'Terkirim ke kotak masukan',
3448
+ 'toast.sendCopied': 'Tidak dapat mengirim — disalin ke papan klip sebagai gantinya',
3290
3449
  'undo.menu': 'Baru dihapus (urungkan)',
3291
3450
  'undo.empty': 'Tidak ada yang bisa diurungkan sekarang',
3292
3451
  'undo.branch': 'Branch',
@@ -3401,6 +3560,8 @@ window.__ModuleLoader__.load({
3401
3560
  'action.resetHard': 'Đặt lại về đây (bỏ thay đổi)',
3402
3561
  'action.copyHash': 'Sao chép số hiệu bản sửa',
3403
3562
  'action.details': 'Chi tiết commit',
3563
+ 'menu.sendToChat': 'Gửi vào hội thoại',
3564
+ 'menu.sendFilePathToChat': 'Gửi đường dẫn tệp vào hội thoại',
3404
3565
  'history.empty': 'Kho này chưa có commit nào',
3405
3566
  'history.loadMore': 'Tải thêm',
3406
3567
  'history.filter': 'Văn bản hoặc hash',
@@ -3412,6 +3573,11 @@ window.__ModuleLoader__.load({
3412
3573
  'pane.resize': 'Kéo để đổi kích thước {name} (nhấp đúp để đặt lại, phím mũi tên để tinh chỉnh)',
3413
3574
  'diff.binary': 'Tệp nhị phân',
3414
3575
  'diff.loading': 'Đang tải diff...',
3576
+ 'diff.codeLabel': 'Khác biệt',
3577
+ 'diff.wrapLabel': 'Ngắt dòng',
3578
+ 'diff.unwrapLabel': 'Giữ độ rộng cột gốc',
3579
+ 'diff.copy': 'Sao chép',
3580
+ 'diff.copied': 'Đã sao chép',
3415
3581
  'toolbar.refresh': 'Làm mới',
3416
3582
  'toolbar.newBranch': 'Nhánh mới',
3417
3583
  'toolbar.fetch': 'Fetch',
@@ -3478,6 +3644,8 @@ window.__ModuleLoader__.load({
3478
3644
  'toast.discarded': 'Đã bỏ thay đổi trong {path}',
3479
3645
  'toast.discardNoUndo': 'Đã bỏ thay đổi trong {path} (quá lớn để giữ hoàn tác)',
3480
3646
  'toast.restored': 'Đã khôi phục',
3647
+ 'toast.sentToChat': 'Đã gửi vào ô nhập',
3648
+ 'toast.sendCopied': 'Không thể gửi — đã sao chép vào clipboard để thay thế',
3481
3649
  'undo.menu': 'Vừa xoá (hoàn tác)',
3482
3650
  'undo.empty': 'Hiện không có gì để hoàn tác',
3483
3651
  'undo.branch': 'Nhánh',
@@ -3592,6 +3760,8 @@ window.__ModuleLoader__.load({
3592
3760
  'action.resetHard': 'إعادة التعيين هنا (مع تجاهل التغييرات)',
3593
3761
  'action.copyHash': 'نسخ رقم المراجعة',
3594
3762
  'action.details': 'تفاصيل الالتزام',
3763
+ 'menu.sendToChat': 'إرسال إلى المحادثة',
3764
+ 'menu.sendFilePathToChat': 'إرسال مسار الملف إلى المحادثة',
3595
3765
  'history.empty': 'لا توجد التزامات في هذا المستودع بعد',
3596
3766
  'history.loadMore': 'تحميل المزيد',
3597
3767
  'history.filter': 'نص أو تجزئة',
@@ -3603,6 +3773,11 @@ window.__ModuleLoader__.load({
3603
3773
  'pane.resize': 'اسحب لتغيير حجم {name} (النقر المزدوج يعيد الضبط، ومفاتيح الأسهم للضبط الدقيق)',
3604
3774
  'diff.binary': 'ملف ثنائي',
3605
3775
  'diff.loading': 'جارٍ تحميل الفروق...',
3776
+ 'diff.codeLabel': 'الفروق',
3777
+ 'diff.wrapLabel': 'التفاف الأسطر',
3778
+ 'diff.unwrapLabel': 'الحفاظ على أعمدة المصدر',
3779
+ 'diff.copy': 'نسخ',
3780
+ 'diff.copied': 'تم النسخ',
3606
3781
  'toolbar.refresh': 'تحديث',
3607
3782
  'toolbar.newBranch': 'فرع جديد',
3608
3783
  'toolbar.fetch': 'جلب',
@@ -3669,6 +3844,8 @@ window.__ModuleLoader__.load({
3669
3844
  'toast.discarded': 'تم تجاهل التغييرات في {path}',
3670
3845
  'toast.discardNoUndo': 'تم تجاهل التغييرات في {path} (أكبر من أن يُحتفظ بالتراجع)',
3671
3846
  'toast.restored': 'تمت الاستعادة',
3847
+ 'toast.sentToChat': 'تم الإرسال إلى مربع الكتابة',
3848
+ 'toast.sendCopied': 'تعذّر الإرسال — تم النسخ إلى الحافظة بدلًا من ذلك',
3672
3849
  'undo.menu': 'المحذوف حديثًا (تراجع)',
3673
3850
  'undo.empty': 'لا شيء لتراجعه الآن',
3674
3851
  'undo.branch': 'فرع',
@@ -3783,6 +3960,8 @@ window.__ModuleLoader__.load({
3783
3960
  'action.resetHard': 'यहाँ रीसेट करें (परिवर्तन छोड़ें)',
3784
3961
  'action.copyHash': 'रिवीज़न संख्या कॉपी करें',
3785
3962
  'action.details': 'कमिट विवरण',
3963
+ 'menu.sendToChat': 'बातचीत में भेजें',
3964
+ 'menu.sendFilePathToChat': 'फ़ाइल पथ बातचीत में भेजें',
3786
3965
  'history.empty': 'इस रिपॉज़िटरी में अभी कोई कमिट नहीं है',
3787
3966
  'history.loadMore': 'और लोड करें',
3788
3967
  'history.filter': 'टेक्स्ट या हैश',
@@ -3794,6 +3973,11 @@ window.__ModuleLoader__.load({
3794
3973
  'pane.resize': '{name} का आकार बदलने के लिए खींचें (डबल-क्लिक रीसेट, तीर कुंजियाँ सूक्ष्म समायोजन)',
3795
3974
  'diff.binary': 'बाइनरी फ़ाइल',
3796
3975
  'diff.loading': 'अंतर लोड हो रहा है...',
3976
+ 'diff.codeLabel': 'अंतर',
3977
+ 'diff.wrapLabel': 'पंक्तियाँ रैप करें',
3978
+ 'diff.unwrapLabel': 'स्रोत कॉलम बनाए रखें',
3979
+ 'diff.copy': 'कॉपी करें',
3980
+ 'diff.copied': 'कॉपी हो गया',
3797
3981
  'toolbar.refresh': 'ताज़ा करें',
3798
3982
  'toolbar.newBranch': 'नई शाखा',
3799
3983
  'toolbar.fetch': 'फ़ेच',
@@ -3860,6 +4044,8 @@ window.__ModuleLoader__.load({
3860
4044
  'toast.discarded': '{path} में परिवर्तन छोड़े गए',
3861
4045
  'toast.discardNoUndo': '{path} में परिवर्तन छोड़े गए (पूर्ववत रखने के लिए बहुत बड़ा)',
3862
4046
  'toast.restored': 'बहाल किया गया',
4047
+ 'toast.sentToChat': 'इनपुट बॉक्स में भेज दिया गया',
4048
+ 'toast.sendCopied': 'भेजा नहीं जा सका — इसके बजाय क्लिपबोर्ड पर कॉपी कर दिया गया',
3863
4049
  'undo.menu': 'हाल में हटाए गए (पूर्ववत करें)',
3864
4050
  'undo.empty': 'अभी पूर्ववत करने के लिए कुछ नहीं',
3865
4051
  'undo.branch': 'शाखा',
@@ -3974,6 +4160,8 @@ window.__ModuleLoader__.load({
3974
4160
  'action.resetHard': 'รีเซ็ตมาที่นี่ (ทิ้งการเปลี่ยนแปลง)',
3975
4161
  'action.copyHash': 'คัดลอกเลขรีวิชัน',
3976
4162
  'action.details': 'รายละเอียดคอมมิต',
4163
+ 'menu.sendToChat': 'ส่งไปยังบทสนทนา',
4164
+ 'menu.sendFilePathToChat': 'ส่งพาธไฟล์ไปยังบทสนทนา',
3977
4165
  'history.empty': 'รีโพซิทอรีนี้ยังไม่มีคอมมิต',
3978
4166
  'history.loadMore': 'โหลดเพิ่ม',
3979
4167
  'history.filter': 'ข้อความหรือแฮช',
@@ -3985,6 +4173,11 @@ window.__ModuleLoader__.load({
3985
4173
  'pane.resize': 'ลากเพื่อปรับขนาด {name} (ดับเบิลคลิกเพื่อรีเซ็ต ปุ่มลูกศรปรับละเอียด)',
3986
4174
  'diff.binary': 'ไฟล์ไบนารี',
3987
4175
  'diff.loading': 'กำลังโหลดความต่าง...',
4176
+ 'diff.codeLabel': 'ความต่าง',
4177
+ 'diff.wrapLabel': 'ตัดบรรทัด',
4178
+ 'diff.unwrapLabel': 'คงความกว้างคอลัมน์เดิม',
4179
+ 'diff.copy': 'คัดลอก',
4180
+ 'diff.copied': 'คัดลอกแล้ว',
3988
4181
  'toolbar.refresh': 'รีเฟรช',
3989
4182
  'toolbar.newBranch': 'แบรนช์ใหม่',
3990
4183
  'toolbar.fetch': 'Fetch',
@@ -4051,6 +4244,8 @@ window.__ModuleLoader__.load({
4051
4244
  'toast.discarded': 'ทิ้งการเปลี่ยนแปลงใน {path} แล้ว',
4052
4245
  'toast.discardNoUndo': 'ทิ้งการเปลี่ยนแปลงใน {path} แล้ว (ใหญ่เกินกว่าจะเก็บไว้เลิกทำ)',
4053
4246
  'toast.restored': 'กู้คืนแล้ว',
4247
+ 'toast.sentToChat': 'ส่งไปที่กล่องป้อนข้อความแล้ว',
4248
+ 'toast.sendCopied': 'ส่งไม่สำเร็จ – คัดลอกไปยังคลิปบอร์ดแทนแล้ว',
4054
4249
  'undo.menu': 'ลบล่าสุด (เลิกทำ)',
4055
4250
  'undo.empty': 'ตอนนี้ไม่มีอะไรให้เลิกทำ',
4056
4251
  'undo.branch': 'แบรนช์',
@@ -4251,31 +4446,92 @@ window.__ModuleLoader__.load({
4251
4446
  is still essentially correct, so it seeds the states and a background
4252
4447
  refresh lands the fresh data a moment later. Pure functions; the panel
4253
4448
  only ever reads them at effect time and writes after a fetch. */
4254
- const PAINT_TTL_MS = 30_000
4449
+ /* Two TTLs, because the two seeds age very differently. The repos scan is
4450
+ the SLOW part (a multi-repo workspace walks directories) and its result
4451
+ barely changes, so its seed stays usable for 10 minutes: freshness is
4452
+ the background refresh's job — every mount refreshes anyway — the TTL
4453
+ only decides how old a seed may be to still paint the first screen
4454
+ (user report: coming back to the Git tab after a while re-ran the whole
4455
+ discovery). The first page (summary / branches / log) changes with every
4456
+ commit, so its seed keeps the short 30s window it always had. */
4457
+ const PAINT_REPOS_TTL_MS = 600_000
4458
+ const PAINT_PAGE_TTL_MS = 30_000
4255
4459
  const paintRepos = new Map() /* cwd -> { at, data } */
4256
4460
  const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
4257
4461
  const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
4258
4462
 
4463
+ /* The seed also survives a FULL PAGE RELOAD: sessionStorage holds the same
4464
+ maps for the lifetime of the tab, so reloading the page (the other way
4465
+ users "come back") paints from the seed instead of re-running the slow
4466
+ discovery. Hydrate runs once, lazily before the first read; every write
4467
+ mirrors the in-memory state back. All of it is best-effort: private
4468
+ mode, quota errors and corrupted payloads degrade to "no seed", never to
4469
+ an error — and never to a rollback of live state (seeding still only
4470
+ fills cold state, see the panel side below). */
4471
+ const PAINT_KEY = 'dsh-ide-git.paint.v1'
4472
+ let paintHydrated = false
4473
+
4474
+ function hydratePaint() {
4475
+ if (paintHydrated === true) return
4476
+ paintHydrated = true
4477
+ let parsed = null
4478
+ try {
4479
+ const raw = window.sessionStorage.getItem(PAINT_KEY)
4480
+ if (typeof raw === 'string' && raw !== '') parsed = JSON.parse(raw)
4481
+ } catch (error) { void error; return }
4482
+ if (parsed === null || typeof parsed !== 'object') return
4483
+ /* Entries already in memory stay untouched: a write that happened before
4484
+ the first read is always fresher than what the storage holds. */
4485
+ const feed = (map, stored) => {
4486
+ if (stored === null || typeof stored !== 'object') return
4487
+ let count = 0
4488
+ for (const key of Object.keys(stored)) {
4489
+ const entry = stored[key]
4490
+ if (map.has(key) || entry === null || typeof entry !== 'object' || typeof entry.at !== 'number') continue
4491
+ if (count >= 16) break
4492
+ map.set(key, entry)
4493
+ count += 1
4494
+ }
4495
+ }
4496
+ feed(paintRepos, parsed.repos)
4497
+ feed(paintPage, parsed.page)
4498
+ }
4499
+
4500
+ function persistPaint() {
4501
+ try {
4502
+ const pack = (map) => {
4503
+ const out = {}
4504
+ for (const [key, entry] of map) out[key] = entry
4505
+ return out
4506
+ }
4507
+ window.sessionStorage.setItem(PAINT_KEY, JSON.stringify({ repos: pack(paintRepos), page: pack(paintPage) }))
4508
+ } catch (error) { void error }
4509
+ }
4510
+
4259
4511
  function readPaintRepos(cwd) {
4512
+ hydratePaint()
4260
4513
  const entry = paintRepos.get(cwd)
4261
- if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4514
+ if (entry === undefined || Date.now() - entry.at > PAINT_REPOS_TTL_MS) return undefined
4262
4515
  return entry.data
4263
4516
  }
4264
4517
 
4265
4518
  function writePaintRepos(cwd, data) {
4266
4519
  paintRepos.set(cwd, { at: Date.now(), data: data })
4267
4520
  if (paintRepos.size > 16) paintRepos.delete(paintRepos.keys().next().value)
4521
+ persistPaint()
4268
4522
  }
4269
4523
 
4270
4524
  function readPaintPage(repoRoot) {
4525
+ hydratePaint()
4271
4526
  const entry = paintPage.get(repoRoot)
4272
- if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4527
+ if (entry === undefined || Date.now() - entry.at > PAINT_PAGE_TTL_MS) return undefined
4273
4528
  return entry
4274
4529
  }
4275
4530
 
4276
4531
  function writePaintPage(repoRoot, page) {
4277
4532
  paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
4278
4533
  if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
4534
+ persistPaint()
4279
4535
  }
4280
4536
 
4281
4537
  function rememberPaintRoot(cwd, repoRoot) {
@@ -4701,12 +4957,19 @@ window.__ModuleLoader__.load({
4701
4957
  is stored is a RATIO of the measured container: resizing the dock or the
4702
4958
  window rescales the panes instead of breaking the layout. */
4703
4959
  const PANES_KEY = 'dsh-ide-git.panes.v1'
4960
+ /* Every chrome can now move all three panes (the detail diff window used to
4961
+ be a fixed max-height:55% in the columns chrome — user report: it could
4962
+ not be resized). The DIFF pane is special: it is carved out of the middle
4963
+ pane, so it is sized by HEIGHT in every chrome, even where the other
4964
+ panes measure width (columns). */
4704
4965
  const PANE_CHROME_KEYS = {
4705
- columns: ['tree', 'changes'],
4966
+ columns: ['tree', 'changes', 'diff'],
4706
4967
  stack: ['tree', 'changes', 'diff'],
4707
4968
  compact: ['tree', 'diff'],
4708
4969
  }
4709
- /* Whether a key measures a height (stacked chromes) or a width (columns). */
4970
+ /* Whether a key measures a height (stacked chromes) or a width (columns).
4971
+ This is the chrome's DEFAULT for the tree/changes panes; the diff pane
4972
+ overrides it to height unconditionally (see paneGeometry). */
4710
4973
  const PANE_HEIGHT_CHROME = { columns: false, stack: true, compact: true }
4711
4974
  /* The pane a separator resizes, and the key its label is translated from. */
4712
4975
  const PANE_NAME_KEYS = { tree: 'toolbar.tree', changes: 'changes.title', diff: 'pane.diff' }
@@ -4715,6 +4978,7 @@ window.__ModuleLoader__.load({
4715
4978
  const PANE_LIMITS = {
4716
4979
  'columns:tree': { min: 140, max: 560 },
4717
4980
  'columns:changes': { min: 200, max: 640 },
4981
+ 'columns:diff': { min: 120, max: 900 },
4718
4982
  'stack:tree': { min: 100, max: 420 },
4719
4983
  'stack:changes': { min: 120, max: 460 },
4720
4984
  'stack:diff': { min: 120, max: 900 },
@@ -4723,10 +4987,12 @@ window.__ModuleLoader__.load({
4723
4987
  }
4724
4988
  /* Today's fixed sizes stay the default — and stay in the stylesheet: a pane
4725
4989
  the user never dragged keeps exactly the look it had before, while a
4726
- dragged one is remembered as a ratio. */
4990
+ dragged one is remembered as a ratio. The diff's 0.55 IS the stylesheet's
4991
+ max-height:55%, so an undragged window renders identically to before. */
4727
4992
  const PANE_DEFAULTS = {
4728
4993
  'columns:tree': { px: 200 },
4729
4994
  'columns:changes': { px: 290 },
4995
+ 'columns:diff': { ratio: 0.55 },
4730
4996
  'stack:tree': { ratio: 0.36 },
4731
4997
  'stack:changes': { ratio: 0.46 },
4732
4998
  'compact:tree': { ratio: 0.42 },
@@ -4829,9 +5095,14 @@ window.__ModuleLoader__.load({
4829
5095
  const effective = {}
4830
5096
  const limits = {}
4831
5097
  const sizeOf = (key) => (effective[key] === undefined ? 0 : effective[key])
5098
+ /* The diff is carved out of the middle pane, so it measures HEIGHT in
5099
+ every chrome — even in columns, where tree/changes run sideways and
5100
+ the chrome's own dimension is width. Its ratios therefore divide the
5101
+ body HEIGHT, whatever `reference` says. */
5102
+ const referenceOf = (key) => (key === 'diff' ? box.height : reference)
4832
5103
  for (const key of keys) {
4833
5104
  const spec = PANE_DEFAULTS[chrome + ':' + key]
4834
- const fallback = spec === undefined ? 0 : (spec.px === undefined ? spec.ratio * reference : spec.px)
5105
+ const fallback = spec === undefined ? 0 : (spec.px === undefined ? spec.ratio * referenceOf(key) : spec.px)
4835
5106
  const live = measured[key]
4836
5107
  effective[key] = typeof live === 'number' && Number.isFinite(live) && live > 0 ? live : fallback
4837
5108
  }
@@ -4846,7 +5117,14 @@ window.__ModuleLoader__.load({
4846
5117
  const current = sizeOf(key)
4847
5118
  let ceiling = spec.max
4848
5119
  if (key === 'diff') {
4849
- const main = space - sizeOf('changes') - sizeOf('tree') - PANE_GUTTER_PX
5120
+ /* The diff lives INSIDE the main pane, so its height budget is the
5121
+ main pane's height: the full body height in the columns chrome
5122
+ (the other panes run sideways there and share none of it), the
5123
+ body height minus the stacked siblings otherwise. Either way the
5124
+ history list keeps its own minimum. */
5125
+ const main = height
5126
+ ? space - sizeOf('changes') - sizeOf('tree') - PANE_GUTTER_PX
5127
+ : box.height
4850
5128
  ceiling = Math.min(ceiling, main - PANE_HISTORY_MIN_H)
4851
5129
  } else {
4852
5130
  let taken = 0
@@ -4868,7 +5146,7 @@ window.__ModuleLoader__.load({
4868
5146
  limits[key] = { min: spec.min, max: high }
4869
5147
  const ratio = ratios[key]
4870
5148
  if (typeof ratio === 'number' && Number.isFinite(ratio)) {
4871
- const px = Math.round(Math.min(Math.max(ratio * reference, spec.min), high))
5149
+ const px = Math.round(Math.min(Math.max(ratio * referenceOf(key), spec.min), high))
4872
5150
  overrides[key] = px
4873
5151
  effective[key] = px
4874
5152
  }
@@ -5126,6 +5404,120 @@ window.__ModuleLoader__.load({
5126
5404
  return E('svg', { className: 'dig-graph', width: width, height: height, viewBox: '0 0 ' + width + ' ' + height }, children)
5127
5405
  }
5128
5406
 
5407
+ /* ============================== composer send ============================== */
5408
+ /* "Send to chat": drop a reference chip / short text into the CURRENT
5409
+ session's composer, the way typing `@file` there would. The host channel
5410
+ is ui-conversation's SessionInput facade (research brief 2026-10-07):
5411
+ `ctx.get('conversation').input.for(ctx.get('sessions').scope(id))`. Every
5412
+ hop is probed — the services may be absent (a trimmed host) and `for`
5413
+ THROWS on a scope without a retained session generation — so a miss
5414
+ degrades to a clipboard copy with a toast, never a dead click.
5415
+
5416
+ Two spellings:
5417
+ - file/folder → insertReference(): a structured Lexical chip whose model
5418
+ serialization is the plain `@path` text (the same thing the @ menu
5419
+ inserts; source 'reference', appearance 'file'/'folder').
5420
+ - text (short hash, branch name, raw path) → the scoped
5421
+ `slash/input-insert-text` bail event, the machine entrance of the same
5422
+ pipeline. NOT `inputTriggers...openReference` — that one opens a
5423
+ preview and never touches the draft. */
5424
+
5425
+ /* `formatFileMention` (@deepseek-ai/dsh-file-reference/grammar) without the
5426
+ require: that package is not on the seed table, so the plugin inlines the
5427
+ grammar — trailing `/` for a directory, `@"path"` when whitespace is
5428
+ present (a quoted directory keeps its quote open), and `null` for paths
5429
+ the editor grammar cannot represent (control characters, quotes). */
5430
+ function fileMentionOf(relPath) {
5431
+ const isDir = relPath.slice(-1) === '/'
5432
+ if (/[\u0000-\u001f\u007f-\u009f"]/u.test(relPath) === true) return null
5433
+ if (/\s/u.test(relPath) === false) return '@' + relPath
5434
+ return isDir === true ? '@"' + relPath : '@"' + relPath + '"'
5435
+ }
5436
+
5437
+ /* Resolve { actx, input } for the current session, or null. `input.for`
5438
+ requires a RETAINED Session scope and throws otherwise — that is part of
5439
+ its contract (contract/input.ts), so the whole probe rides a try/catch. */
5440
+ function composerTargetOf(ctx, sessionId) {
5441
+ try {
5442
+ if (ctx === undefined || ctx === null || ctx.get === undefined) return null
5443
+ if (typeof sessionId !== 'string' || sessionId === '' || sessionId === 'default') return null
5444
+ const sessions = ctx.get('sessions')
5445
+ const conversation = ctx.get('conversation')
5446
+ if (sessions === undefined || sessions === null || conversation === undefined || conversation === null) return null
5447
+ if (typeof sessions.scope !== 'function' || conversation.input === undefined || conversation.input === null) return null
5448
+ const actx = sessions.scope(sessionId)
5449
+ if (actx === undefined || actx === null) return null
5450
+ const input = conversation.input.for(actx)
5451
+ if (input === undefined || input === null) return null
5452
+ return { actx: actx, input: input }
5453
+ } catch (error) { void error; return null }
5454
+ }
5455
+
5456
+ /* apply(actx, input, span) → boolean. The span is an EMPTY span at the draft
5457
+ tail in detect coordinates (state.draft is exactly the projection the CAS
5458
+ compares). A false answer is the user typing between our read and write —
5459
+ re-read once and retry (the brief's advice: one retry, no spinning). The
5460
+ busy phases (adjudicating/submitting) also answer false; that lands on the
5461
+ same clipboard fallback, which is the honest outcome. */
5462
+ function insertIntoComposer(ctx, sessionId, apply) {
5463
+ const target = composerTargetOf(ctx, sessionId)
5464
+ if (target === null) return false
5465
+ for (let attempt = 0; attempt < 2; attempt += 1) {
5466
+ let state
5467
+ try { state = target.input.state.getSnapshot() } catch (error) { void error; return false }
5468
+ if (state === undefined || state === null || typeof state.draft !== 'string' || typeof state.draftRev !== 'number') return false
5469
+ const span = { start: state.draft.length, end: state.draft.length, draftRev: state.draftRev }
5470
+ let applied = false
5471
+ try { applied = apply(target.actx, target.input, span) === true } catch (error) { void error; applied = false }
5472
+ if (applied === true) {
5473
+ try { target.input.focus() } catch (error) { void error }
5474
+ return true
5475
+ }
5476
+ }
5477
+ return false
5478
+ }
5479
+
5480
+ /* One single-file unified diff (what the host `diff` method returns) → the
5481
+ one DiffHunk DiffBlock renders, by replaying ` `/`-`/`+` lines back into
5482
+ the two sides. No `@@` at all (binary patches, mode-only changes) or a
5483
+ pathless header → null, and the caller keeps the hand-drawn rows. */
5484
+ function patchToHunk(patch) {
5485
+ const text = typeof patch === 'string' ? patch : ''
5486
+ if (text === '' || text.indexOf('@@') < 0) return null
5487
+ const lines = text.split('\n')
5488
+ let path = null
5489
+ const oldText = []
5490
+ const newText = []
5491
+ for (const line of lines) {
5492
+ if (line.indexOf('+++ ') === 0) {
5493
+ const name = line.slice(4).split('\t')[0]
5494
+ if (name !== '/dev/null') path = name.replace(/^b\//, '')
5495
+ continue
5496
+ }
5497
+ if (line.indexOf('--- ') === 0) {
5498
+ if (path === null) {
5499
+ const name = line.slice(4).split('\t')[0]
5500
+ if (name !== '/dev/null') path = name.replace(/^a\//, '')
5501
+ }
5502
+ continue
5503
+ }
5504
+ if (line.indexOf('@@') === 0) continue
5505
+ if (line.indexOf('+') === 0) { newText.push(line.slice(1)); continue }
5506
+ if (line.indexOf('-') === 0) { oldText.push(line.slice(1)); continue }
5507
+ if (line.indexOf('\\') === 0) continue
5508
+ if (line.indexOf(' ') === 0) { oldText.push(line.slice(1)); newText.push(line.slice(1)); continue }
5509
+ // diff --git / index / new file mode / rename from ...: metadata above
5510
+ // the first hunk; anything else after it would be malformed.
5511
+ continue
5512
+ }
5513
+ if (path === null || path === '') return null
5514
+ // A patch without body lines (mode-only, bare metadata) replays into an
5515
+ // empty hunk that DiffBlock draws as nothing at all — keep the hand-drawn
5516
+ // rows for those, they at least show what happened.
5517
+ if (oldText.length === 0 && newText.length === 0) return null
5518
+ return { path: path, oldText: oldText.length === 0 ? null : oldText.join('\n'), newText: newText.join('\n') }
5519
+ }
5520
+
5129
5521
  /* ============================== diff ============================== */
5130
5522
 
5131
5523
  function diffLines(patch) {
@@ -5156,11 +5548,47 @@ window.__ModuleLoader__.load({
5156
5548
  return out
5157
5549
  }
5158
5550
 
5551
+ /* DiffBlock's own 16-line cap would REPLACE scrolling with a fold button;
5552
+ this panel keeps the old contract (render everything, scroll inside the
5553
+ .dig-diff-pane max-height), so the cap is pushed out of reach and the
5554
+ fold labels below are placeholders that never render at this size. */
5555
+ const DIFF_BLOCK_MAX_LINES = 1000000
5556
+
5557
+ function diffLabelsOf(t) {
5558
+ return {
5559
+ codeLabel: t('diff.codeLabel'),
5560
+ wrapLabel: t('diff.wrapLabel'),
5561
+ unwrapLabel: t('diff.unwrapLabel'),
5562
+ copy: t('diff.copy'),
5563
+ copied: t('diff.copied'),
5564
+ collapseAria: 'Collapse diff',
5565
+ expandAria: (hidden) => 'Show ' + String(hidden) + ' more diff lines',
5566
+ collapse: 'Show less',
5567
+ expand: (hidden) => 'Show ' + String(hidden) + ' more',
5568
+ }
5569
+ }
5570
+
5159
5571
  function DiffBody(props) {
5160
5572
  const rows = useMemo(() => diffLines(props.patch), [props.patch])
5161
5573
  if (props.loading === true) return E('div', { className: 'dig-empty' }, props.t('diff.loading'))
5162
5574
  if (props.binary === true) return E('div', { className: 'dig-empty' }, props.t('diff.binary'))
5163
5575
  if (rows.length === 0) return E('div', { className: 'dig-empty' }, props.t('diff.empty'))
5576
+ /* Host-rendered first (v0.14): the ui-primitives diff card brings the
5577
+ shared toolbar (copy / wrap / language) and the product's diff colors.
5578
+ A patch we cannot replay into a hunk (binary, mode-only) or a host
5579
+ without the seed module falls back to the hand-drawn rows below. */
5580
+ if (UIPrimitives !== null && typeof UIPrimitives.DiffBlock === 'function') {
5581
+ const hunk = patchToHunk(props.patch)
5582
+ if (hunk !== null) {
5583
+ return E('div', { className: 'dig-diff-host' },
5584
+ E(UIPrimitives.DiffBlock, {
5585
+ diffs: [hunk],
5586
+ labels: diffLabelsOf(props.t),
5587
+ maxLines: DIFF_BLOCK_MAX_LINES,
5588
+ className: 'dig-diff-card',
5589
+ }))
5590
+ }
5591
+ }
5164
5592
  return E('div', { className: 'dig-diff' }, rows.map((row, index) => E('div', { key: index, className: 'dig-diff-line dig-diff-' + row.kind },
5165
5593
  E('span', { className: 'dig-diff-gutter' }, row.oldLine === null ? '' : String(row.oldLine)),
5166
5594
  E('span', { className: 'dig-diff-gutter' }, row.newLine === null ? '' : String(row.newLine)),
@@ -5168,6 +5596,22 @@ window.__ModuleLoader__.load({
5168
5596
  )))
5169
5597
  }
5170
5598
 
5599
+ /* The leading glyph of a change row (v0.14): the host's FileTypeIcon —
5600
+ classifyFileType runs inside when given a path. Directories (the
5601
+ trailing slash an `ls-files --directory` row carries) ask for the
5602
+ folder kind explicitly, since their basename is empty. A host without
5603
+ the seed module returns null and the row keeps its status letter as the
5604
+ only leading mark. Sizing follows the status letter's box (14px, the
5605
+ .dig-row line height). */
5606
+ function FileTypeGlyph(props) {
5607
+ if (UIPrimitives === null || typeof UIPrimitives.FileTypeIcon !== 'function') return null
5608
+ const path = typeof props.path === 'string' ? props.path : ''
5609
+ if (path === '') return null
5610
+ return E(UIPrimitives.FileTypeIcon, path.slice(-1) === '/'
5611
+ ? { kind: 'folder', size: 14, className: 'dig-file-icon' }
5612
+ : { path: path, size: 14, className: 'dig-file-icon' })
5613
+ }
5614
+
5171
5615
  /* ============================== ui kit ============================== */
5172
5616
 
5173
5617
  const ICONS = {
@@ -5213,6 +5657,9 @@ window.__ModuleLoader__.load({
5213
5657
  split: ['M2.5 3h11v10h-11z', 'M8 3v10'],
5214
5658
  expand: ['M4.5 6.2 8 9.7l3.5-3.5', 'M3 12.4h10'],
5215
5659
  collapse: ['M4.5 9.8 8 6.3l3.5 3.5', 'M3 3.6h10'],
5660
+ // "Send to chat": a paper plane — the shared glyph of the three context
5661
+ // menus that drop a reference / hash / branch name into the composer.
5662
+ send: ['M14.3 1.7 1.7 7.4l4.9 1.9 1.9 4.9L14.3 1.7Z', 'M6.6 9.3 14.3 1.7'],
5216
5663
  }
5217
5664
 
5218
5665
  function Icon(props) {
@@ -6174,6 +6621,7 @@ window.__ModuleLoader__.load({
6174
6621
  onClick: () => props.onDiff(item, props.group),
6175
6622
  onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, item, props.group) },
6176
6623
  },
6624
+ FileTypeGlyph({ path: item.path }),
6177
6625
  E('span', { className: 'dig-file-status dig-file-status-' + (status === '?' ? 'U' : status) }, status),
6178
6626
  // An ignored DIRECTORY comes back as "dist/" (ls-files --directory), whose
6179
6627
  // basename is empty — show the whole path instead of a blank label.
@@ -6617,7 +7065,9 @@ window.__ModuleLoader__.load({
6617
7065
  className: 'dig-row dig-row-file' + (props.selectedPath === file.path ? ' dig-row-selected' : ''),
6618
7066
  title: file.path,
6619
7067
  onClick: () => props.onSelectFile(file),
7068
+ onContextMenu: props.onFileMenu === undefined ? undefined : (event) => { event.preventDefault(); props.onFileMenu(event, file) },
6620
7069
  },
7070
+ FileTypeGlyph({ path: file.path }),
6621
7071
  E('span', { className: 'dig-row-label' }, baseName(file.path)),
6622
7072
  E('span', { className: 'dig-row-sub dig-row-dir' }, dirName(file.path)),
6623
7073
  file.additions > 0 ? E('span', { className: 'dig-stat-add' }, '+' + file.additions) : null,
@@ -6767,7 +7217,10 @@ window.__ModuleLoader__.load({
6767
7217
  if (element === null) return found
6768
7218
  for (const node of element.querySelectorAll('[data-pane]')) {
6769
7219
  const rect = node.getBoundingClientRect()
6770
- found[node.getAttribute('data-pane')] = Math.round(paneByWidth ? rect.width : rect.height)
7220
+ // The diff pane is measured in HEIGHT under every chrome — it is
7221
+ // carved out of the middle pane, whose siblings may run sideways.
7222
+ const key = node.getAttribute('data-pane')
7223
+ found[key] = Math.round(key === 'diff' || paneByWidth === false ? rect.height : rect.width)
6771
7224
  }
6772
7225
  return found
6773
7226
  }
@@ -6804,7 +7257,8 @@ window.__ModuleLoader__.load({
6804
7257
  setPaneSizes((current) => (samePaneSizes(current, live) ? current : live))
6805
7258
  const layout = paneGeometry(chrome, paneBox, panesRef.current, live)
6806
7259
  const limit = layout.limits[key]
6807
- const reference = paneByWidth ? paneBox.width : paneBox.height
7260
+ // The diff divides the body HEIGHT in every chrome (see paneGeometry).
7261
+ const reference = key === 'diff' ? paneBox.height : (paneByWidth ? paneBox.width : paneBox.height)
6808
7262
  if (limit === undefined || !(reference > 0)) return
6809
7263
  const px = Math.round(Math.min(Math.max(rawPx, limit.min), limit.max))
6810
7264
  commitPanes(withPaneRatio(panesRef.current, layout.bucket, key, px / reference))
@@ -6834,7 +7288,9 @@ window.__ModuleLoader__.load({
6834
7288
  const paneStyle = (key) => {
6835
7289
  const px = paneLayout.overrides[key]
6836
7290
  if (px === undefined) return null
6837
- return paneByWidth ? { width: px + 'px', maxWidth: px + 'px' } : { height: px + 'px', maxHeight: px + 'px' }
7291
+ // The diff always takes its size as a height, in every chrome.
7292
+ const vertical = key === 'diff' ? true : paneByWidth === false
7293
+ return vertical ? { height: px + 'px', maxHeight: px + 'px' } : { width: px + 'px', maxWidth: px + 'px' }
6838
7294
  }
6839
7295
 
6840
7296
  /* A divider is a control, so it only exists when it can actually move
@@ -6848,7 +7304,10 @@ window.__ModuleLoader__.load({
6848
7304
  key: 'gutter-' + key,
6849
7305
  t: t,
6850
7306
  nameKey: PANE_NAME_KEYS[key],
6851
- vertical: paneByWidth,
7307
+ // The diff divider is a horizontal bar in every chrome: it moves a
7308
+ // window that lives BELOW it, even where tree/changes are dragged
7309
+ // sideways (columns).
7310
+ vertical: key === 'diff' ? false : paneByWidth,
6852
7311
  side: side,
6853
7312
  active: paneDragging === key,
6854
7313
  // Now is clamped into [min, max]: an aria window the panel contradicts is
@@ -7086,6 +7545,71 @@ window.__ModuleLoader__.load({
7086
7545
  })
7087
7546
  }, [])
7088
7547
 
7548
+ /* ---------- toast plumbing ---------- */
7549
+ /* Declared BEFORE the context menus on purpose: the send-to-chat bindings
7550
+ and every menu builder close over pushToast, and a hooks dependency
7551
+ array is evaluated during render (invariant 12) — the plumbing has to
7552
+ exist by the time those arrays are evaluated. */
7553
+ const toastTimers = useRef([])
7554
+ const toastSeq = useRef(0)
7555
+
7556
+ useEffect(() => () => {
7557
+ for (const timer of toastTimers.current) clearTimeout(timer)
7558
+ toastTimers.current = []
7559
+ }, [])
7560
+
7561
+ const dropToast = useCallback((id) => {
7562
+ setToasts((list) => list.filter((item) => item.id !== id))
7563
+ }, [])
7564
+
7565
+ const pushToast = useCallback((toast) => {
7566
+ toastSeq.current += 1
7567
+ const id = 'toast-' + String(toastSeq.current)
7568
+ setToasts((list) => list.concat([Object.assign({ id: id, onClose: () => dropToast(id) }, toast)]))
7569
+ const ttl = toast.ttl === undefined ? (toast.actionLabel === undefined ? 9000 : 30000) : toast.ttl
7570
+ if (ttl > 0) toastTimers.current.push(setTimeout(() => dropToast(id), ttl))
7571
+ return id
7572
+ }, [dropToast])
7573
+
7574
+ /* Send-to-chat (v0.14): one funnel for the three context menus. The
7575
+ insertion target is the CURRENT session's composer (scope.sessionId);
7576
+ a miss — service absent, no retained session, phase busy, CAS raced
7577
+ twice — still gives feedback: the payload is copied to the clipboard
7578
+ and a toast says so. */
7579
+ const sendToComposer = useCallback((payload) => {
7580
+ const applied = insertIntoComposer(props.ctx, sessionId, (actx, input, span) => {
7581
+ if (payload.appearance !== undefined) {
7582
+ return input.insertReference({
7583
+ source: 'reference',
7584
+ ref: payload.mention,
7585
+ label: payload.label,
7586
+ appearance: payload.appearance,
7587
+ clipboardText: payload.mention,
7588
+ }, span)
7589
+ }
7590
+ // Plain text rides the scoped insert-text event (bail value IS the
7591
+ // answer); a trailing space keeps the next keystroke off the token.
7592
+ return actx.bail(actx, 'slash/input-insert-text', { text: payload.text + ' ', span: span }) === true
7593
+ })
7594
+ if (applied === true) {
7595
+ pushToast({ text: t('toast.sentToChat'), icon: 'send', tone: 'ok', ttl: 6000 })
7596
+ return
7597
+ }
7598
+ copyText(payload.fallback)
7599
+ pushToast({ text: t('toast.sendCopied'), icon: 'tag', tone: 'warn', ttl: 6000 })
7600
+ }, [props.ctx, sessionId, pushToast, t])
7601
+
7602
+ const sendFileReferenceToChat = useCallback((path) => {
7603
+ const mention = fileMentionOf(path)
7604
+ if (mention === null) { sendToComposer({ text: path, fallback: path }); return }
7605
+ sendToComposer({
7606
+ mention: mention,
7607
+ label: path.slice(-1) === '/' ? path.slice(0, -1) : baseName(path),
7608
+ appearance: path.slice(-1) === '/' ? 'folder' : 'file',
7609
+ fallback: path,
7610
+ })
7611
+ }, [sendToComposer])
7612
+
7089
7613
  const branchMenu = useCallback((event, entry) => {
7090
7614
  const current = branches === null ? '' : branches.branch
7091
7615
  const isLocal = entry.remote !== true && entry.tag !== true
@@ -7095,6 +7619,7 @@ window.__ModuleLoader__.load({
7095
7619
  isLocal ? { id: 'rebase', icon: 'compare', tone: 'violet', label: t('action.rebaseCurrentOnto'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('rebase', { onto: entry.name }) } } : null,
7096
7620
  isLocal ? { id: 'merge', icon: 'compare', tone: 'accent', label: t('action.mergeIntoCurrent'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('merge', { branch: entry.name }) } } : null,
7097
7621
  { id: 'compare', icon: 'filter', tone: 'violet', label: t('action.compare'), disabled: entry.head === true || current === '', reason: reason, run: () => { void compareWith(current, entry.name) } },
7622
+ { id: 'send', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendToComposer({ text: entry.name, fallback: entry.name }) },
7098
7623
  null,
7099
7624
  { id: 'favorite', icon: 'star', tone: 'warn', label: t('action.favorite'), active: favorites.indexOf(entry.name) >= 0, run: () => toggleFavoriteBranch(entry.name) },
7100
7625
  { id: 'newBranch', icon: 'plus', tone: 'success', label: t('action.newBranchFrom'), run: () => setDialog({ kind: 'newBranch', from: entry.name }) },
@@ -7105,12 +7630,14 @@ window.__ModuleLoader__.load({
7105
7630
  { id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) },
7106
7631
  ].filter((item) => item !== null)
7107
7632
  openMenuAt(event, items)
7108
- }, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked])
7633
+ }, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendToComposer])
7109
7634
 
7110
7635
  const commitMenu = useCallback((event, commit) => {
7636
+ const shortHash = commit.hash.slice(0, 7)
7111
7637
  const items = [
7112
7638
  { id: 'details', icon: 'file', tone: 'primary', label: t('action.details'), run: () => { void selectCommit(commit) } },
7113
7639
  { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyHash'), run: () => { copyText(commit.hash) } },
7640
+ { id: 'send', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendToComposer({ text: shortHash, fallback: shortHash }) },
7114
7641
  null,
7115
7642
  { id: 'checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout'), run: () => setDialog({ kind: 'checkoutCommit', hash: commit.hash }) },
7116
7643
  { id: 'branch', icon: 'plus', tone: 'success', label: t('action.newBranchHere'), run: () => setDialog({ kind: 'newBranch', from: commit.hash }) },
@@ -7123,11 +7650,13 @@ window.__ModuleLoader__.load({
7123
7650
  { id: 'resetHard', icon: 'trash', tone: 'danger', label: t('action.resetHard'), danger: true, disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'reset', hash: commit.hash, mode: 'hard' }) },
7124
7651
  ]
7125
7652
  openMenuAt(event, items)
7126
- }, [run, selectCommit, t, openMenuAt, blocked])
7653
+ }, [run, selectCommit, t, openMenuAt, blocked, sendToComposer])
7127
7654
 
7128
7655
  const changeMenu = useCallback((event, item, group) => {
7129
7656
  const items = [
7130
7657
  { id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { void openDiff({ path: item.path, staged: group === 'staged' }) } },
7658
+ { id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(item.path) },
7659
+ { id: 'sendPath', icon: 'file', tone: 'secondary', label: t('menu.sendFilePathToChat'), run: () => sendToComposer({ text: item.path, fallback: item.path }) },
7131
7660
  null,
7132
7661
  group === 'staged'
7133
7662
  ? { id: 'unstage', icon: 'minus', tone: 'warn', label: t('action.unstage'), run: () => { void run('unstage', { paths: [item.path] }) } }
@@ -7137,7 +7666,20 @@ window.__ModuleLoader__.load({
7137
7666
  { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(item.path) } },
7138
7667
  ]
7139
7668
  openMenuAt(event, items)
7140
- }, [openDiff, run, t, openMenuAt])
7669
+ }, [openDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer])
7670
+
7671
+ /* The commit-detail file list shares the working-tree row menu, minus the
7672
+ staging verbs: a committed file's diff replays against its commit. */
7673
+ const detailFileMenu = useCallback((event, file) => {
7674
+ openMenuAt(event, [
7675
+ { id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { setSelectedPath(file.path); void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path }) } },
7676
+ null,
7677
+ { id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(file.path) },
7678
+ { id: 'sendPath', icon: 'file', tone: 'secondary', label: t('menu.sendFilePathToChat'), run: () => sendToComposer({ text: file.path, fallback: file.path }) },
7679
+ null,
7680
+ { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(file.path) } },
7681
+ ])
7682
+ }, [detail, openDiff, openMenuAt, sendFileReferenceToChat, sendToComposer, t])
7141
7683
 
7142
7684
  const submitDialog = useCallback(async (state, value) => {
7143
7685
  setDialog(null)
@@ -7148,27 +7690,6 @@ window.__ModuleLoader__.load({
7148
7690
 
7149
7691
  /* ---------- toasts + undo ---------- */
7150
7692
 
7151
- const toastTimers = useRef([])
7152
- const toastSeq = useRef(0)
7153
-
7154
- useEffect(() => () => {
7155
- for (const timer of toastTimers.current) clearTimeout(timer)
7156
- toastTimers.current = []
7157
- }, [])
7158
-
7159
- const dropToast = useCallback((id) => {
7160
- setToasts((list) => list.filter((item) => item.id !== id))
7161
- }, [])
7162
-
7163
- const pushToast = useCallback((toast) => {
7164
- toastSeq.current += 1
7165
- const id = 'toast-' + String(toastSeq.current)
7166
- setToasts((list) => list.concat([Object.assign({ id: id, onClose: () => dropToast(id) }, toast)]))
7167
- const ttl = toast.ttl === undefined ? (toast.actionLabel === undefined ? 9000 : 30000) : toast.ttl
7168
- if (ttl > 0) toastTimers.current.push(setTimeout(() => dropToast(id), ttl))
7169
- return id
7170
- }, [dropToast])
7171
-
7172
7693
  // The delete really happened; the host kept an object id for a while, and this
7173
7694
  // asks it to recreate the branch / stash entry from that id.
7174
7695
  const undoAction = useCallback(async (id) => {
@@ -7343,6 +7864,7 @@ window.__ModuleLoader__.load({
7343
7864
  setSelectedPath(file.path)
7344
7865
  void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
7345
7866
  },
7867
+ onFileMenu: detailFileMenu,
7346
7868
  })
7347
7869
  : E(HistoryList, {
7348
7870
  t: t, commits: commits, hasMore: hasMore, busy: busy, selectedHash: selectedHash,
@@ -8046,6 +8568,14 @@ window.__ModuleLoader__.load({
8046
8568
  '.dig-diff-del{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent)}',
8047
8569
  '.dig-diff-hunk{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}',
8048
8570
  '.dig-diff-meta{color:var(--dsw-alias-label-tertiary)}',
8571
+ /* Host-rendered diff (ui-primitives DiffBlock): the host box keeps the
8572
+ pane's scroll contract (everything renders, the pane scrolls), and the
8573
+ card's own 16px vertical margin is tightened to the panel rhythm. */
8574
+ '.dig-diff-host{flex:1;overflow:auto;min-height:0}',
8575
+ '.dig-diff-card{margin:4px 6px 8px;font-size:12px}',
8576
+ /* File-type glyphs lead every change row (v0.14); a transparent-theme
8577
+ host still tints them through the component's own category colors. */
8578
+ '.dig-file-icon{flex:none;opacity:.95}',
8049
8579
  '.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}',
8050
8580
  '.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))}',
8051
8581
  '.dig-dialog-wide{min-width:min(360px,92%)}',
@@ -8108,7 +8638,7 @@ window.__ModuleLoader__.load({
8108
8638
  const openSettings = navigation !== undefined && navigation !== null && typeof navigation.openBundle === 'function'
8109
8639
  ? () => { navigation.openBundle(SETTINGS_BUNDLE) }
8110
8640
  : null
8111
- return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings })
8641
+ return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings, ctx: props.ctx })
8112
8642
  }
8113
8643
 
8114
8644
  /* The conversation-area seat (issue #8): the same panel as a view tab
@@ -8160,13 +8690,35 @@ window.__ModuleLoader__.load({
8160
8690
  useEffect(() => {
8161
8691
  const el = seatRef.current
8162
8692
  if (el === null) return undefined
8693
+ /* Leftover wheel scroll, root cause: the composer seat is sticky, but a
8694
+ sticky box still occupies FLOW space inside the scrollport, so even
8695
+ with the panel seat pinned to the visible height the scrollHeight
8696
+ stays clientHeight + that flow space — the wheel could still scroll
8697
+ the whole window by exactly that leftover (user report: the layout
8698
+ fits, yet the page keeps scrolling). The overflow only exists AFTER
8699
+ the new height has been applied, so measure() sets the visible-height
8700
+ target and then, one frame later, CONVERGES: it reads the real
8701
+ overflow and shrinks the seat by that amount. Guard rails keep it
8702
+ stable: shrink only while overflow > 0 (measure() itself recomputes
8703
+ the base on every pass, so the correction never compounds), clamp at
8704
+ the same 240 floor, and the correction is remembered so the next
8705
+ measure() starts from the already-corrected target. */
8706
+ const leftover = { current: 0 }
8163
8707
  const measure = () => {
8164
8708
  let scroll = null
8165
8709
  try { scroll = el.closest('[data-conversation-scroll]') } catch (error) { void error }
8166
8710
  if (scroll === null || scroll === undefined) return
8167
8711
  const offset = Math.max(0, el.getBoundingClientRect().top - scroll.getBoundingClientRect().top)
8168
- const available = Math.round(scroll.clientHeight - offset - 8)
8712
+ const available = Math.round(scroll.clientHeight - offset - 8) - leftover.current
8169
8713
  if (available >= 240) setSeatHeight(available)
8714
+ requestAnimationFrame(() => {
8715
+ if (el.isConnected !== true) return
8716
+ const overflow = scroll.scrollHeight - scroll.clientHeight
8717
+ if (overflow > 0) {
8718
+ leftover.current = overflow
8719
+ setSeatHeight(Math.max(240, available - overflow))
8720
+ }
8721
+ })
8170
8722
  }
8171
8723
  measure()
8172
8724
  const raf = requestAnimationFrame(() => { measure() })