dsh-ide-git 0.13.1 → 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
@@ -28,6 +28,18 @@ window.__ModuleLoader__.load({
28
28
  const useCallback = React.useCallback
29
29
  const useLayoutEffect = React.useLayoutEffect
30
30
  const memo = React.memo
31
+ /* Measure-and-derive effects run here for the layout timing (the chrome and
32
+ the rail capacity derive from these numbers — a paint-then-correct cycle
33
+ read as buttons lighting up one by one on every seat switch). Environments
34
+ without a frame loop (the SSR self-check's shims) get the plain effect —
35
+ there is no paint to sync with there, and no server warning either. */
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 }
31
43
 
32
44
  /* API base is MOUNT-RELATIVE (no leading slash). dsh 0.1.7 serves the
33
45
  shell with <base href="./">, so document.baseURI is the mount the page
@@ -140,6 +152,8 @@ window.__ModuleLoader__.load({
140
152
  'action.resetHard': '重置到此(丢弃更改)',
141
153
  'action.copyHash': '复制修订号',
142
154
  'action.details': '提交详情',
155
+ 'menu.sendToChat': '发送到对话',
156
+ 'menu.sendFilePathToChat': '发送文件路径到对话',
143
157
  'history.empty': '这个仓库还没有提交',
144
158
  'history.loadMore': '加载更多',
145
159
  'history.filter': '文本或哈希',
@@ -151,6 +165,11 @@ window.__ModuleLoader__.load({
151
165
  'pane.resize': '拖动调整{name}尺寸,双击复位,方向键微调',
152
166
  'diff.binary': '二进制文件',
153
167
  'diff.loading': '正在加载差异...',
168
+ 'diff.codeLabel': '差异',
169
+ 'diff.wrapLabel': '自动换行',
170
+ 'diff.unwrapLabel': '按源列宽显示',
171
+ 'diff.copy': '复制',
172
+ 'diff.copied': '已复制',
154
173
  'toolbar.refresh': '刷新',
155
174
  'toolbar.newBranch': '新建分支',
156
175
  'toolbar.fetch': '抓取(Fetch)',
@@ -217,6 +236,8 @@ window.__ModuleLoader__.load({
217
236
  'toast.discarded': '已丢弃 {path} 的更改',
218
237
  'toast.discardNoUndo': '已丢弃 {path} 的更改(文件过大或过多,未保留撤回)',
219
238
  'toast.restored': '已恢复',
239
+ 'toast.sentToChat': '已发送到对话框',
240
+ 'toast.sendCopied': '无法发送,已复制到剪贴板',
220
241
  'undo.menu': '最近可撤回的操作',
221
242
  'undo.empty': '现在没有可撤回的操作',
222
243
  'undo.branch': '分支',
@@ -331,6 +352,8 @@ window.__ModuleLoader__.load({
331
352
  'action.resetHard': 'Reset here (discard changes)',
332
353
  'action.copyHash': 'Copy revision number',
333
354
  'action.details': 'Commit details',
355
+ 'menu.sendToChat': 'Send to chat',
356
+ 'menu.sendFilePathToChat': 'Send file path to chat',
334
357
  'history.empty': 'This repository has no commits yet',
335
358
  'history.loadMore': 'Load more',
336
359
  'history.filter': 'Text or hash',
@@ -342,6 +365,11 @@ window.__ModuleLoader__.load({
342
365
  'pane.resize': 'Drag to resize {name}: double-click resets, arrow keys nudge',
343
366
  'diff.binary': 'Binary file',
344
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',
345
373
  'toolbar.refresh': 'Refresh',
346
374
  'toolbar.newBranch': 'New branch',
347
375
  'toolbar.fetch': 'Fetch',
@@ -408,6 +436,8 @@ window.__ModuleLoader__.load({
408
436
  'toast.discarded': 'Discarded changes in {path}',
409
437
  'toast.discardNoUndo': 'Discarded changes in {path} (too large to keep an undo)',
410
438
  'toast.restored': 'Restored',
439
+ 'toast.sentToChat': 'Sent to the chat box',
440
+ 'toast.sendCopied': 'Could not send — copied to the clipboard instead',
411
441
  'undo.menu': 'Recently deleted (undo)',
412
442
  'undo.empty': 'Nothing to undo right now',
413
443
  'undo.branch': 'Branch',
@@ -530,6 +560,8 @@ window.__ModuleLoader__.load({
530
560
  'action.resetHard': '重置到此(丟棄變更)',
531
561
  'action.copyHash': '複製修訂號',
532
562
  'action.details': '提交詳情',
563
+ 'menu.sendToChat': '傳送到對話',
564
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
533
565
  'history.empty': '呢個存放庫仲未有提交',
534
566
  'history.loadMore': '載入更多',
535
567
  'history.filter': '文字或雜湊',
@@ -541,6 +573,11 @@ window.__ModuleLoader__.load({
541
573
  'pane.resize': '拖動調整{name}尺寸,雙擊復位,方向鍵微調',
542
574
  'diff.binary': '二進位檔案',
543
575
  'diff.loading': '正在載入差異...',
576
+ 'diff.codeLabel': '差異',
577
+ 'diff.wrapLabel': '自動換行',
578
+ 'diff.unwrapLabel': '按原始欄寬顯示',
579
+ 'diff.copy': '複製',
580
+ 'diff.copied': '已複製',
544
581
  'toolbar.refresh': '重新整理',
545
582
  'toolbar.newBranch': '新建分支',
546
583
  'toolbar.fetch': '抓取',
@@ -607,6 +644,8 @@ window.__ModuleLoader__.load({
607
644
  'toast.discarded': '已丟棄 {path} 嘅變更',
608
645
  'toast.discardNoUndo': '已丟棄 {path} 嘅變更(太大,冇保留撤回)',
609
646
  'toast.restored': '已還原',
647
+ 'toast.sentToChat': '已傳送到對話框',
648
+ 'toast.sendCopied': '無法傳送,已複製到剪貼板',
610
649
  'undo.menu': '最近刪除(撤回)',
611
650
  'undo.empty': '暫時冇嘢可以撤回',
612
651
  'undo.branch': '分支',
@@ -721,6 +760,8 @@ window.__ModuleLoader__.load({
721
760
  'action.resetHard': '重置到此(丟棄變更)',
722
761
  'action.copyHash': '複製修訂編號',
723
762
  'action.details': '提交詳情',
763
+ 'menu.sendToChat': '傳送至對話',
764
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
724
765
  'history.empty': '這個版本庫還沒有提交',
725
766
  'history.loadMore': '載入更多',
726
767
  'history.filter': '文字或雜湊',
@@ -732,6 +773,11 @@ window.__ModuleLoader__.load({
732
773
  'pane.resize': '拖曳調整{name}尺寸,雙擊重設,方向鍵微調',
733
774
  'diff.binary': '二進位檔案',
734
775
  'diff.loading': '正在載入差異...',
776
+ 'diff.codeLabel': '差異',
777
+ 'diff.wrapLabel': '自動換行',
778
+ 'diff.unwrapLabel': '依原始欄寬顯示',
779
+ 'diff.copy': '複製',
780
+ 'diff.copied': '已複製',
735
781
  'toolbar.refresh': '重新整理',
736
782
  'toolbar.newBranch': '新建分支',
737
783
  'toolbar.fetch': '抓取',
@@ -798,6 +844,8 @@ window.__ModuleLoader__.load({
798
844
  'toast.discarded': '已丟棄 {path} 的變更',
799
845
  'toast.discardNoUndo': '已丟棄 {path} 的變更(太大,未保留撤回)',
800
846
  'toast.restored': '已還原',
847
+ 'toast.sentToChat': '已傳送至對話框',
848
+ 'toast.sendCopied': '無法傳送,已複製到剪貼簿',
801
849
  'undo.menu': '最近刪除(撤回)',
802
850
  'undo.empty': '目前沒有可撤回的項目',
803
851
  'undo.branch': '分支',
@@ -912,6 +960,8 @@ window.__ModuleLoader__.load({
912
960
  'action.resetHard': '重置到此(丟棄變更)',
913
961
  'action.copyHash': '複製修訂號',
914
962
  'action.details': '提交詳情',
963
+ 'menu.sendToChat': '傳送至對話',
964
+ 'menu.sendFilePathToChat': '傳送檔案路徑至對話',
915
965
  'history.empty': '呢個存放庫仲未有提交',
916
966
  'history.loadMore': '載入更多',
917
967
  'history.filter': '文字或雜湊',
@@ -923,6 +973,11 @@ window.__ModuleLoader__.load({
923
973
  'pane.resize': '拖動調整{name}尺寸,雙擊復位,方向鍵微調',
924
974
  'diff.binary': '二進位檔案',
925
975
  'diff.loading': '正在載入差異...',
976
+ 'diff.codeLabel': '差異',
977
+ 'diff.wrapLabel': '自動換行',
978
+ 'diff.unwrapLabel': '依原始欄寬顯示',
979
+ 'diff.copy': '複製',
980
+ 'diff.copied': '已複製',
926
981
  'toolbar.refresh': '重新整理',
927
982
  'toolbar.newBranch': '新建分支',
928
983
  'toolbar.fetch': '抓取',
@@ -989,6 +1044,8 @@ window.__ModuleLoader__.load({
989
1044
  'toast.discarded': '已丟棄 {path} 嘅變更',
990
1045
  'toast.discardNoUndo': '已丟棄 {path} 嘅變更(太大,冇保留撤回)',
991
1046
  'toast.restored': '已還原',
1047
+ 'toast.sentToChat': '已傳送至對話框',
1048
+ 'toast.sendCopied': '無法傳送,已複製到剪貼簿',
992
1049
  'undo.menu': '最近刪除(撤回)',
993
1050
  'undo.empty': '暫時冇嘢可以撤回',
994
1051
  'undo.branch': '分支',
@@ -1103,6 +1160,8 @@ window.__ModuleLoader__.load({
1103
1160
  'action.resetHard': 'ここにリセット(変更を破棄)',
1104
1161
  'action.copyHash': 'リビジョン番号をコピー',
1105
1162
  'action.details': 'コミットの詳細',
1163
+ 'menu.sendToChat': 'チャットへ送る',
1164
+ 'menu.sendFilePathToChat': 'ファイルパスをチャットへ送る',
1106
1165
  'history.empty': 'このリポジトリにはまだコミットがありません',
1107
1166
  'history.loadMore': 'さらに読み込む',
1108
1167
  'history.filter': 'テキストまたはハッシュ',
@@ -1114,6 +1173,11 @@ window.__ModuleLoader__.load({
1114
1173
  'pane.resize': '{name}のサイズをドラッグで調整(ダブルクリックでリセット、矢印キーで微調整)',
1115
1174
  'diff.binary': 'バイナリファイル',
1116
1175
  'diff.loading': '差分を読み込み中...',
1176
+ 'diff.codeLabel': '差分',
1177
+ 'diff.wrapLabel': '折り返し',
1178
+ 'diff.unwrapLabel': 'ソースの列幅を維持',
1179
+ 'diff.copy': 'コピー',
1180
+ 'diff.copied': 'コピーしました',
1117
1181
  'toolbar.refresh': '更新',
1118
1182
  'toolbar.newBranch': '新しいブランチ',
1119
1183
  'toolbar.fetch': 'フェッチ',
@@ -1180,6 +1244,8 @@ window.__ModuleLoader__.load({
1180
1244
  'toast.discarded': '{path} の変更を破棄しました',
1181
1245
  'toast.discardNoUndo': '{path} の変更を破棄しました(大きすぎるため取り消しは保持していません)',
1182
1246
  'toast.restored': '復元しました',
1247
+ 'toast.sentToChat': '入力欄に送りました',
1248
+ 'toast.sendCopied': '送信できなかったためクリップボードにコピーしました',
1183
1249
  'undo.menu': '最近の削除(元に戻す)',
1184
1250
  'undo.empty': '今すぐ元に戻せるものはありません',
1185
1251
  'undo.branch': 'ブランチ',
@@ -1294,6 +1360,8 @@ window.__ModuleLoader__.load({
1294
1360
  'action.resetHard': '여기로 리셋(변경 버림)',
1295
1361
  'action.copyHash': '리비전 번호 복사',
1296
1362
  'action.details': '커밋 상세',
1363
+ 'menu.sendToChat': '대화로 보내기',
1364
+ 'menu.sendFilePathToChat': '파일 경로를 대화로 보내기',
1297
1365
  'history.empty': '이 저장소에는 아직 커밋이 없습니다',
1298
1366
  'history.loadMore': '더 불러오기',
1299
1367
  'history.filter': '텍스트 또는 해시',
@@ -1305,6 +1373,11 @@ window.__ModuleLoader__.load({
1305
1373
  'pane.resize': '{name} 크기를 드래그해 조절(더블클릭 초기화, 화살표 키 미세 조정)',
1306
1374
  'diff.binary': '바이너리 파일',
1307
1375
  'diff.loading': '차이 불러오는 중...',
1376
+ 'diff.codeLabel': '차이',
1377
+ 'diff.wrapLabel': '줄 바꿈',
1378
+ 'diff.unwrapLabel': '원본 열 너비 유지',
1379
+ 'diff.copy': '복사',
1380
+ 'diff.copied': '복사됨',
1308
1381
  'toolbar.refresh': '새로 고침',
1309
1382
  'toolbar.newBranch': '새 브랜치',
1310
1383
  'toolbar.fetch': '페치',
@@ -1371,6 +1444,8 @@ window.__ModuleLoader__.load({
1371
1444
  'toast.discarded': '{path}의 변경 사항을 버렸습니다',
1372
1445
  'toast.discardNoUndo': '{path}의 변경 사항을 버렸습니다(너무 커서 되돌리기를 보관하지 않음)',
1373
1446
  'toast.restored': '복원했습니다',
1447
+ 'toast.sentToChat': '입력창에 보냈습니다',
1448
+ 'toast.sendCopied': '보낼 수 없어 클립보드에 복사했습니다',
1374
1449
  'undo.menu': '최근 삭제(되돌리기)',
1375
1450
  'undo.empty': '지금 되돌릴 항목이 없습니다',
1376
1451
  'undo.branch': '브랜치',
@@ -1485,6 +1560,8 @@ window.__ModuleLoader__.load({
1485
1560
  'action.resetHard': 'Hierher zurücksetzen (Änderungen verwerfen)',
1486
1561
  'action.copyHash': 'Revisionsnummer kopieren',
1487
1562
  'action.details': 'Commit-Details',
1563
+ 'menu.sendToChat': 'Im Chat senden',
1564
+ 'menu.sendFilePathToChat': 'Dateipfad im Chat senden',
1488
1565
  'history.empty': 'Dieses Repository hat noch keine Commits',
1489
1566
  'history.loadMore': 'Mehr laden',
1490
1567
  'history.filter': 'Text oder Hash',
@@ -1496,6 +1573,11 @@ window.__ModuleLoader__.load({
1496
1573
  'pane.resize': 'Größe von {name} per Ziehen ändern (Doppelklick setzt zurück, Pfeiltasten justieren)',
1497
1574
  'diff.binary': 'Binärdatei',
1498
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',
1499
1581
  'toolbar.refresh': 'Aktualisieren',
1500
1582
  'toolbar.newBranch': 'Neuer Branch',
1501
1583
  'toolbar.fetch': 'Fetch',
@@ -1562,6 +1644,8 @@ window.__ModuleLoader__.load({
1562
1644
  'toast.discarded': 'Änderungen in {path} verworfen',
1563
1645
  'toast.discardNoUndo': 'Änderungen in {path} verworfen (zu groß für ein Rückgängig)',
1564
1646
  'toast.restored': 'Wiederhergestellt',
1647
+ 'toast.sentToChat': 'In das Eingabefeld eingefügt',
1648
+ 'toast.sendCopied': 'Senden nicht möglich – stattdessen in die Zwischenablage kopiert',
1565
1649
  'undo.menu': 'Zuletzt gelöscht (rückgängig)',
1566
1650
  'undo.empty': 'Derzeit nichts rückgängig zu machen',
1567
1651
  'undo.branch': 'Branch',
@@ -1676,6 +1760,8 @@ window.__ModuleLoader__.load({
1676
1760
  'action.resetHard': 'Réinitialiser ici (abandonner les modifications)',
1677
1761
  'action.copyHash': 'Copier le numéro de révision',
1678
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',
1679
1765
  'history.empty': 'Ce dépôt n\'a pas encore de commit',
1680
1766
  'history.loadMore': 'Charger plus',
1681
1767
  'history.filter': 'Texte ou empreinte',
@@ -1687,6 +1773,11 @@ window.__ModuleLoader__.load({
1687
1773
  'pane.resize': 'Glisser pour redimensionner {name} (double-clic pour réinitialiser, flèches pour ajuster)',
1688
1774
  'diff.binary': 'Fichier binaire',
1689
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é',
1690
1781
  'toolbar.refresh': 'Rafraîchir',
1691
1782
  'toolbar.newBranch': 'Nouvelle branche',
1692
1783
  'toolbar.fetch': 'Récupérer',
@@ -1753,6 +1844,8 @@ window.__ModuleLoader__.load({
1753
1844
  'toast.discarded': 'Modifications de {path} abandonnées',
1754
1845
  'toast.discardNoUndo': 'Modifications de {path} abandonnées (trop volumineuses pour être annulées)',
1755
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',
1756
1849
  'undo.menu': 'Supprimés récemment (annuler)',
1757
1850
  'undo.empty': 'Rien à annuler pour le moment',
1758
1851
  'undo.branch': 'Branche',
@@ -1867,6 +1960,8 @@ window.__ModuleLoader__.load({
1867
1960
  'action.resetHard': 'Сбросить сюда (отбросить изменения)',
1868
1961
  'action.copyHash': 'Скопировать номер ревизии',
1869
1962
  'action.details': 'Сведения о коммите',
1963
+ 'menu.sendToChat': 'Отправить в чат',
1964
+ 'menu.sendFilePathToChat': 'Отправить путь к файлу в чат',
1870
1965
  'history.empty': 'В этом репозитории ещё нет коммитов',
1871
1966
  'history.loadMore': 'Загрузить ещё',
1872
1967
  'history.filter': 'Текст или хеш',
@@ -1878,6 +1973,11 @@ window.__ModuleLoader__.load({
1878
1973
  'pane.resize': 'Перетащите, чтобы изменить размер {name} (двойной клик — сброс, стрелки — точная настройка)',
1879
1974
  'diff.binary': 'Двоичный файл',
1880
1975
  'diff.loading': 'Загрузка различий...',
1976
+ 'diff.codeLabel': 'Различия',
1977
+ 'diff.wrapLabel': 'Переносить строки',
1978
+ 'diff.unwrapLabel': 'Сохранять исходные колонки',
1979
+ 'diff.copy': 'Копировать',
1980
+ 'diff.copied': 'Скопировано',
1881
1981
  'toolbar.refresh': 'Обновить',
1882
1982
  'toolbar.newBranch': 'Новая ветка',
1883
1983
  'toolbar.fetch': 'Получить',
@@ -1944,6 +2044,8 @@ window.__ModuleLoader__.load({
1944
2044
  'toast.discarded': 'Изменения в {path} отменены',
1945
2045
  'toast.discardNoUndo': 'Изменения в {path} отменены (слишком велики, отмена не сохранена)',
1946
2046
  'toast.restored': 'Восстановлено',
2047
+ 'toast.sentToChat': 'Отправлено в поле ввода',
2048
+ 'toast.sendCopied': 'Не удалось отправить — скопировано в буфер обмена',
1947
2049
  'undo.menu': 'Недавно удалённое (отменить)',
1948
2050
  'undo.empty': 'Сейчас нечего отменять',
1949
2051
  'undo.branch': 'Ветка',
@@ -2058,6 +2160,8 @@ window.__ModuleLoader__.load({
2058
2160
  'action.resetHard': 'Redefinir aqui (descartar alterações)',
2059
2161
  'action.copyHash': 'Copiar número da revisão',
2060
2162
  'action.details': 'Detalhes do commit',
2163
+ 'menu.sendToChat': 'Enviar para a conversa',
2164
+ 'menu.sendFilePathToChat': 'Enviar caminho do arquivo para a conversa',
2061
2165
  'history.empty': 'Este repositório ainda não tem commits',
2062
2166
  'history.loadMore': 'Carregar mais',
2063
2167
  'history.filter': 'Texto ou hash',
@@ -2069,6 +2173,11 @@ window.__ModuleLoader__.load({
2069
2173
  'pane.resize': 'Arraste para redimensionar {name} (duplo clique restaura, setas ajustam)',
2070
2174
  'diff.binary': 'Arquivo binário',
2071
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',
2072
2181
  'toolbar.refresh': 'Atualizar',
2073
2182
  'toolbar.newBranch': 'Nova branch',
2074
2183
  'toolbar.fetch': 'Buscar',
@@ -2135,6 +2244,8 @@ window.__ModuleLoader__.load({
2135
2244
  'toast.discarded': 'Alterações em {path} descartadas',
2136
2245
  'toast.discardNoUndo': 'Alterações em {path} descartadas (grandes demais para desfazer)',
2137
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',
2138
2249
  'undo.menu': 'Excluídos recentemente (desfazer)',
2139
2250
  'undo.empty': 'Nada para desfazer agora',
2140
2251
  'undo.branch': 'Branch',
@@ -2249,6 +2360,8 @@ window.__ModuleLoader__.load({
2249
2360
  'action.resetHard': 'Ripristina qui (scarta le modifiche)',
2250
2361
  'action.copyHash': 'Copia il numero di revisione',
2251
2362
  'action.details': 'Dettagli del commit',
2363
+ 'menu.sendToChat': 'Invia alla conversazione',
2364
+ 'menu.sendFilePathToChat': 'Invia il percorso del file alla conversazione',
2252
2365
  'history.empty': 'Questo repository non ha ancora commit',
2253
2366
  'history.loadMore': 'Carica altro',
2254
2367
  'history.filter': 'Testo o hash',
@@ -2260,6 +2373,11 @@ window.__ModuleLoader__.load({
2260
2373
  'pane.resize': 'Trascina per ridimensionare {name} (doppio clic ripristina, frecce per regolare)',
2261
2374
  'diff.binary': 'File binario',
2262
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',
2263
2381
  'toolbar.refresh': 'Aggiorna',
2264
2382
  'toolbar.newBranch': 'Nuovo branch',
2265
2383
  'toolbar.fetch': 'Fetch',
@@ -2326,6 +2444,8 @@ window.__ModuleLoader__.load({
2326
2444
  'toast.discarded': 'Modifiche in {path} scartate',
2327
2445
  'toast.discardNoUndo': 'Modifiche in {path} scartate (troppo grandi per essere annullate)',
2328
2446
  'toast.restored': 'Ripristinato',
2447
+ 'toast.sentToChat': 'Inviato nella casella di input',
2448
+ 'toast.sendCopied': 'Impossibile inviare — copiato negli appunti',
2329
2449
  'undo.menu': 'Eliminati di recente (annulla)',
2330
2450
  'undo.empty': 'Niente da annullare al momento',
2331
2451
  'undo.branch': 'Branch',
@@ -2440,6 +2560,8 @@ window.__ModuleLoader__.load({
2440
2560
  'action.resetHard': 'Hierheen resetten (wijzigingen weggooien)',
2441
2561
  'action.copyHash': 'Revisienummer kopiëren',
2442
2562
  'action.details': 'Commit-details',
2563
+ 'menu.sendToChat': 'Naar het gesprek verzenden',
2564
+ 'menu.sendFilePathToChat': 'Bestandspad naar het gesprek verzenden',
2443
2565
  'history.empty': 'Deze repository heeft nog geen commits',
2444
2566
  'history.loadMore': 'Meer laden',
2445
2567
  'history.filter': 'Tekst of hash',
@@ -2451,6 +2573,11 @@ window.__ModuleLoader__.load({
2451
2573
  'pane.resize': 'Sleep om {name} te vergroten (dubbelklik herstelt, pijltjes stellen fijn af)',
2452
2574
  'diff.binary': 'Binair bestand',
2453
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',
2454
2581
  'toolbar.refresh': 'Vernieuwen',
2455
2582
  'toolbar.newBranch': 'Nieuwe branch',
2456
2583
  'toolbar.fetch': 'Fetch',
@@ -2517,6 +2644,8 @@ window.__ModuleLoader__.load({
2517
2644
  'toast.discarded': 'Wijzigingen in {path} weggegooid',
2518
2645
  'toast.discardNoUndo': 'Wijzigingen in {path} weggegooid (te groot om ongedaan te maken)',
2519
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',
2520
2649
  'undo.menu': 'Onlangs verwijderd (ongedaan maken)',
2521
2650
  'undo.empty': 'Nu niets om ongedaan te maken',
2522
2651
  'undo.branch': 'Branch',
@@ -2631,6 +2760,8 @@ window.__ModuleLoader__.load({
2631
2760
  'action.resetHard': 'Przywróć tutaj (odrzuć zmiany)',
2632
2761
  'action.copyHash': 'Kopiuj numer wersji',
2633
2762
  'action.details': 'Szczegóły commita',
2763
+ 'menu.sendToChat': 'Wyślij do rozmowy',
2764
+ 'menu.sendFilePathToChat': 'Wyślij ścieżkę pliku do rozmowy',
2634
2765
  'history.empty': 'To repozytorium nie ma jeszcze commitów',
2635
2766
  'history.loadMore': 'Wczytaj więcej',
2636
2767
  'history.filter': 'Tekst lub hash',
@@ -2642,6 +2773,11 @@ window.__ModuleLoader__.load({
2642
2773
  'pane.resize': 'Przeciągnij, aby zmienić rozmiar {name} (podwójne kliknięcie przywraca, strzałki korygują)',
2643
2774
  'diff.binary': 'Plik binarny',
2644
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',
2645
2781
  'toolbar.refresh': 'Odśwież',
2646
2782
  'toolbar.newBranch': 'Nowa gałąź',
2647
2783
  'toolbar.fetch': 'Pobierz',
@@ -2708,6 +2844,8 @@ window.__ModuleLoader__.load({
2708
2844
  'toast.discarded': 'Odrzucono zmiany w {path}',
2709
2845
  'toast.discardNoUndo': 'Odrzucono zmiany w {path} (zbyt duże, aby zachować cofnięcie)',
2710
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',
2711
2849
  'undo.menu': 'Ostatnio usunięte (cofnij)',
2712
2850
  'undo.empty': 'Teraz nie ma czego cofać',
2713
2851
  'undo.branch': 'Gałąź',
@@ -2822,6 +2960,8 @@ window.__ModuleLoader__.load({
2822
2960
  'action.resetHard': 'Återställ hit (förkasta ändringar)',
2823
2961
  'action.copyHash': 'Kopiera revisionsnummer',
2824
2962
  'action.details': 'Commit-detaljer',
2963
+ 'menu.sendToChat': 'Skicka till samtalet',
2964
+ 'menu.sendFilePathToChat': 'Skicka filsökvägen till samtalet',
2825
2965
  'history.empty': 'Det här arkivet har inga commits ännu',
2826
2966
  'history.loadMore': 'Ladda mer',
2827
2967
  'history.filter': 'Text eller hash',
@@ -2833,6 +2973,11 @@ window.__ModuleLoader__.load({
2833
2973
  'pane.resize': 'Dra för att ändra storleken på {name} (dubbelklick återställer, piltangenter finjusterar)',
2834
2974
  'diff.binary': 'Binär fil',
2835
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',
2836
2981
  'toolbar.refresh': 'Uppdatera',
2837
2982
  'toolbar.newBranch': 'Ny gren',
2838
2983
  'toolbar.fetch': 'Fetch',
@@ -2899,6 +3044,8 @@ window.__ModuleLoader__.load({
2899
3044
  'toast.discarded': 'Ändringar i {path} förkastade',
2900
3045
  'toast.discardNoUndo': 'Ändringar i {path} förkastade (för stora för att ångra)',
2901
3046
  'toast.restored': 'Återställd',
3047
+ 'toast.sentToChat': 'Skickat till inmatningsrutan',
3048
+ 'toast.sendCopied': 'Det gick inte att skicka – kopierade till urklipp istället',
2902
3049
  'undo.menu': 'Nyligen borttaget (ångra)',
2903
3050
  'undo.empty': 'Inget att ångra just nu',
2904
3051
  'undo.branch': 'Gren',
@@ -3013,6 +3160,8 @@ window.__ModuleLoader__.load({
3013
3160
  'action.resetHard': 'Buraya sıfırla (değişiklikleri at)',
3014
3161
  'action.copyHash': 'Revizyon numarasını kopyala',
3015
3162
  'action.details': 'Commit ayrıntıları',
3163
+ 'menu.sendToChat': 'Sohbete gönder',
3164
+ 'menu.sendFilePathToChat': 'Dosya yolunu sohbete gönder',
3016
3165
  'history.empty': 'Bu depoda henüz commit yok',
3017
3166
  'history.loadMore': 'Daha fazla yükle',
3018
3167
  'history.filter': 'Metin veya hash',
@@ -3024,6 +3173,11 @@ window.__ModuleLoader__.load({
3024
3173
  'pane.resize': '{name} boyutunu sürükleyerek ayarlayın (çift tıklama sıfırlar, ok tuşları ince ayar yapar)',
3025
3174
  'diff.binary': 'İkili dosya',
3026
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ı',
3027
3181
  'toolbar.refresh': 'Yenile',
3028
3182
  'toolbar.newBranch': 'Yeni dal',
3029
3183
  'toolbar.fetch': 'Fetch',
@@ -3090,6 +3244,8 @@ window.__ModuleLoader__.load({
3090
3244
  'toast.discarded': '{path} içindeki değişiklikler atıldı',
3091
3245
  'toast.discardNoUndo': '{path} içindeki değişiklikler atıldı (geri alma için fazla büyük)',
3092
3246
  'toast.restored': 'Geri yüklendi',
3247
+ 'toast.sentToChat': 'Giriş kutusuna gönderildi',
3248
+ 'toast.sendCopied': 'Gönderilemedi – bunun yerine panoya kopyalandı',
3093
3249
  'undo.menu': 'Son silinenler (geri al)',
3094
3250
  'undo.empty': 'Şu anda geri alınacak bir şey yok',
3095
3251
  'undo.branch': 'Dal',
@@ -3204,6 +3360,8 @@ window.__ModuleLoader__.load({
3204
3360
  'action.resetHard': 'Reset ke sini (buang perubahan)',
3205
3361
  'action.copyHash': 'Salin nomor revisi',
3206
3362
  'action.details': 'Detail commit',
3363
+ 'menu.sendToChat': 'Kirim ke obrolan',
3364
+ 'menu.sendFilePathToChat': 'Kirim jalur berkas ke obrolan',
3207
3365
  'history.empty': 'Repositori ini belum punya commit',
3208
3366
  'history.loadMore': 'Muat lebih banyak',
3209
3367
  'history.filter': 'Teks atau hash',
@@ -3215,6 +3373,11 @@ window.__ModuleLoader__.load({
3215
3373
  'pane.resize': 'Seret untuk mengubah ukuran {name} (klik ganda mengatur ulang, tombol panah menyesuaikan)',
3216
3374
  'diff.binary': 'Berkas biner',
3217
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',
3218
3381
  'toolbar.refresh': 'Segarkan',
3219
3382
  'toolbar.newBranch': 'Branch baru',
3220
3383
  'toolbar.fetch': 'Fetch',
@@ -3281,6 +3444,8 @@ window.__ModuleLoader__.load({
3281
3444
  'toast.discarded': 'Perubahan di {path} dibuang',
3282
3445
  'toast.discardNoUndo': 'Perubahan di {path} dibuang (terlalu besar untuk diurungkan)',
3283
3446
  'toast.restored': 'Dipulihkan',
3447
+ 'toast.sentToChat': 'Terkirim ke kotak masukan',
3448
+ 'toast.sendCopied': 'Tidak dapat mengirim — disalin ke papan klip sebagai gantinya',
3284
3449
  'undo.menu': 'Baru dihapus (urungkan)',
3285
3450
  'undo.empty': 'Tidak ada yang bisa diurungkan sekarang',
3286
3451
  'undo.branch': 'Branch',
@@ -3395,6 +3560,8 @@ window.__ModuleLoader__.load({
3395
3560
  'action.resetHard': 'Đặt lại về đây (bỏ thay đổi)',
3396
3561
  'action.copyHash': 'Sao chép số hiệu bản sửa',
3397
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',
3398
3565
  'history.empty': 'Kho này chưa có commit nào',
3399
3566
  'history.loadMore': 'Tải thêm',
3400
3567
  'history.filter': 'Văn bản hoặc hash',
@@ -3406,6 +3573,11 @@ window.__ModuleLoader__.load({
3406
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)',
3407
3574
  'diff.binary': 'Tệp nhị phân',
3408
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',
3409
3581
  'toolbar.refresh': 'Làm mới',
3410
3582
  'toolbar.newBranch': 'Nhánh mới',
3411
3583
  'toolbar.fetch': 'Fetch',
@@ -3472,6 +3644,8 @@ window.__ModuleLoader__.load({
3472
3644
  'toast.discarded': 'Đã bỏ thay đổi trong {path}',
3473
3645
  'toast.discardNoUndo': 'Đã bỏ thay đổi trong {path} (quá lớn để giữ hoàn tác)',
3474
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ế',
3475
3649
  'undo.menu': 'Vừa xoá (hoàn tác)',
3476
3650
  'undo.empty': 'Hiện không có gì để hoàn tác',
3477
3651
  'undo.branch': 'Nhánh',
@@ -3586,6 +3760,8 @@ window.__ModuleLoader__.load({
3586
3760
  'action.resetHard': 'إعادة التعيين هنا (مع تجاهل التغييرات)',
3587
3761
  'action.copyHash': 'نسخ رقم المراجعة',
3588
3762
  'action.details': 'تفاصيل الالتزام',
3763
+ 'menu.sendToChat': 'إرسال إلى المحادثة',
3764
+ 'menu.sendFilePathToChat': 'إرسال مسار الملف إلى المحادثة',
3589
3765
  'history.empty': 'لا توجد التزامات في هذا المستودع بعد',
3590
3766
  'history.loadMore': 'تحميل المزيد',
3591
3767
  'history.filter': 'نص أو تجزئة',
@@ -3597,6 +3773,11 @@ window.__ModuleLoader__.load({
3597
3773
  'pane.resize': 'اسحب لتغيير حجم {name} (النقر المزدوج يعيد الضبط، ومفاتيح الأسهم للضبط الدقيق)',
3598
3774
  'diff.binary': 'ملف ثنائي',
3599
3775
  'diff.loading': 'جارٍ تحميل الفروق...',
3776
+ 'diff.codeLabel': 'الفروق',
3777
+ 'diff.wrapLabel': 'التفاف الأسطر',
3778
+ 'diff.unwrapLabel': 'الحفاظ على أعمدة المصدر',
3779
+ 'diff.copy': 'نسخ',
3780
+ 'diff.copied': 'تم النسخ',
3600
3781
  'toolbar.refresh': 'تحديث',
3601
3782
  'toolbar.newBranch': 'فرع جديد',
3602
3783
  'toolbar.fetch': 'جلب',
@@ -3663,6 +3844,8 @@ window.__ModuleLoader__.load({
3663
3844
  'toast.discarded': 'تم تجاهل التغييرات في {path}',
3664
3845
  'toast.discardNoUndo': 'تم تجاهل التغييرات في {path} (أكبر من أن يُحتفظ بالتراجع)',
3665
3846
  'toast.restored': 'تمت الاستعادة',
3847
+ 'toast.sentToChat': 'تم الإرسال إلى مربع الكتابة',
3848
+ 'toast.sendCopied': 'تعذّر الإرسال — تم النسخ إلى الحافظة بدلًا من ذلك',
3666
3849
  'undo.menu': 'المحذوف حديثًا (تراجع)',
3667
3850
  'undo.empty': 'لا شيء لتراجعه الآن',
3668
3851
  'undo.branch': 'فرع',
@@ -3777,6 +3960,8 @@ window.__ModuleLoader__.load({
3777
3960
  'action.resetHard': 'यहाँ रीसेट करें (परिवर्तन छोड़ें)',
3778
3961
  'action.copyHash': 'रिवीज़न संख्या कॉपी करें',
3779
3962
  'action.details': 'कमिट विवरण',
3963
+ 'menu.sendToChat': 'बातचीत में भेजें',
3964
+ 'menu.sendFilePathToChat': 'फ़ाइल पथ बातचीत में भेजें',
3780
3965
  'history.empty': 'इस रिपॉज़िटरी में अभी कोई कमिट नहीं है',
3781
3966
  'history.loadMore': 'और लोड करें',
3782
3967
  'history.filter': 'टेक्स्ट या हैश',
@@ -3788,6 +3973,11 @@ window.__ModuleLoader__.load({
3788
3973
  'pane.resize': '{name} का आकार बदलने के लिए खींचें (डबल-क्लिक रीसेट, तीर कुंजियाँ सूक्ष्म समायोजन)',
3789
3974
  'diff.binary': 'बाइनरी फ़ाइल',
3790
3975
  'diff.loading': 'अंतर लोड हो रहा है...',
3976
+ 'diff.codeLabel': 'अंतर',
3977
+ 'diff.wrapLabel': 'पंक्तियाँ रैप करें',
3978
+ 'diff.unwrapLabel': 'स्रोत कॉलम बनाए रखें',
3979
+ 'diff.copy': 'कॉपी करें',
3980
+ 'diff.copied': 'कॉपी हो गया',
3791
3981
  'toolbar.refresh': 'ताज़ा करें',
3792
3982
  'toolbar.newBranch': 'नई शाखा',
3793
3983
  'toolbar.fetch': 'फ़ेच',
@@ -3854,6 +4044,8 @@ window.__ModuleLoader__.load({
3854
4044
  'toast.discarded': '{path} में परिवर्तन छोड़े गए',
3855
4045
  'toast.discardNoUndo': '{path} में परिवर्तन छोड़े गए (पूर्ववत रखने के लिए बहुत बड़ा)',
3856
4046
  'toast.restored': 'बहाल किया गया',
4047
+ 'toast.sentToChat': 'इनपुट बॉक्स में भेज दिया गया',
4048
+ 'toast.sendCopied': 'भेजा नहीं जा सका — इसके बजाय क्लिपबोर्ड पर कॉपी कर दिया गया',
3857
4049
  'undo.menu': 'हाल में हटाए गए (पूर्ववत करें)',
3858
4050
  'undo.empty': 'अभी पूर्ववत करने के लिए कुछ नहीं',
3859
4051
  'undo.branch': 'शाखा',
@@ -3968,6 +4160,8 @@ window.__ModuleLoader__.load({
3968
4160
  'action.resetHard': 'รีเซ็ตมาที่นี่ (ทิ้งการเปลี่ยนแปลง)',
3969
4161
  'action.copyHash': 'คัดลอกเลขรีวิชัน',
3970
4162
  'action.details': 'รายละเอียดคอมมิต',
4163
+ 'menu.sendToChat': 'ส่งไปยังบทสนทนา',
4164
+ 'menu.sendFilePathToChat': 'ส่งพาธไฟล์ไปยังบทสนทนา',
3971
4165
  'history.empty': 'รีโพซิทอรีนี้ยังไม่มีคอมมิต',
3972
4166
  'history.loadMore': 'โหลดเพิ่ม',
3973
4167
  'history.filter': 'ข้อความหรือแฮช',
@@ -3979,6 +4173,11 @@ window.__ModuleLoader__.load({
3979
4173
  'pane.resize': 'ลากเพื่อปรับขนาด {name} (ดับเบิลคลิกเพื่อรีเซ็ต ปุ่มลูกศรปรับละเอียด)',
3980
4174
  'diff.binary': 'ไฟล์ไบนารี',
3981
4175
  'diff.loading': 'กำลังโหลดความต่าง...',
4176
+ 'diff.codeLabel': 'ความต่าง',
4177
+ 'diff.wrapLabel': 'ตัดบรรทัด',
4178
+ 'diff.unwrapLabel': 'คงความกว้างคอลัมน์เดิม',
4179
+ 'diff.copy': 'คัดลอก',
4180
+ 'diff.copied': 'คัดลอกแล้ว',
3982
4181
  'toolbar.refresh': 'รีเฟรช',
3983
4182
  'toolbar.newBranch': 'แบรนช์ใหม่',
3984
4183
  'toolbar.fetch': 'Fetch',
@@ -4045,6 +4244,8 @@ window.__ModuleLoader__.load({
4045
4244
  'toast.discarded': 'ทิ้งการเปลี่ยนแปลงใน {path} แล้ว',
4046
4245
  'toast.discardNoUndo': 'ทิ้งการเปลี่ยนแปลงใน {path} แล้ว (ใหญ่เกินกว่าจะเก็บไว้เลิกทำ)',
4047
4246
  'toast.restored': 'กู้คืนแล้ว',
4247
+ 'toast.sentToChat': 'ส่งไปที่กล่องป้อนข้อความแล้ว',
4248
+ 'toast.sendCopied': 'ส่งไม่สำเร็จ – คัดลอกไปยังคลิปบอร์ดแทนแล้ว',
4048
4249
  'undo.menu': 'ลบล่าสุด (เลิกทำ)',
4049
4250
  'undo.empty': 'ตอนนี้ไม่มีอะไรให้เลิกทำ',
4050
4251
  'undo.branch': 'แบรนช์',
@@ -4245,31 +4446,92 @@ window.__ModuleLoader__.load({
4245
4446
  is still essentially correct, so it seeds the states and a background
4246
4447
  refresh lands the fresh data a moment later. Pure functions; the panel
4247
4448
  only ever reads them at effect time and writes after a fetch. */
4248
- 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
4249
4459
  const paintRepos = new Map() /* cwd -> { at, data } */
4250
4460
  const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
4251
4461
  const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
4252
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
+
4253
4511
  function readPaintRepos(cwd) {
4512
+ hydratePaint()
4254
4513
  const entry = paintRepos.get(cwd)
4255
- 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
4256
4515
  return entry.data
4257
4516
  }
4258
4517
 
4259
4518
  function writePaintRepos(cwd, data) {
4260
4519
  paintRepos.set(cwd, { at: Date.now(), data: data })
4261
4520
  if (paintRepos.size > 16) paintRepos.delete(paintRepos.keys().next().value)
4521
+ persistPaint()
4262
4522
  }
4263
4523
 
4264
4524
  function readPaintPage(repoRoot) {
4525
+ hydratePaint()
4265
4526
  const entry = paintPage.get(repoRoot)
4266
- 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
4267
4528
  return entry
4268
4529
  }
4269
4530
 
4270
4531
  function writePaintPage(repoRoot, page) {
4271
4532
  paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
4272
4533
  if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
4534
+ persistPaint()
4273
4535
  }
4274
4536
 
4275
4537
  function rememberPaintRoot(cwd, repoRoot) {
@@ -4282,6 +4544,38 @@ window.__ModuleLoader__.load({
4282
4544
 
4283
4545
  /* ============================== storage ============================== */
4284
4546
 
4547
+ /* ---- tree UI state (v0.13.1): the session-scoped seats remount on every
4548
+ session switch, and component state died with them — branch sections
4549
+ snapped back to defaults and folders re-folded. One localStorage home,
4550
+ GLOBAL by design: which sections you keep folded is a habit, not a
4551
+ property of the repository. ---- */
4552
+ const TREE_KEY = 'dsh-ide-git.tree.v1'
4553
+
4554
+ function readTreeState() {
4555
+ try {
4556
+ const raw = window.localStorage.getItem(TREE_KEY)
4557
+ if (raw === null || raw === '') return null
4558
+ const parsed = JSON.parse(raw)
4559
+ return parsed !== null && typeof parsed === 'object' ? parsed : null
4560
+ } catch (error) { void error }
4561
+ return null
4562
+ }
4563
+
4564
+ function writeTreeState(state) {
4565
+ try { window.localStorage.setItem(TREE_KEY, JSON.stringify(state)) } catch (error) { void error }
4566
+ }
4567
+
4568
+ function treeSectionDefaults() {
4569
+ return { favorites: false, remote: false, tags: true }
4570
+ }
4571
+
4572
+ function treeSectionsOf(stored) {
4573
+ const sections = stored !== null && typeof stored === 'object' && stored.sections !== null && typeof stored.sections === 'object'
4574
+ ? stored.sections
4575
+ : {}
4576
+ return Object.assign(treeSectionDefaults(), sections)
4577
+ }
4578
+
4285
4579
  /* ---- ai commit pickers (issue #7): the model catalog and per-model
4286
4580
  reasoning efforts, served by the host's commit-models / commit-efforts
4287
4581
  routes. Cached for a short window so the settings card can mount twice
@@ -4663,12 +4957,19 @@ window.__ModuleLoader__.load({
4663
4957
  is stored is a RATIO of the measured container: resizing the dock or the
4664
4958
  window rescales the panes instead of breaking the layout. */
4665
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). */
4666
4965
  const PANE_CHROME_KEYS = {
4667
- columns: ['tree', 'changes'],
4966
+ columns: ['tree', 'changes', 'diff'],
4668
4967
  stack: ['tree', 'changes', 'diff'],
4669
4968
  compact: ['tree', 'diff'],
4670
4969
  }
4671
- /* 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). */
4672
4973
  const PANE_HEIGHT_CHROME = { columns: false, stack: true, compact: true }
4673
4974
  /* The pane a separator resizes, and the key its label is translated from. */
4674
4975
  const PANE_NAME_KEYS = { tree: 'toolbar.tree', changes: 'changes.title', diff: 'pane.diff' }
@@ -4677,6 +4978,7 @@ window.__ModuleLoader__.load({
4677
4978
  const PANE_LIMITS = {
4678
4979
  'columns:tree': { min: 140, max: 560 },
4679
4980
  'columns:changes': { min: 200, max: 640 },
4981
+ 'columns:diff': { min: 120, max: 900 },
4680
4982
  'stack:tree': { min: 100, max: 420 },
4681
4983
  'stack:changes': { min: 120, max: 460 },
4682
4984
  'stack:diff': { min: 120, max: 900 },
@@ -4685,10 +4987,12 @@ window.__ModuleLoader__.load({
4685
4987
  }
4686
4988
  /* Today's fixed sizes stay the default — and stay in the stylesheet: a pane
4687
4989
  the user never dragged keeps exactly the look it had before, while a
4688
- 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. */
4689
4992
  const PANE_DEFAULTS = {
4690
4993
  'columns:tree': { px: 200 },
4691
4994
  'columns:changes': { px: 290 },
4995
+ 'columns:diff': { ratio: 0.55 },
4692
4996
  'stack:tree': { ratio: 0.36 },
4693
4997
  'stack:changes': { ratio: 0.46 },
4694
4998
  'compact:tree': { ratio: 0.42 },
@@ -4791,9 +5095,14 @@ window.__ModuleLoader__.load({
4791
5095
  const effective = {}
4792
5096
  const limits = {}
4793
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)
4794
5103
  for (const key of keys) {
4795
5104
  const spec = PANE_DEFAULTS[chrome + ':' + key]
4796
- 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)
4797
5106
  const live = measured[key]
4798
5107
  effective[key] = typeof live === 'number' && Number.isFinite(live) && live > 0 ? live : fallback
4799
5108
  }
@@ -4808,7 +5117,14 @@ window.__ModuleLoader__.load({
4808
5117
  const current = sizeOf(key)
4809
5118
  let ceiling = spec.max
4810
5119
  if (key === 'diff') {
4811
- 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
4812
5128
  ceiling = Math.min(ceiling, main - PANE_HISTORY_MIN_H)
4813
5129
  } else {
4814
5130
  let taken = 0
@@ -4830,7 +5146,7 @@ window.__ModuleLoader__.load({
4830
5146
  limits[key] = { min: spec.min, max: high }
4831
5147
  const ratio = ratios[key]
4832
5148
  if (typeof ratio === 'number' && Number.isFinite(ratio)) {
4833
- 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))
4834
5150
  overrides[key] = px
4835
5151
  effective[key] = px
4836
5152
  }
@@ -5088,6 +5404,120 @@ window.__ModuleLoader__.load({
5088
5404
  return E('svg', { className: 'dig-graph', width: width, height: height, viewBox: '0 0 ' + width + ' ' + height }, children)
5089
5405
  }
5090
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
+
5091
5521
  /* ============================== diff ============================== */
5092
5522
 
5093
5523
  function diffLines(patch) {
@@ -5118,11 +5548,47 @@ window.__ModuleLoader__.load({
5118
5548
  return out
5119
5549
  }
5120
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
+
5121
5571
  function DiffBody(props) {
5122
5572
  const rows = useMemo(() => diffLines(props.patch), [props.patch])
5123
5573
  if (props.loading === true) return E('div', { className: 'dig-empty' }, props.t('diff.loading'))
5124
5574
  if (props.binary === true) return E('div', { className: 'dig-empty' }, props.t('diff.binary'))
5125
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
+ }
5126
5592
  return E('div', { className: 'dig-diff' }, rows.map((row, index) => E('div', { key: index, className: 'dig-diff-line dig-diff-' + row.kind },
5127
5593
  E('span', { className: 'dig-diff-gutter' }, row.oldLine === null ? '' : String(row.oldLine)),
5128
5594
  E('span', { className: 'dig-diff-gutter' }, row.newLine === null ? '' : String(row.newLine)),
@@ -5130,6 +5596,22 @@ window.__ModuleLoader__.load({
5130
5596
  )))
5131
5597
  }
5132
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
+
5133
5615
  /* ============================== ui kit ============================== */
5134
5616
 
5135
5617
  const ICONS = {
@@ -5175,6 +5657,9 @@ window.__ModuleLoader__.load({
5175
5657
  split: ['M2.5 3h11v10h-11z', 'M8 3v10'],
5176
5658
  expand: ['M4.5 6.2 8 9.7l3.5-3.5', 'M3 12.4h10'],
5177
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'],
5178
5663
  }
5179
5664
 
5180
5665
  function Icon(props) {
@@ -6022,9 +6507,23 @@ window.__ModuleLoader__.load({
6022
6507
  function BranchTree(props) {
6023
6508
  const t = props.t
6024
6509
  const [filter, setFilter] = useState('')
6025
- const [collapsed, setCollapsed] = useState({ favorites: false, remote: false, tags: true })
6510
+ const [collapsed, setCollapsed] = useState(() => treeSectionsOf(readTreeState()))
6026
6511
  const [picked, setPicked] = useState('')
6027
- const [foldedFolders, setFoldedFolders] = useState({})
6512
+ const [foldedFolders, setFoldedFolders] = useState(() => {
6513
+ const stored = readTreeState()
6514
+ return stored !== null && typeof stored === 'object' && stored.folders !== null && typeof stored.folders === 'object'
6515
+ ? stored.folders
6516
+ : {}
6517
+ })
6518
+ /* Survive the remount a session switch performs (v0.13.1). */
6519
+ useEffect(() => {
6520
+ const stored = readTreeState()
6521
+ writeTreeState(Object.assign({}, stored, { sections: collapsed }))
6522
+ }, [collapsed])
6523
+ useEffect(() => {
6524
+ const stored = readTreeState()
6525
+ writeTreeState(Object.assign({}, stored, { folders: foldedFolders }))
6526
+ }, [foldedFolders])
6028
6527
  const branches = props.branches
6029
6528
  const needle = filter.trim().toLowerCase()
6030
6529
  const match = (name) => needle === '' || String(name).toLowerCase().indexOf(needle) >= 0
@@ -6122,6 +6621,7 @@ window.__ModuleLoader__.load({
6122
6621
  onClick: () => props.onDiff(item, props.group),
6123
6622
  onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, item, props.group) },
6124
6623
  },
6624
+ FileTypeGlyph({ path: item.path }),
6125
6625
  E('span', { className: 'dig-file-status dig-file-status-' + (status === '?' ? 'U' : status) }, status),
6126
6626
  // An ignored DIRECTORY comes back as "dist/" (ls-files --directory), whose
6127
6627
  // basename is empty — show the whole path instead of a blank label.
@@ -6140,7 +6640,15 @@ window.__ModuleLoader__.load({
6140
6640
  const summary = props.summary
6141
6641
  const [message, setMessage] = useState('')
6142
6642
  const [amend, setAmend] = useState(false)
6143
- const [collapsed, setCollapsed] = useState(false)
6643
+ const [collapsed, setCollapsed] = useState(() => {
6644
+ const stored = readTreeState()
6645
+ return stored !== null && typeof stored === 'object' && stored.changesCollapsed === true
6646
+ })
6647
+ /* Same remount-survival as the branch sections (v0.13.1). */
6648
+ useEffect(() => {
6649
+ const stored = readTreeState()
6650
+ writeTreeState(Object.assign({}, stored, { changesCollapsed: collapsed === true }))
6651
+ }, [collapsed])
6144
6652
  /* AI-written message (issue #6). The draft is kept so the write can be
6145
6653
  undone, and an existing draft is never overwritten without a word. */
6146
6654
  const [aiBusy, setAiBusy] = useState(false)
@@ -6557,7 +7065,9 @@ window.__ModuleLoader__.load({
6557
7065
  className: 'dig-row dig-row-file' + (props.selectedPath === file.path ? ' dig-row-selected' : ''),
6558
7066
  title: file.path,
6559
7067
  onClick: () => props.onSelectFile(file),
7068
+ onContextMenu: props.onFileMenu === undefined ? undefined : (event) => { event.preventDefault(); props.onFileMenu(event, file) },
6560
7069
  },
7070
+ FileTypeGlyph({ path: file.path }),
6561
7071
  E('span', { className: 'dig-row-label' }, baseName(file.path)),
6562
7072
  E('span', { className: 'dig-row-sub dig-row-dir' }, dirName(file.path)),
6563
7073
  file.additions > 0 ? E('span', { className: 'dig-stat-add' }, '+' + file.additions) : null,
@@ -6620,7 +7130,12 @@ window.__ModuleLoader__.load({
6620
7130
  const sessionId = typeof scope.sessionId === 'string' ? scope.sessionId : 'default'
6621
7131
  const base = useMemo(() => ({ cwd: cwd, repoRoot: repoRoot === null ? undefined : repoRoot }), [cwd, repoRoot])
6622
7132
 
6623
- useEffect(() => {
7133
+ /* Layout effect ON PURPOSE: the chrome (compact/columns/stack) and the
7134
+ rail capacity derive from these numbers, and a paint-then-correct
7135
+ cycle showed up on every seat switch as buttons lighting up one by
7136
+ one (v0.13.1 report). Measuring in the commit phase keeps the first
7137
+ paint on the final chrome. */
7138
+ useMeasureLayoutEffect(() => {
6624
7139
  const element = hostRef.current
6625
7140
  if (element === null) return undefined
6626
7141
  const measure = () => {
@@ -6664,7 +7179,7 @@ window.__ModuleLoader__.load({
6664
7179
  // Like the rail, the panes measure the box that actually holds them instead
6665
7180
  // of trusting the root probe: the column chrome keeps a vertical rail
6666
7181
  // beside the body, and the header sits above it.
6667
- useEffect(() => {
7182
+ useMeasureLayoutEffect(() => {
6668
7183
  const element = bodyRef.current
6669
7184
  if (element === null) { setBodyBox({ width: 0, height: 0 }); return undefined }
6670
7185
  const measure = () => {
@@ -6702,7 +7217,10 @@ window.__ModuleLoader__.load({
6702
7217
  if (element === null) return found
6703
7218
  for (const node of element.querySelectorAll('[data-pane]')) {
6704
7219
  const rect = node.getBoundingClientRect()
6705
- 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)
6706
7224
  }
6707
7225
  return found
6708
7226
  }
@@ -6739,7 +7257,8 @@ window.__ModuleLoader__.load({
6739
7257
  setPaneSizes((current) => (samePaneSizes(current, live) ? current : live))
6740
7258
  const layout = paneGeometry(chrome, paneBox, panesRef.current, live)
6741
7259
  const limit = layout.limits[key]
6742
- 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)
6743
7262
  if (limit === undefined || !(reference > 0)) return
6744
7263
  const px = Math.round(Math.min(Math.max(rawPx, limit.min), limit.max))
6745
7264
  commitPanes(withPaneRatio(panesRef.current, layout.bucket, key, px / reference))
@@ -6769,7 +7288,9 @@ window.__ModuleLoader__.load({
6769
7288
  const paneStyle = (key) => {
6770
7289
  const px = paneLayout.overrides[key]
6771
7290
  if (px === undefined) return null
6772
- 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' }
6773
7294
  }
6774
7295
 
6775
7296
  /* A divider is a control, so it only exists when it can actually move
@@ -6783,7 +7304,10 @@ window.__ModuleLoader__.load({
6783
7304
  key: 'gutter-' + key,
6784
7305
  t: t,
6785
7306
  nameKey: PANE_NAME_KEYS[key],
6786
- 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,
6787
7311
  side: side,
6788
7312
  active: paneDragging === key,
6789
7313
  // Now is clamped into [min, max]: an aria window the panel contradicts is
@@ -6802,7 +7326,9 @@ window.__ModuleLoader__.load({
6802
7326
  // function of the strip that holds them (a 1200x300 workbench and a 300x900
6803
7327
  // sidebar disagree about that). railBox keeps the last measured span; until
6804
7328
  // the first measurement lands, every action is rendered.
6805
- useEffect(() => {
7329
+ // Layout effect (see the host measure above): the button count derives
7330
+ // from this box, and a late measure reads as the rail lighting up late.
7331
+ useMeasureLayoutEffect(() => {
6806
7332
  const element = railHostRef.current
6807
7333
  if (element === null) { setRailBox({ width: 0, height: 0 }); return undefined }
6808
7334
  const measure = () => {
@@ -7019,6 +7545,71 @@ window.__ModuleLoader__.load({
7019
7545
  })
7020
7546
  }, [])
7021
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
+
7022
7613
  const branchMenu = useCallback((event, entry) => {
7023
7614
  const current = branches === null ? '' : branches.branch
7024
7615
  const isLocal = entry.remote !== true && entry.tag !== true
@@ -7028,6 +7619,7 @@ window.__ModuleLoader__.load({
7028
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,
7029
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,
7030
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 }) },
7031
7623
  null,
7032
7624
  { id: 'favorite', icon: 'star', tone: 'warn', label: t('action.favorite'), active: favorites.indexOf(entry.name) >= 0, run: () => toggleFavoriteBranch(entry.name) },
7033
7625
  { id: 'newBranch', icon: 'plus', tone: 'success', label: t('action.newBranchFrom'), run: () => setDialog({ kind: 'newBranch', from: entry.name }) },
@@ -7038,12 +7630,14 @@ window.__ModuleLoader__.load({
7038
7630
  { id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) },
7039
7631
  ].filter((item) => item !== null)
7040
7632
  openMenuAt(event, items)
7041
- }, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked])
7633
+ }, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendToComposer])
7042
7634
 
7043
7635
  const commitMenu = useCallback((event, commit) => {
7636
+ const shortHash = commit.hash.slice(0, 7)
7044
7637
  const items = [
7045
7638
  { id: 'details', icon: 'file', tone: 'primary', label: t('action.details'), run: () => { void selectCommit(commit) } },
7046
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 }) },
7047
7641
  null,
7048
7642
  { id: 'checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout'), run: () => setDialog({ kind: 'checkoutCommit', hash: commit.hash }) },
7049
7643
  { id: 'branch', icon: 'plus', tone: 'success', label: t('action.newBranchHere'), run: () => setDialog({ kind: 'newBranch', from: commit.hash }) },
@@ -7056,11 +7650,13 @@ window.__ModuleLoader__.load({
7056
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' }) },
7057
7651
  ]
7058
7652
  openMenuAt(event, items)
7059
- }, [run, selectCommit, t, openMenuAt, blocked])
7653
+ }, [run, selectCommit, t, openMenuAt, blocked, sendToComposer])
7060
7654
 
7061
7655
  const changeMenu = useCallback((event, item, group) => {
7062
7656
  const items = [
7063
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 }) },
7064
7660
  null,
7065
7661
  group === 'staged'
7066
7662
  ? { id: 'unstage', icon: 'minus', tone: 'warn', label: t('action.unstage'), run: () => { void run('unstage', { paths: [item.path] }) } }
@@ -7070,7 +7666,20 @@ window.__ModuleLoader__.load({
7070
7666
  { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(item.path) } },
7071
7667
  ]
7072
7668
  openMenuAt(event, items)
7073
- }, [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])
7074
7683
 
7075
7684
  const submitDialog = useCallback(async (state, value) => {
7076
7685
  setDialog(null)
@@ -7081,27 +7690,6 @@ window.__ModuleLoader__.load({
7081
7690
 
7082
7691
  /* ---------- toasts + undo ---------- */
7083
7692
 
7084
- const toastTimers = useRef([])
7085
- const toastSeq = useRef(0)
7086
-
7087
- useEffect(() => () => {
7088
- for (const timer of toastTimers.current) clearTimeout(timer)
7089
- toastTimers.current = []
7090
- }, [])
7091
-
7092
- const dropToast = useCallback((id) => {
7093
- setToasts((list) => list.filter((item) => item.id !== id))
7094
- }, [])
7095
-
7096
- const pushToast = useCallback((toast) => {
7097
- toastSeq.current += 1
7098
- const id = 'toast-' + String(toastSeq.current)
7099
- setToasts((list) => list.concat([Object.assign({ id: id, onClose: () => dropToast(id) }, toast)]))
7100
- const ttl = toast.ttl === undefined ? (toast.actionLabel === undefined ? 9000 : 30000) : toast.ttl
7101
- if (ttl > 0) toastTimers.current.push(setTimeout(() => dropToast(id), ttl))
7102
- return id
7103
- }, [dropToast])
7104
-
7105
7693
  // The delete really happened; the host kept an object id for a while, and this
7106
7694
  // asks it to recreate the branch / stash entry from that id.
7107
7695
  const undoAction = useCallback(async (id) => {
@@ -7276,6 +7864,7 @@ window.__ModuleLoader__.load({
7276
7864
  setSelectedPath(file.path)
7277
7865
  void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
7278
7866
  },
7867
+ onFileMenu: detailFileMenu,
7279
7868
  })
7280
7869
  : E(HistoryList, {
7281
7870
  t: t, commits: commits, hasMore: hasMore, busy: busy, selectedHash: selectedHash,
@@ -7979,6 +8568,14 @@ window.__ModuleLoader__.load({
7979
8568
  '.dig-diff-del{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 16%, transparent)}',
7980
8569
  '.dig-diff-hunk{color:var(--dsw-alias-brand-primary);background:color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent)}',
7981
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}',
7982
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}',
7983
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))}',
7984
8581
  '.dig-dialog-wide{min-width:min(360px,92%)}',
@@ -8041,7 +8638,7 @@ window.__ModuleLoader__.load({
8041
8638
  const openSettings = navigation !== undefined && navigation !== null && typeof navigation.openBundle === 'function'
8042
8639
  ? () => { navigation.openBundle(SETTINGS_BUNDLE) }
8043
8640
  : null
8044
- 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 })
8045
8642
  }
8046
8643
 
8047
8644
  /* The conversation-area seat (issue #8): the same panel as a view tab
@@ -8079,6 +8676,69 @@ window.__ModuleLoader__.load({
8079
8676
  const ctx = props.ctx
8080
8677
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
8081
8678
  const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
8679
+ /* The active conversation's view area is `flex: 1 0 auto; min-height:
8680
+ auto` — it GROWS with content and scrolls inside the shared
8681
+ conversation scrollport. An auto-height panel there measured
8682
+ content-tall (a 120-commit history, a long diff), so the width-vs-
8683
+ height chrome test flipped the wide seat into the stacked sidebar
8684
+ layout and everything shifted downward (v0.13.1 report). Pin the seat
8685
+ to the scrollport's VISIBLE height instead: the panel fills exactly
8686
+ the on-screen area and scrolls internally, the way the sidebar seats
8687
+ behave. */
8688
+ const seatRef = useRef(null)
8689
+ const [seatHeight, setSeatHeight] = useState(null)
8690
+ useEffect(() => {
8691
+ const el = seatRef.current
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 }
8707
+ const measure = () => {
8708
+ let scroll = null
8709
+ try { scroll = el.closest('[data-conversation-scroll]') } catch (error) { void error }
8710
+ if (scroll === null || scroll === undefined) return
8711
+ const offset = Math.max(0, el.getBoundingClientRect().top - scroll.getBoundingClientRect().top)
8712
+ const available = Math.round(scroll.clientHeight - offset - 8) - leftover.current
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
+ })
8722
+ }
8723
+ measure()
8724
+ const raf = requestAnimationFrame(() => { measure() })
8725
+ const late = setTimeout(() => { measure() }, 400)
8726
+ let observer
8727
+ try {
8728
+ const scroll = el.closest('[data-conversation-scroll]')
8729
+ if (scroll !== null && scroll !== undefined && typeof ResizeObserver === 'function') {
8730
+ observer = new ResizeObserver(() => { measure() })
8731
+ observer.observe(scroll)
8732
+ }
8733
+ } catch (error) { void error }
8734
+ window.addEventListener('resize', measure)
8735
+ return () => {
8736
+ cancelAnimationFrame(raf)
8737
+ clearTimeout(late)
8738
+ if (observer !== undefined) observer.disconnect()
8739
+ window.removeEventListener('resize', measure)
8740
+ }
8741
+ }, [])
8082
8742
  useEffect(() => {
8083
8743
  let alive = true
8084
8744
  const sync = () => { if (alive === true) setCwd(sessionCwdOf(ctx, sessionId)) }
@@ -8102,7 +8762,11 @@ window.__ModuleLoader__.load({
8102
8762
  }
8103
8763
  }, [ctx, sessionId])
8104
8764
  const scope = useMemo(() => ({ cwd: cwd, sessionId: sessionId }), [cwd, sessionId])
8105
- return E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true })
8765
+ return E('div', {
8766
+ ref: seatRef,
8767
+ className: 'dig-conversation-seat',
8768
+ style: seatHeight === null ? undefined : { height: seatHeight + 'px' },
8769
+ }, E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true }))
8106
8770
  }, (prev, next) => prev.sessionId === next.sessionId && prev.t === next.t && prev.ctx === next.ctx)
8107
8771
 
8108
8772
  function NativePanel(props) {