dsh-ide-git 0.13.5 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.13.5",
3
+ "version": "0.14.0",
4
4
  "main": "./src/index.js",
5
5
  "description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
6
6
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ide-git",
3
- "version": "0.13.5",
3
+ "version": "0.14.0",
4
4
  "description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
5
5
  "type": "module",
6
6
  "repository": {
package/src/client.js CHANGED
@@ -195,6 +195,10 @@ window.__ModuleLoader__.load({
195
195
  'settings.placement.title': '面板放置',
196
196
  'settings.placement.tab': '在主对话区显示 Git 标签页',
197
197
  'settings.placement.tabHint': '关闭后仍可从右侧栏或底部面板打开。',
198
+ 'settings.placement.immersive': '沉浸模式(占满全高,隐藏输入框)',
199
+ 'settings.placement.immersiveHint': '开启后 Git 标签页占满整个会话区高度,输入框自动收起;审批与提问仍会正常显示。',
200
+ 'preview.title': 'Git 预览',
201
+ 'preview.hint': '点击文件或提交的「显示差异」,差异会在侧边栏这里打开。',
198
202
  'settings.rail.title': '动作条按钮',
199
203
  'settings.rail.hint': '勾选要显示在动作条上的动作;顺序固定为默认顺序,取消勾选即隐藏。',
200
204
  'settings.rail.unavailable': '当前宿主没有设置服务,请用面板内的「动作条设置」。',
@@ -406,6 +410,10 @@ window.__ModuleLoader__.load({
406
410
  'settings.placement.title': 'Placement',
407
411
  'settings.placement.tab': 'Show the Git tab in the conversation area',
408
412
  'settings.placement.tabHint': 'Off, the panel stays reachable from the right sidebar and the bottom panel.',
413
+ 'settings.placement.immersive': 'Immersive mode (full height, hidden composer)',
414
+ 'settings.placement.immersiveHint': 'On, the Git tab fills the whole conversation area and the composer steps aside; approvals and questions still show.',
415
+ 'preview.title': 'Git Preview',
416
+ 'preview.hint': 'Click “Show diff” on a file or commit — the diff opens here in the sidebar.',
409
417
  'settings.rail.title': 'Action rail buttons',
410
418
  'settings.rail.hint': 'Tick the actions shown on the rail. The order is fixed; unticking one hides it.',
411
419
  'settings.rail.unavailable': 'This host has no settings service; use the in-panel “Action rail settings”.',
@@ -625,6 +633,10 @@ window.__ModuleLoader__.load({
625
633
  'settings.placement.title': '面板放置',
626
634
  'settings.placement.tab': '喺主對話區顯示 Git 分頁',
627
635
  'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
636
+ 'settings.placement.immersive': '沉浸模式(佔滿全高,隱藏輸入框)',
637
+ 'settings.placement.immersiveHint': '開啟之後 Git 分頁會佔滿整個會話區高度,輸入框自動收起;審批與提問仍會正常顯示。',
638
+ 'preview.title': 'Git 預覽',
639
+ 'preview.hint': '喺檔案或提交撳「顯示差異」,差異就會喺側邊欄度開啟。',
628
640
  'settings.rail.title': '動作列按鈕',
629
641
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
630
642
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -836,6 +848,10 @@ window.__ModuleLoader__.load({
836
848
  'settings.placement.title': '面板放置',
837
849
  'settings.placement.tab': '在主對話區顯示 Git 分頁',
838
850
  'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
851
+ 'settings.placement.immersive': '沉浸模式(佔滿全高,隱藏輸入框)',
852
+ 'settings.placement.immersiveHint': '開啟之後 Git 分頁會佔滿整個對話區高度,輸入框自動收起;審核與提問仍會正常顯示。',
853
+ 'preview.title': 'Git 預覽',
854
+ 'preview.hint': '點選檔案或提交的「顯示差異」,差異就會在側邊欄開啟。',
839
855
  'settings.rail.title': '動作列按鈕',
840
856
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
841
857
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -1047,6 +1063,10 @@ window.__ModuleLoader__.load({
1047
1063
  'settings.placement.title': '面板放置',
1048
1064
  'settings.placement.tab': '喺主對話區顯示 Git 分頁',
1049
1065
  'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
1066
+ 'settings.placement.immersive': '沉浸模式(佔滿全高,隱藏輸入框)',
1067
+ 'settings.placement.immersiveHint': '開啟之後 Git 分頁會佔滿整個會話區高度,輸入框自動收起;審批與提問仍會正常顯示。',
1068
+ 'preview.title': 'Git 預覽',
1069
+ 'preview.hint': '喺檔案或提交撳「顯示差異」,差異就會喺側邊欄度開啟。',
1050
1070
  'settings.rail.title': '動作列按鈕',
1051
1071
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
1052
1072
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -1258,6 +1278,10 @@ window.__ModuleLoader__.load({
1258
1278
  'settings.placement.title': 'パネル配置',
1259
1279
  'settings.placement.tab': '会話エリアに Git タブを表示',
1260
1280
  'settings.placement.tabHint': 'オフにしても右サイドバーと下部パネルから開けます。',
1281
+ 'settings.placement.immersive': '没入モード(全高表示・入力欄を非表示)',
1282
+ 'settings.placement.immersiveHint': 'オンにすると Git タブが会話エリアの高さいっぱいに広がり、入力欄は引っ込みます。承認や質問は通常どおり表示されます。',
1283
+ 'preview.title': 'Git プレビュー',
1284
+ 'preview.hint': 'ファイルやコミットの「差分を表示」をクリックすると、差分がここに開きます。',
1261
1285
  'settings.rail.title': 'アクションレールのボタン',
1262
1286
  'settings.rail.hint': 'レールに表示する操作にチェックを入れてください。順序は固定で、チェックを外すと非表示になります。',
1263
1287
  'settings.rail.unavailable': 'このホストには設定サービスがないため、パネル内の「アクションレール設定」を使ってください。',
@@ -1469,6 +1493,10 @@ window.__ModuleLoader__.load({
1469
1493
  'settings.placement.title': '패널 배치',
1470
1494
  'settings.placement.tab': '대화 영역에 Git 탭 표시',
1471
1495
  'settings.placement.tabHint': '꺼도 오른쪽 사이드바와 하단 패널에서 열 수 있습니다.',
1496
+ 'settings.placement.immersive': '몰입 모드(전체 높이, 입력창 숨김)',
1497
+ 'settings.placement.immersiveHint': '켜면 Git 탭이 대화 영역 전체 높이를 차지하고 입력창은 숨겨집니다. 승인과 질문은 정상적으로 표시됩니다.',
1498
+ 'preview.title': 'Git 미리보기',
1499
+ 'preview.hint': '파일이나 커밋의 「차이 보기」를 클릭하면 차이가 사이드바 여기에 열립니다.',
1472
1500
  'settings.rail.title': '액션 레일 버튼',
1473
1501
  'settings.rail.hint': '레일에 표시할 동작을 선택하세요. 순서는 고정이며 선택을 해제하면 숨겨집니다.',
1474
1502
  'settings.rail.unavailable': '이 호스트에는 설정 서비스가 없으므로 패널의 "액션 레일 설정"을 사용하세요.',
@@ -1680,6 +1708,10 @@ window.__ModuleLoader__.load({
1680
1708
  'settings.placement.title': 'Panel-Platzierung',
1681
1709
  'settings.placement.tab': 'Git-Tab im Gesprächsbereich anzeigen',
1682
1710
  'settings.placement.tabHint': 'Ausgeschaltet bleibt das Panel über rechte Seitenleiste und unteres Panel erreichbar.',
1711
+ 'settings.placement.immersive': 'Immersiver Modus (volle Höhe, ausgeblendetes Eingabefeld)',
1712
+ 'settings.placement.immersiveHint': 'Eingeschaltet füllt der Git-Tab die gesamte Gesprächshöhe und das Eingabefeld weicht zurück; Freigaben und Rückfragen bleiben sichtbar.',
1713
+ 'preview.title': 'Git-Vorschau',
1714
+ 'preview.hint': '„Diff anzeigen“ an einer Datei oder einem Commit öffnet den Diff hier in der Seitenleiste.',
1683
1715
  'settings.rail.title': 'Schaltflächen der Aktionsleiste',
1684
1716
  'settings.rail.hint': 'Die auf der Leiste sichtbaren Aktionen ankreuzen. Die Reihenfolge ist fest; abwählen blendet eine aus.',
1685
1717
  'settings.rail.unavailable': 'Dieser Host hat keinen Einstellungsdienst; nutze die „Aktionsleisten-Einstellungen“ im Panel.',
@@ -1891,6 +1923,10 @@ window.__ModuleLoader__.load({
1891
1923
  'settings.placement.title': 'Placement du panneau',
1892
1924
  'settings.placement.tab': 'Afficher l’onglet Git dans la zone de conversation',
1893
1925
  'settings.placement.tabHint': 'Désactivé, le panneau reste accessible depuis la barre latérale droite et le panneau inférieur.',
1926
+ 'settings.placement.immersive': 'Mode immersif (pleine hauteur, zone de saisie masquée)',
1927
+ 'settings.placement.immersiveHint': 'Activé, l’onglet Git occupe toute la hauteur de la conversation et la zone de saisie s’efface ; approbations et questions restent visibles.',
1928
+ 'preview.title': 'Aperçu Git',
1929
+ 'preview.hint': 'Cliquez sur « Afficher le diff » sur un fichier ou un commit : le diff s’ouvre ici, dans la barre latérale.',
1894
1930
  'settings.rail.title': 'Boutons de la barre d’actions',
1895
1931
  'settings.rail.hint': 'Cochez les actions affichées sur la barre. L’ordre est fixe ; décocher masque une action.',
1896
1932
  'settings.rail.unavailable': 'Cet hôte n’a pas de service de réglages ; utilisez « Réglages de la barre d’actions » dans le panneau.',
@@ -2102,6 +2138,10 @@ window.__ModuleLoader__.load({
2102
2138
  'settings.placement.title': 'Размещение панели',
2103
2139
  'settings.placement.tab': 'Показывать вкладку Git в области диалога',
2104
2140
  'settings.placement.tabHint': 'В выключенном виде панель доступна из правой боковой панели и нижней панели.',
2141
+ 'settings.placement.immersive': 'Полноэкранный режим (во всю высоту, без поля ввода)',
2142
+ 'settings.placement.immersiveHint': 'Во включённом виде вкладка Git занимает всю высоту области диалога, поле ввода скрывается; подтверждения и вопросы остаются на виду.',
2143
+ 'preview.title': 'Git-предпросмотр',
2144
+ 'preview.hint': 'Нажмите «Показать различия» у файла или коммита — различия откроются здесь, в боковой панели.',
2105
2145
  'settings.rail.title': 'Кнопки панели действий',
2106
2146
  'settings.rail.hint': 'Отметьте действия, показываемые на панели. Порядок фиксирован; снятие флажка скрывает действие.',
2107
2147
  'settings.rail.unavailable': 'На этом хосте нет службы настроек; используйте «Настройки панели действий» в панели.',
@@ -2313,6 +2353,10 @@ window.__ModuleLoader__.load({
2313
2353
  'settings.placement.title': 'Posição do painel',
2314
2354
  'settings.placement.tab': 'Mostrar a aba Git na área de conversa',
2315
2355
  'settings.placement.tabHint': 'Desligado, o painel continua disponível na barra lateral direita e no painel inferior.',
2356
+ 'settings.placement.immersive': 'Modo imersivo (altura total, campo de entrada oculto)',
2357
+ 'settings.placement.immersiveHint': 'Ativado, a aba Git ocupa toda a altura da conversa e o campo de entrada fica de lado; aprovações e perguntas continuam visíveis.',
2358
+ 'preview.title': 'Pré-visualização do Git',
2359
+ 'preview.hint': 'Clique em “Mostrar diff” num arquivo ou commit — o diff abre aqui, na barra lateral.',
2316
2360
  'settings.rail.title': 'Botões da barra de ações',
2317
2361
  'settings.rail.hint': 'Marque as ações exibidas na barra. A ordem é fixa; desmarcar oculta uma ação.',
2318
2362
  'settings.rail.unavailable': 'Este host não tem serviço de configurações; use «Configurações da barra de ações» no painel.',
@@ -2524,6 +2568,10 @@ window.__ModuleLoader__.load({
2524
2568
  'settings.placement.title': 'Posizione del pannello',
2525
2569
  'settings.placement.tab': 'Mostra la scheda Git nell’area conversazione',
2526
2570
  'settings.placement.tabHint': 'Da spento, il pannello resta raggiungibile dalla barra laterale destra e dal pannello inferiore.',
2571
+ 'settings.placement.immersive': 'Modalità immersiva (altezza piena, input nascosto)',
2572
+ 'settings.placement.immersiveHint': 'Attiva, la scheda Git occupa tutta l’altezza della conversazione e la barra di input resta nascosta; approvazioni e domande restano visibili.',
2573
+ 'preview.title': 'Anteprima Git',
2574
+ 'preview.hint': 'Fai clic su «Mostra diff» su un file o un commit: il diff si apre qui, nella barra laterale.',
2527
2575
  'settings.rail.title': 'Pulsanti della barra azioni',
2528
2576
  'settings.rail.hint': 'Seleziona le azioni mostrate sulla barra. L’ordine è fisso; deselezionando se ne nasconde una.',
2529
2577
  'settings.rail.unavailable': 'Questo host non ha un servizio di impostazioni; usa «Impostazioni barra azioni» nel pannello.',
@@ -2735,6 +2783,10 @@ window.__ModuleLoader__.load({
2735
2783
  'settings.placement.title': 'Paneelplaatsing',
2736
2784
  'settings.placement.tab': 'Git-tabblad in het gespreksgebied tonen',
2737
2785
  'settings.placement.tabHint': 'Uitgeschakeld blijft het paneel bereikbaar via de rechterzijbalk en het onderpaneel.',
2786
+ 'settings.placement.immersive': 'Meeslepende modus (volledige hoogte, verborgen invoerveld)',
2787
+ 'settings.placement.immersiveHint': 'Aan: het Git-tabblad vult de hele gespreksruimte en het invoerveld wijkt; goedkeuringen en vragen blijven zichtbaar.',
2788
+ 'preview.title': 'Git-voorbeeld',
2789
+ 'preview.hint': 'Klik op „Diff tonen“ bij een bestand of commit — de diff opent hier in de zijbalk.',
2738
2790
  'settings.rail.title': 'Knoppen van de actiebalk',
2739
2791
  'settings.rail.hint': 'Vink de acties aan die op de balk verschijnen. De volgorde ligt vast; uitvinken verbergt er een.',
2740
2792
  'settings.rail.unavailable': 'Deze host heeft geen instellingenservice; gebruik "Actiebalk-instellingen" in het paneel.',
@@ -2946,6 +2998,10 @@ window.__ModuleLoader__.load({
2946
2998
  'settings.placement.title': 'Rozmieszczenie panelu',
2947
2999
  'settings.placement.tab': 'Pokazuj kartę Git w obszarze rozmowy',
2948
3000
  'settings.placement.tabHint': 'Po wyłączeniu panel pozostaje dostępny z prawego paska bocznego i dolnego panelu.',
3001
+ 'settings.placement.immersive': 'Tryb immersyjny (pełna wysokość, ukryte pole wpisywania)',
3002
+ 'settings.placement.immersiveHint': 'Po włączeniu karta Git wypełnia całą wysokość rozmowy, a pole wpisywania się chowa; zatwierdzenia i pytania pozostają widoczne.',
3003
+ 'preview.title': 'Podgląd Git',
3004
+ 'preview.hint': 'Kliknij „Pokaż różnice” przy pliku lub commicie — różnice otworzą się tutaj, na pasku bocznym.',
2949
3005
  'settings.rail.title': 'Przyciski paska akcji',
2950
3006
  'settings.rail.hint': 'Zaznacz akcje widoczne na pasku. Kolejność jest stała; odznaczenie ukrywa akcję.',
2951
3007
  'settings.rail.unavailable': 'Ten host nie ma usługi ustawień; użyj „Ustawienia paska akcji” w panelu.',
@@ -3157,6 +3213,10 @@ window.__ModuleLoader__.load({
3157
3213
  'settings.placement.title': 'Panelplacering',
3158
3214
  'settings.placement.tab': 'Visa Git-fliken i konversationsytan',
3159
3215
  'settings.placement.tabHint': 'Avstängd finns panelen kvar via höger sidopanel och nedre panelen.',
3216
+ 'settings.placement.immersive': 'Uppslukande läge (full höjd, dolt inmatningsfält)',
3217
+ 'settings.placement.immersiveHint': 'På: Git-fliken fyller hela konversationsytan och inmatningsfältet träder åt sidan; godkännanden och frågor visas fortfarande.',
3218
+ 'preview.title': 'Git-förhandsvisning',
3219
+ 'preview.hint': 'Klicka på ”Visa diff” på en fil eller en commit — diffen öppnas här i sidopanelen.',
3160
3220
  'settings.rail.title': 'Knappar i åtgärdsfältet',
3161
3221
  'settings.rail.hint': 'Markera de åtgärder som visas i fältet. Ordningen är fast; avmarkering döljer en.',
3162
3222
  'settings.rail.unavailable': 'Denna värd saknar inställningstjänst; använd ”Åtgärdsfältets inställningar” i panelen.',
@@ -3368,6 +3428,10 @@ window.__ModuleLoader__.load({
3368
3428
  'settings.placement.title': 'Panel yerleşimi',
3369
3429
  'settings.placement.tab': 'Konuşma alanında Git sekmesini göster',
3370
3430
  'settings.placement.tabHint': 'Kapatıldığında panel sağ kenar çubuğundan ve alt panelden açılabilir.',
3431
+ 'settings.placement.immersive': 'Odaklanma modu (tam yükseklik, gizli giriş alanı)',
3432
+ 'settings.placement.immersiveHint': 'Açıkken Git sekmesi tüm konuşma alanını kaplar ve giriş alanı gizlenir; onaylar ve sorular yine de görünür.',
3433
+ 'preview.title': 'Git Önizlemesi',
3434
+ 'preview.hint': 'Bir dosya veya commit üzerinde „Farkı göster”e tıklayın — fark burada, kenar çubuğunda açılır.',
3371
3435
  'settings.rail.title': 'Eylem çubuğu düğmeleri',
3372
3436
  'settings.rail.hint': 'Çubukta gösterilecek eylemleri işaretleyin. Sıra sabittir; işareti kaldırmak eylemi gizler.',
3373
3437
  'settings.rail.unavailable': 'Bu ana bilgisayarda ayar hizmeti yok; paneldeki "Eylem çubuğu ayarları"nı kullanın.',
@@ -3579,6 +3643,10 @@ window.__ModuleLoader__.load({
3579
3643
  'settings.placement.title': 'Penempatan panel',
3580
3644
  'settings.placement.tab': 'Tampilkan tab Git di area percakapan',
3581
3645
  'settings.placement.tabHint': 'Dimatikan, panel tetap bisa dibuka dari bilah sisi kanan dan panel bawah.',
3646
+ 'settings.placement.immersive': 'Mode imersif (tinggi penuh, kolom masukan tersembunyi)',
3647
+ 'settings.placement.immersiveHint': 'Aktif: tab Git memenuhi seluruh tinggi area percakapan dan kolom masukan mengalah; persetujuan dan pertanyaan tetap tampil.',
3648
+ 'preview.title': 'Pratinjau Git',
3649
+ 'preview.hint': 'Klik “Tampilkan diff” pada file atau commit — diff terbuka di sini, di bilah sisi.',
3582
3650
  'settings.rail.title': 'Tombol bilah aksi',
3583
3651
  'settings.rail.hint': 'Centang aksi yang ditampilkan di bilah. Urutannya tetap; menghapus centang menyembunyikannya.',
3584
3652
  'settings.rail.unavailable': 'Host ini tidak punya layanan pengaturan; gunakan "Pengaturan bilah aksi" di panel.',
@@ -3790,6 +3858,10 @@ window.__ModuleLoader__.load({
3790
3858
  'settings.placement.title': 'Vị trí bảng',
3791
3859
  'settings.placement.tab': 'Hiện thẻ Git trong vùng hội thoại',
3792
3860
  'settings.placement.tabHint': 'Tắt rồi, bảng vẫn mở được từ thanh bên phải và bảng dưới.',
3861
+ 'settings.placement.immersive': 'Chế độ đắm chìm (chiều cao đầy đủ, ẩn ô nhập)',
3862
+ 'settings.placement.immersiveHint': 'Bật: thẻ Git chiếm toàn bộ chiều cao vùng hội thoại và ô nhập thu vào; phê duyệt và câu hỏi vẫn hiển thị bình thường.',
3863
+ 'preview.title': 'Xem trước Git',
3864
+ 'preview.hint': 'Bấm “Xem diff” trên tệp hoặc commit — diff sẽ mở ở đây, trên thanh bên.',
3793
3865
  'settings.rail.title': 'Nút thanh tác vụ',
3794
3866
  'settings.rail.hint': 'Chọn các tác vụ hiển thị trên thanh. Thứ tự cố định; bỏ chọn sẽ ẩn tác vụ đó.',
3795
3867
  'settings.rail.unavailable': 'Máy chủ này không có dịch vụ cài đặt; hãy dùng “Cài đặt thanh tác vụ” trong bảng điều khiển.',
@@ -4001,6 +4073,10 @@ window.__ModuleLoader__.load({
4001
4073
  'settings.placement.title': 'موضع اللوحة',
4002
4074
  'settings.placement.tab': 'إظهار تبويب Git في منطقة المحادثة',
4003
4075
  'settings.placement.tabHint': 'عند الإيقاف تبقى اللوحة متاحة من الشريط الجانبي الأيمن واللوحة السفلية.',
4076
+ 'settings.placement.immersive': 'الوضع الغامر (ارتفاع كامل، إخفاء صندوق الكتابة)',
4077
+ 'settings.placement.immersiveHint': 'عند التشغيل يملأ تبويب Git منطقة المحادثة بكامل ارتفاعها ويتراجع صندوق الكتابة؛ تظل الموافقات والأسئلة ظاهرة كالمعتاد.',
4078
+ 'preview.title': 'معاينة Git',
4079
+ 'preview.hint': 'انقر «عرض الفروق» على ملف أو commit — تُفتح الفروق هنا في الشريط الجانبي.',
4004
4080
  'settings.rail.title': 'أزرار شريط الإجراءات',
4005
4081
  'settings.rail.hint': 'حدّد الإجراءات المعروضة على الشريط. الترتيب ثابت، وإلغاء التحديد يخفي الإجراء.',
4006
4082
  'settings.rail.unavailable': 'لا تتوفر خدمة إعدادات على هذا المضيف؛ استخدم «إعدادات شريط الإجراءات» في اللوحة.',
@@ -4212,6 +4288,10 @@ window.__ModuleLoader__.load({
4212
4288
  'settings.placement.title': 'पैनल प्लेसमेंट',
4213
4289
  'settings.placement.tab': 'संवाद क्षेत्र में Git टैब दिखाएँ',
4214
4290
  'settings.placement.tabHint': 'बंद करने पर भी पैनल दाएँ साइडबार और निचले पैनल से खुलता है।',
4291
+ 'settings.placement.immersive': 'इमर्सिव मोड (पूरी ऊँचाई, छिपा इनपुट बॉक्स)',
4292
+ 'settings.placement.immersiveHint': 'चालू होने पर Git टैब पूरे संवाद क्षेत्र की ऊँचाई घेर लेता है और इनपुट बॉक्स हट जाता है; अनुमोदन और प्रश्न फिर भी दिखते हैं।',
4293
+ 'preview.title': 'Git पूर्वावलोकन',
4294
+ 'preview.hint': 'फ़ाइल या कमिट पर “अंतर दिखाएँ” पर क्लिक करें — अंतर यहीं साइडबार में खुलेगा।',
4215
4295
  'settings.rail.title': 'एक्शन रेल बटन',
4216
4296
  'settings.rail.hint': 'रेल पर दिखने वाली क्रियाएँ चुनें। क्रम निश्चित है; चुनाव हटाने पर वह छिप जाती है।',
4217
4297
  'settings.rail.unavailable': 'इस होस्ट में सेटिंग सेवा नहीं है; पैनल में «एक्शन रेल सेटिंग» का उपयोग करें।',
@@ -4423,6 +4503,10 @@ window.__ModuleLoader__.load({
4423
4503
  'settings.placement.title': 'ตำแหน่งแผง',
4424
4504
  'settings.placement.tab': 'แสดงแท็บ Git ในพื้นที่บทสนทนา',
4425
4505
  'settings.placement.tabHint': 'ปิดแล้วยังเปิดแผงจากแถบข้างขวาหรือแผงล่างได้',
4506
+ 'settings.placement.immersive': 'โหมดดื่มด่ำ (เต็มความสูง, ซ่อนกล่องพิมพ์)',
4507
+ 'settings.placement.immersiveHint': 'เมื่อเปิด แท็บ Git จะกินพื้นที่สูงทั้งหมดของบทสนทนาและกล่องพิมพ์จะถอยออก; การอนุมัติและคำถามยังแสดงตามปกติ',
4508
+ 'preview.title': 'ดูตัวอย่าง Git',
4509
+ 'preview.hint': 'คลิก “แสดงความต่าง” ที่ไฟล์หรือคอมมิต — ความต่างจะเปิดที่นี่ในแถบข้าง',
4426
4510
  'settings.rail.title': 'ปุ่มแถบการทำงาน',
4427
4511
  'settings.rail.hint': 'ทำเครื่องหมายการทำงานที่จะแสดงบนแถบ ลำดับคงที่; ยกเลิกเครื่องหมายเพื่อซ่อน',
4428
4512
  'settings.rail.unavailable': 'โฮสต์นี้ไม่มีบริการตั้งค่า ให้ใช้ «ตั้งค่าแถบการทำงาน» ในแผง',
@@ -4677,16 +4761,19 @@ window.__ModuleLoader__.load({
4677
4761
  is still essentially correct, so it seeds the states and a background
4678
4762
  refresh lands the fresh data a moment later. Pure functions; the panel
4679
4763
  only ever reads them at effect time and writes after a fetch. */
4680
- /* Two TTLs, because the two seeds age very differently. The repos scan is
4681
- the SLOW part (a multi-repo workspace walks directories) and its result
4682
- barely changes, so its seed stays usable for 10 minutes: freshness is
4683
- the background refresh's job — every mount refreshes anyway — the TTL
4684
- only decides how old a seed may be to still paint the first screen
4685
- (user report: coming back to the Git tab after a while re-ran the whole
4686
- discovery). The first page (summary / branches / log) changes with every
4687
- commit, so its seed keeps the short 30s window it always had. */
4764
+ /* Two seeds age very differently. The repos scan is the SLOW part (a
4765
+ multi-repo workspace walks directories) and its result barely changes,
4766
+ so its seed keeps a 10-minute TTL: freshness is the background refresh's
4767
+ job — every mount refreshes anyway — the TTL only decides how old a seed
4768
+ may be to still paint the first screen. The page seed (summary /
4769
+ branches / log) used to expire after 30s, which is exactly the user
4770
+ complaint this queue answers ("come back to the panel after a while and
4771
+ everything re-loads"): it is a KEEP-WARM QUEUE now — no expiry at all,
4772
+ an LRU of three workspaces. Every page in the queue paints instantly no
4773
+ matter how long the panel stayed closed; the mount refresh re-proves
4774
+ every field silently right after, so freshness never regresses. */
4688
4775
  const PAINT_REPOS_TTL_MS = 600_000
4689
- const PAINT_PAGE_TTL_MS = 30_000
4776
+ const PAINT_PAGE_KEEP_WARM = 3
4690
4777
  const paintRepos = new Map() /* cwd -> { at, data } */
4691
4778
  const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
4692
4779
  const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
@@ -4726,6 +4813,13 @@ window.__ModuleLoader__.load({
4726
4813
  }
4727
4814
  feed(paintRepos, parsed.repos)
4728
4815
  feed(paintPage, parsed.page)
4816
+ /* The queue cap applies to restored pages too: storage may hold more
4817
+ than the live queue does (an older build wrote 16), trim to the same
4818
+ three — restored insertion order mirrors the order that was in
4819
+ memory, so the survivors are the most recent pages. */
4820
+ while (paintPage.size > PAINT_PAGE_KEEP_WARM) {
4821
+ paintPage.delete(paintPage.keys().next().value)
4822
+ }
4729
4823
  }
4730
4824
 
4731
4825
  function persistPaint() {
@@ -4755,13 +4849,24 @@ window.__ModuleLoader__.load({
4755
4849
  function readPaintPage(repoRoot) {
4756
4850
  hydratePaint()
4757
4851
  const entry = paintPage.get(repoRoot)
4758
- if (entry === undefined || Date.now() - entry.at > PAINT_PAGE_TTL_MS) return undefined
4852
+ if (entry === undefined) return undefined
4853
+ /* A read is a USE: re-insert at the tail so eviction tracks what the
4854
+ user actually visits, not which page happened to be written last. */
4855
+ paintPage.delete(repoRoot)
4856
+ paintPage.set(repoRoot, entry)
4759
4857
  return entry
4760
4858
  }
4761
4859
 
4762
4860
  function writePaintPage(repoRoot, page) {
4763
4861
  paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
4764
- if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
4862
+ /* The keep-warm queue cap: three workspaces stay painted, the fourth
4863
+ evicts the least-recently-used one. Pure LRU over insertion order —
4864
+ reads re-tail themselves (readPaintPage), so the survivor set is the
4865
+ three repo pages the user actually keeps coming back to. */
4866
+ while (paintPage.size > PAINT_PAGE_KEEP_WARM) {
4867
+ const oldest = paintPage.keys().next().value
4868
+ paintPage.delete(oldest)
4869
+ }
4765
4870
  persistPaint()
4766
4871
  }
4767
4872
 
@@ -5018,6 +5123,19 @@ window.__ModuleLoader__.load({
5018
5123
  return snapshot
5019
5124
  }
5020
5125
 
5126
+ /* The conversation seat's immersive mode (issue #8 round 3, placement
5127
+ group): the view rides the host's composer-overlay contract so it fills
5128
+ the WHOLE conversation height, and the composer seat hides while the Git
5129
+ view owns the ring. Default ON — that full-height space is why the tab
5130
+ placement exists at all; the settings card flips it live. A host without
5131
+ the row Config keeps the immersive seat (same policy as the tab switch). */
5132
+ function conversationImmersiveEnabled() {
5133
+ const ready = railFormReady()
5134
+ if (ready === null) return true
5135
+ const value = ready.value !== null && typeof ready.value === 'object' ? ready.value.immersive : undefined
5136
+ return value !== false
5137
+ }
5138
+
5021
5139
  function readRailConfig() {
5022
5140
  const ready = railFormReady()
5023
5141
  if (ready !== null) return railConfigOfValues(ready.value)
@@ -6655,6 +6773,50 @@ window.__ModuleLoader__.load({
6655
6773
  E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t('settings.placement.tab'))),
6656
6774
  E('div', { className: 'dig-settings-subnote' }, t('settings.placement.tabHint')))
6657
6775
  })(),
6776
+ (() => {
6777
+ /* Immersive mode (issue #8 round 3): the conversation seat's
6778
+ full-height overlay layout + hidden composer. Default ON — the
6779
+ full-height space is what the tab placement is for; flipping it
6780
+ re-renders the seat live (the ConversationPanel subscribes to the
6781
+ same config). */
6782
+ const field = 'immersive'
6783
+ const inputId = 'dig-placement-' + field
6784
+ const shown = Object.prototype.hasOwnProperty.call(intent.current, field)
6785
+ ? intent.current[field] === true
6786
+ : values[field] !== false
6787
+ const toggle = (event) => {
6788
+ if (readOnly === true) { setNote(t('settings.rail.readonly')); return }
6789
+ const fromControl = event !== undefined && event !== null && event.target !== null
6790
+ && event.target !== undefined && typeof event.target.checked === 'boolean'
6791
+ ? event.target.checked
6792
+ : null
6793
+ const wanted = fromControl !== null
6794
+ ? fromControl
6795
+ : (Object.prototype.hasOwnProperty.call(intent.current, field)
6796
+ ? intent.current[field] !== true
6797
+ : values[field] !== false) !== true
6798
+ intent.current[field] = wanted
6799
+ dirty.current.set(field, wanted)
6800
+ setPending((now) => Object.assign({}, now, { [field]: wanted }))
6801
+ setNote('')
6802
+ bump()
6803
+ scheduleFlush()
6804
+ }
6805
+ return E('div', { key: field },
6806
+ E('div', { className: 'dig-settings-row', 'data-placement-row': field },
6807
+ E('input', {
6808
+ id: inputId,
6809
+ type: 'checkbox',
6810
+ className: 'dig-settings-check',
6811
+ 'aria-labelledby': inputId + '-label',
6812
+ checked: shown,
6813
+ disabled: readOnly === true,
6814
+ onChange: toggle,
6815
+ }),
6816
+ E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: 'commit', size: 13 })),
6817
+ E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t('settings.placement.immersive'))),
6818
+ E('div', { className: 'dig-settings-subnote' }, t('settings.placement.immersiveHint')))
6819
+ })(),
6658
6820
  E('div', { className: 'dig-settings-sub' }, t('settings.commit.title')),
6659
6821
  (() => {
6660
6822
  /* The model row is a picker over the host's provider catalog; while
@@ -7682,8 +7844,23 @@ window.__ModuleLoader__.load({
7682
7844
  props.openSettings()
7683
7845
  }
7684
7846
 
7847
+ /* The immersive conversation seat (v0.14) hands the panel a SQUARE-ish
7848
+ box by construction: the conversation column clamps content width
7849
+ (~920px) while the seat fills the view's whole height, so the
7850
+ bottom-workbench-vs-sidebar ratio (`width >= height * 1.15`) can never
7851
+ hold there and the panel would flip to the stacked layout. The seat is
7852
+ a tool window — neither a horizontal dock nor a narrow rail — so it
7853
+ always takes the columns chrome (compact still wins: a truly narrow
7854
+ viewport keeps the single-row layout). */
7855
+ const [immersiveSeat, setImmersiveSeat] = useState(false)
7856
+ useMeasureLayoutEffect(() => {
7857
+ const node = hostRef.current
7858
+ const holder = node === null || typeof node.closest !== 'function' ? null : node.closest('.dig-conversation-seat[data-immersive="1"]')
7859
+ setImmersiveSeat(holder !== null)
7860
+ })
7685
7861
  const compact = size.width > 0 && size.width < COMPACT_MAX_WIDTH
7686
- const columns = !compact && size.width >= 600 && size.width >= size.height * 1.15
7862
+ const columns = (immersiveSeat === true && compact === false)
7863
+ || (!compact && size.width >= 600 && size.width >= size.height * 1.15)
7687
7864
 
7688
7865
  /* ---------- draggable panes (issue #5) ---------- */
7689
7866
 
@@ -8010,6 +8187,87 @@ window.__ModuleLoader__.load({
8010
8187
  }
8011
8188
  }, [base])
8012
8189
 
8190
+ /* ---------- external diff preview (user request, 2026-10-08) ---------- */
8191
+ /* 「点击文件预览」不再内嵌:预览交给 better-sidebar 内置的 diff tab
8192
+ (type 'diff',Hidden 内置页,DiffTab 自己经它自家 git 通道拉 patch,
8193
+ 我们只递引用)。placement 跟随点击发生的容器——底部工作台里点开在
8194
+ 底部、右栏里点开在右栏,走底座的官方 onOpenDiff 交接(props 注入,
8195
+ placement 由底座 store 决定);主对话区座位与原生座位没有该通道,
8196
+ 直接 betterSidebar.openTab 定向右栏(target: 'right')。两通道皆
8197
+ 不可用(无底座 / 老底座)→ 返回 false,调用方落回内嵌 diff 窗。
8198
+ ref 的 worktree + repoRoot 都填本面板的仓库根:DiffTab 的加载器把
8199
+ repoRoot 并进它自己的 git 调用,多仓库工作区不会落错仓库。 */
8200
+ const previewDiff = useCallback((seed) => {
8201
+ if (repoRoot === null) return false
8202
+ const commitHash = seed.hash !== undefined && seed.hash !== null ? String(seed.hash) : null
8203
+ const ref = commitHash !== null
8204
+ ? { kind: 'commit', hash: commitHash.slice(0, 7), hashFull: commitHash, subject: seed.subject || '', worktree: repoRoot, repoRoot: repoRoot }
8205
+ : { kind: 'worktree', path: seed.path, staged: seed.staged === true, untracked: seed.untracked === true ? true : undefined, worktree: repoRoot, repoRoot: repoRoot }
8206
+ const title = commitHash !== null
8207
+ ? commitHash.slice(0, 7) + (seed.subject ? ' ' + seed.subject : '')
8208
+ : baseName(String(seed.path))
8209
+ const id = commitHash !== null
8210
+ ? 'diff:c:' + encodeURIComponent(repoRoot) + ':' + commitHash
8211
+ : 'diff:w:' + encodeURIComponent(repoRoot) + ':' + (seed.staged === true ? 's' : 'u') + ':' + seed.path
8212
+ const tab = { id: id, type: 'diff', title: title, diff: ref }
8213
+ /* Seat channel first — the bottom workbench opens its own diff tab,
8214
+ the right column opens it there; placement is the base's call. */
8215
+ if (typeof props.openDiffExternal === 'function') {
8216
+ try {
8217
+ props.openDiffExternal(tab)
8218
+ return true
8219
+ } catch (error) { void error }
8220
+ }
8221
+ /* Conversation seat / native seat: aim at the right sidebar. The
8222
+ capability gate keeps an older base (pre-targetedOpen) from
8223
+ receiving a field it cannot place; any refusal falls through to
8224
+ the inline pane below. */
8225
+ let betterSidebar = undefined
8226
+ try { betterSidebar = props.ctx === undefined || props.ctx === null ? undefined : props.ctx.get('betterSidebar') } catch (error) { void error }
8227
+ if (betterSidebar !== undefined && betterSidebar !== null && typeof betterSidebar.openTab === 'function') {
8228
+ const capabilities = betterSidebar.capabilities
8229
+ const targeted = capabilities === undefined || capabilities === null || (Array.isArray(capabilities) && capabilities.indexOf('targetedOpen') >= 0)
8230
+ if (targeted === true) {
8231
+ try {
8232
+ const scopeObject = scope || {}
8233
+ betterSidebar.openTab({ type: 'diff', title: title, id: id, diff: ref, target: 'right' }, { sessionId: scopeObject.sessionId, cwd: scopeObject.cwd })
8234
+ return true
8235
+ } catch (error) { void error }
8236
+ }
8237
+ }
8238
+ /* Native preview tab: the base is absent (or too old), so the plugin's
8239
+ OWN second native right-sidebar type takes the preview. The seed
8240
+ carries cwd (the route requires it) + repoRoot (skips the rev-parse);
8241
+ the tab is a singleton per pane — a second click re-seeds it in
8242
+ place instead of stacking tabs. */
8243
+ let sidebarRight = undefined
8244
+ try { sidebarRight = props.ctx === undefined || props.ctx === null ? undefined : props.ctx.get('sidebarRight') } catch (error) { void error }
8245
+ if (sidebarRight !== undefined && sidebarRight !== null && typeof sidebarRight.openTab === 'function') {
8246
+ try {
8247
+ const scopeObject = scope || {}
8248
+ previewStore.set(commitHash !== null
8249
+ ? { hash: commitHash, subject: seed.subject || '', cwd: scopeObject.cwd, repoRoot: repoRoot }
8250
+ : { path: seed.path, staged: seed.staged === true, cwd: scopeObject.cwd, repoRoot: repoRoot })
8251
+ sidebarRight.openTab(NATIVE_PREVIEW_KIND)
8252
+ return true
8253
+ } catch (error) { void error }
8254
+ }
8255
+ return false
8256
+ }, [repoRoot, scope, props.openDiffExternal, props.ctx])
8257
+
8258
+ /* The one diff verb every surface calls (row click, row menu, detail
8259
+ file menu, working-diff shortcut): external first, inline fallback. */
8260
+ const showDiff = useCallback((seed) => {
8261
+ if (previewDiff(seed) === true) return
8262
+ if (seed.hash !== undefined && seed.hash !== null) {
8263
+ setDiffContext({ kind: 'commit', hash: seed.hash, subject: seed.subject || '' })
8264
+ void openDiff({ hash: seed.hash, path: seed.path })
8265
+ return
8266
+ }
8267
+ setDiffContext({ kind: 'worktree', staged: seed.staged === true })
8268
+ void openDiff({ path: seed.path, staged: seed.staged === true })
8269
+ }, [previewDiff, openDiff])
8270
+
8013
8271
  const selectCommit = useCallback(async (commit) => {
8014
8272
  setSelectedHash(commit.hash)
8015
8273
  setSelectedPath(null)
@@ -8223,7 +8481,7 @@ window.__ModuleLoader__.load({
8223
8481
 
8224
8482
  const changeMenu = useCallback((event, item, group) => {
8225
8483
  const items = [
8226
- { id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { setDiffContext({ kind: 'worktree', staged: group === 'staged' }); void openDiff({ path: item.path, staged: group === 'staged' }) } },
8484
+ { id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) } },
8227
8485
  { id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(item.path) },
8228
8486
  { id: 'sendPath', icon: 'file', tone: 'secondary', label: t('menu.sendFilePathToChat'), run: () => sendToComposer({ text: item.path, fallback: item.path }) },
8229
8487
  null,
@@ -8235,7 +8493,7 @@ window.__ModuleLoader__.load({
8235
8493
  { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(item.path) } },
8236
8494
  ]
8237
8495
  openMenuAt(event, items)
8238
- }, [openDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer])
8496
+ }, [showDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer])
8239
8497
 
8240
8498
  /* The commit-detail file list shares the working-tree row menu, minus the
8241
8499
  staging verbs: a committed file's diff replays against its commit. */
@@ -8243,8 +8501,11 @@ window.__ModuleLoader__.load({
8243
8501
  openMenuAt(event, [
8244
8502
  { id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => {
8245
8503
  setSelectedPath(file.path)
8246
- setDiffContext({ kind: 'commit', hash: detail === null || detail === undefined ? undefined : detail.hash, subject: detail === null || detail === undefined ? '' : detail.subject })
8247
- void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
8504
+ showDiff({
8505
+ hash: detail === null || detail === undefined ? undefined : detail.hash,
8506
+ subject: detail === null || detail === undefined ? '' : detail.subject,
8507
+ path: file.path,
8508
+ })
8248
8509
  } },
8249
8510
  null,
8250
8511
  { id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(file.path) },
@@ -8252,7 +8513,7 @@ window.__ModuleLoader__.load({
8252
8513
  null,
8253
8514
  { id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(file.path) } },
8254
8515
  ])
8255
- }, [detail, openDiff, openMenuAt, sendFileReferenceToChat, sendToComposer, t])
8516
+ }, [detail, showDiff, openMenuAt, sendFileReferenceToChat, sendToComposer, t])
8256
8517
 
8257
8518
  /* The diff pane's own context menu (v0.13.5): select text in the diff,
8258
8519
  right-click, send a self-describing line reference to the composer.
@@ -8485,7 +8746,7 @@ window.__ModuleLoader__.load({
8485
8746
  onStageAll: (entries) => { void run('stage', { paths: entries.map((entry) => entry.path) }) },
8486
8747
  onUnstageAll: () => { void run('unstage', { paths: (summary === null ? [] : summary.changes.staged).map((entry) => entry.path) }) },
8487
8748
  onDiscard: (item, group) => setDialog({ kind: 'discard', item: item, group: group }),
8488
- onDiff: (item, group) => { setDiffContext({ kind: 'worktree', staged: group === 'staged' }); void openDiff({ path: item.path, staged: group === 'staged' }) },
8749
+ onDiff: (item, group) => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) },
8489
8750
  onChangeMenu: changeMenu,
8490
8751
  })
8491
8752
 
@@ -8495,8 +8756,11 @@ window.__ModuleLoader__.load({
8495
8756
  onBack: () => { setView('history'); setSelectedPath(null); setPatch(''); setDiffContext(null) },
8496
8757
  onSelectFile: (file) => {
8497
8758
  setSelectedPath(file.path)
8498
- setDiffContext({ kind: 'commit', hash: detail === null || detail === undefined ? undefined : detail.hash, subject: detail === null || detail === undefined ? '' : detail.subject })
8499
- void openDiff({ hash: detail === null ? undefined : detail.hash, path: file.path })
8759
+ showDiff({
8760
+ hash: detail === null || detail === undefined ? undefined : detail.hash,
8761
+ subject: detail === null || detail === undefined ? '' : detail.subject,
8762
+ path: file.path,
8763
+ })
8500
8764
  },
8501
8765
  onFileMenu: detailFileMenu,
8502
8766
  })
@@ -8547,7 +8811,7 @@ window.__ModuleLoader__.load({
8547
8811
  ['untracked', changes.untracked, false],
8548
8812
  ]
8549
8813
  for (const entry of order) {
8550
- if (entry[1].length > 0) { void openDiff({ path: entry[1][0].path, staged: entry[2] }); return }
8814
+ if (entry[1].length > 0) { showDiff({ path: entry[1][0].path, staged: entry[2] }); return }
8551
8815
  }
8552
8816
  setNote(t('note.noChanges'))
8553
8817
  }
@@ -9228,6 +9492,37 @@ window.__ModuleLoader__.load({
9228
9492
  '.dig-menu-danger,.dig-menu-danger .dig-menu-label{color:var(--dsw-alias-state-error-primary)}',
9229
9493
  '.dig-menu-disabled{opacity:.38;cursor:default}',
9230
9494
  '.dig-menu-sep{height:1px;margin:3px 6px;background:var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
9495
+ /* ---- immersive conversation seat (issue #8 round 3) ---- */
9496
+ /* The seat itself carries `data-conversation-composer-overlay` (the
9497
+ host's own opt-in, what TrajectoryView rides): the host CSS flips the
9498
+ view area to `flex: 1 1 0; min-height: 0; overflow: hidden` and the
9499
+ composer seat to an overlay. What the host does NOT do is hide the
9500
+ composer — that is ours, and it is a single structural rule patterned
9501
+ after dsh-context's `.lc-root` rule: while a Git view owns the
9502
+ conversation ring, the composer seat displays none. The :not() chain
9503
+ is the SAFETY VALVE and its attribute names are verified against the
9504
+ host sources (ui-approval ApprovalPanel `data-approval-key`,
9505
+ ui-user-questions QuestionComposer `data-question-key`, PlanReviewPanel
9506
+ `data-plan-review-key`): a blocking interaction riding the seat must
9507
+ stay reachable even though the plain input card is hidden, or a user
9508
+ sitting in the Git view could never approve anything. */
9509
+ '[data-conversation-scroll]:has(.dig-conversation-seat[data-immersive="1"]) > [data-composer-seat]:not(:has([data-approval-key],[data-question-key],[data-plan-review-key])){display:none}',
9510
+ /* The transcript-width drag strips are FOLLOWING SIBLINGS of the shared
9511
+ scroller (children of the conversation body), full-height and
9512
+ z-index 8: over a full-bleed Git pane they glow and drag straight
9513
+ through it. Same sibling combinator lesson dsh-context recorded in
9514
+ its own issue #50 — a descendant combinator here matches nothing. */
9515
+ '[data-conversation-scroll]:has(.dig-conversation-seat[data-immersive="1"]) ~ [data-width-handle]{display:none}',
9516
+ /* ---- native preview tab (external diff preview) ---- */
9517
+ /* The body mounts straight into the right-sidebar pane: full height,
9518
+ internal scrolling in the body region, a one-line header for the
9519
+ subject/path. Same tokens as the rest of the panel — no hard-coded
9520
+ surfaces. */
9521
+ '.dig-preview-root{position:relative;height:100%;display:flex;flex-direction:column;min-height:0;background:var(--dsw-alias-bg-base)}',
9522
+ '.dig-preview-head{flex:none;padding:8px 12px;border-bottom:1px solid var(--dsw-alias-border-l2,var(--dsw-alias-border-l1));font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
9523
+ '.dig-preview-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
9524
+ '.dig-preview-body{flex:1;min-height:0;overflow:auto;padding:8px 12px;display:flex;flex-direction:column}',
9525
+ '.dig-preview-body .dig-diff-host,.dig-preview-body .dig-diff{flex:1;min-height:0}',
9231
9526
  ].join('\n')
9232
9527
 
9233
9528
  /* ============================== plugin ============================== */
@@ -9243,6 +9538,34 @@ window.__ModuleLoader__.load({
9243
9538
  Chat (0) and Trajectory (10), and after dsh-context's tab (20) when both
9244
9539
  are installed. */
9245
9540
  const CONVERSATION_VIEW_ID = 'ide-git'
9541
+ /* The native preview tab (external diff preview, 2026-10-08): a SECOND
9542
+ native right-sidebar type that exists only to show one diff. It carries
9543
+ no guide entry on purpose — nothing on the start page opens it; it opens
9544
+ itself when a diff verb fires with no better-sidebar to hand off to.
9545
+ Content follows the module-level store below, so every seat (the
9546
+ conversation tab, the native panel) feeds the same single preview. */
9547
+ const NATIVE_PREVIEW_ID = 'dsh-ide-git-preview'
9548
+ const NATIVE_PREVIEW_KIND = 'ide-git-preview'
9549
+
9550
+ /* The one diff seed the native preview tab shows. set() bumps a monotonous
9551
+ counter so a re-click of the SAME file still re-fetches; subscribers get
9552
+ the barest notify and re-read .seed themselves. Memory-only: after a
9553
+ reload the tab (if the layout restored it) shows the empty hint again —
9554
+ the next click re-seeds it. */
9555
+ const previewStore = {
9556
+ seed: null,
9557
+ listeners: new Set(),
9558
+ set(seed) {
9559
+ this.seed = seed
9560
+ for (const listener of [...this.listeners]) {
9561
+ try { listener() } catch (error) { void error }
9562
+ }
9563
+ },
9564
+ subscribe(listener) {
9565
+ this.listeners.add(listener)
9566
+ return () => { this.listeners.delete(listener) }
9567
+ },
9568
+ }
9246
9569
 
9247
9570
  /* The native seat hands a tab the standard props; the panel wants
9248
9571
  `{ scope, t }`. A session's working directory is the path of the
@@ -9272,7 +9595,7 @@ window.__ModuleLoader__.load({
9272
9595
  const openSettings = navigation !== undefined && navigation !== null && typeof navigation.openBundle === 'function'
9273
9596
  ? () => { navigation.openBundle(SETTINGS_BUNDLE) }
9274
9597
  : null
9275
- return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings, ctx: props.ctx })
9598
+ return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings, ctx: props.ctx, openDiffExternal: props.openDiffExternal })
9276
9599
  }
9277
9600
 
9278
9601
  /* The conversation-area seat (issue #8): the same panel as a view tab
@@ -9310,6 +9633,21 @@ window.__ModuleLoader__.load({
9310
9633
  const ctx = props.ctx
9311
9634
  const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
9312
9635
  const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
9636
+ /* Immersive mode (issue #8 round 3): when on, the seat declares the
9637
+ host's composer-overlay contract (`data-conversation-composer-overlay`)
9638
+ and the conversation view area goes `flex: 1 1 0; min-height: 0;
9639
+ overflow: hidden` — the panel fills the WHOLE conversation height and
9640
+ scrolls inside itself, with the composer seat hidden away (CSS keeps
9641
+ the blocking approvals visible). The v0.13.2 pin-to-scrollport
9642
+ machinery below is the FALLBACK for immersive-off: it exists because
9643
+ the active view area is `flex: 1 0 auto` (content height) in the
9644
+ normal layout, which is exactly what the overlay contract replaces. */
9645
+ const [immersive, setImmersive] = useState(() => conversationImmersiveEnabled())
9646
+ useEffect(() => {
9647
+ const sync = () => { setImmersive(conversationImmersiveEnabled()) }
9648
+ sync()
9649
+ return subscribeRailConfig(sync)
9650
+ }, [])
9313
9651
  /* The active conversation's view area is `flex: 1 0 auto; min-height:
9314
9652
  auto` — it GROWS with content and scrolls inside the shared
9315
9653
  conversation scrollport. An auto-height panel there measured
@@ -9324,6 +9662,12 @@ window.__ModuleLoader__.load({
9324
9662
  useEffect(() => {
9325
9663
  const el = seatRef.current
9326
9664
  if (el === null) return undefined
9665
+ /* Immersive ON: the host's overlay layout owns this seat's height
9666
+ (the view area is the flex child now) — no pinning, no convergence,
9667
+ none of the scrollport arithmetic below applies. Toggling the
9668
+ setting re-runs this effect through the dependency below, so the
9669
+ fallback machinery spins back up on the way off. */
9670
+ if (immersive === true) return undefined
9327
9671
  /* Leftover wheel scroll, root cause: the composer seat is sticky, but a
9328
9672
  sticky box still occupies FLOW space inside the scrollport, so even
9329
9673
  with the panel seat pinned to the visible height the scrollHeight
@@ -9408,7 +9752,7 @@ window.__ModuleLoader__.load({
9408
9752
  if (observer !== undefined) observer.disconnect()
9409
9753
  window.removeEventListener('resize', measure)
9410
9754
  }
9411
- }, [])
9755
+ }, [immersive])
9412
9756
  useEffect(() => {
9413
9757
  let alive = true
9414
9758
  const sync = () => { if (alive === true) setCwd(sessionCwdOf(ctx, sessionId)) }
@@ -9432,11 +9776,20 @@ window.__ModuleLoader__.load({
9432
9776
  }
9433
9777
  }, [ctx, sessionId])
9434
9778
  const scope = useMemo(() => ({ cwd: cwd, sessionId: sessionId }), [cwd, sessionId])
9435
- return E('div', {
9779
+ /* The overlay marker rides the SEAT element (a viewArea descendant): the
9780
+ host's `.scrollBody:has([data-conversation-composer-overlay])` rules
9781
+ pick it up from anywhere inside, flipping the view area to
9782
+ `flex: 1 1 0; min-height: 0; overflow: hidden` (full-height, internal
9783
+ scrolling) and the composer seat to an overlay. `data-immersive`
9784
+ drives the plugin's own CSS (composer hide + width-handle hide). */
9785
+ const immersiveAttrs = immersive === true
9786
+ ? { 'data-conversation-composer-overlay': '', 'data-immersive': '1' }
9787
+ : {}
9788
+ return E('div', Object.assign({
9436
9789
  ref: seatRef,
9437
9790
  className: 'dig-conversation-seat',
9438
- style: seatHeight === null ? undefined : { height: seatHeight + 'px' },
9439
- }, E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true }))
9791
+ style: immersive === true ? { height: '100%' } : (seatHeight === null ? undefined : { height: seatHeight + 'px' }),
9792
+ }, immersiveAttrs), E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true }))
9440
9793
  }, (prev, next) => prev.sessionId === next.sessionId && prev.t === next.t && prev.ctx === next.ctx)
9441
9794
 
9442
9795
  function NativePanel(props) {
@@ -9461,6 +9814,55 @@ window.__ModuleLoader__.load({
9461
9814
  return E(LocaleLive, { ctx: props.ctx, scope: scope, t: props.t, visible: true, dock: dock })
9462
9815
  }
9463
9816
 
9817
+ /* The native preview tab's body (external diff preview): shows exactly one
9818
+ diff — whatever the store holds — fetched fresh from the host route on
9819
+ every seed. The seed carries cwd + repoRoot so a multi-root workspace
9820
+ lands on the right repository (the route requires cwd; repoRoot skips
9821
+ the rev-parse). Rendered through the SAME DiffBody the inline pane uses,
9822
+ so host DiffBlock reuse, the hand-drawn fallback and the binary/empty
9823
+ states are all identical. */
9824
+ function NativePreviewPanel(props) {
9825
+ const t = props.t
9826
+ const [seed, setSeed] = useState(() => previewStore.seed)
9827
+ useEffect(() => previewStore.subscribe(() => { setSeed(previewStore.seed) }), [])
9828
+ const seedKey = seed === null
9829
+ ? ''
9830
+ : (seed.hash !== undefined && seed.hash !== null ? 'c:' + String(seed.hash) : 'w:' + String(seed.staged === true) + ':' + String(seed.path))
9831
+ const [state, setState] = useState({ loading: false, patch: '', error: null })
9832
+ useEffect(() => {
9833
+ if (seed === null) return undefined
9834
+ let alive = true
9835
+ setState({ loading: true, patch: '', error: null })
9836
+ void (async () => {
9837
+ try {
9838
+ const payload = { cwd: seed.cwd }
9839
+ if (seed.repoRoot !== undefined && seed.repoRoot !== null && seed.repoRoot !== '') payload.repoRoot = seed.repoRoot
9840
+ if (seed.hash !== undefined && seed.hash !== null && seed.hash !== '') payload.hash = seed.hash
9841
+ else { payload.path = seed.path; payload.staged = seed.staged === true }
9842
+ const data = await request('diff', payload)
9843
+ if (alive === true) setState({ loading: false, patch: data === undefined || data === null || typeof data.patch !== 'string' ? '' : data.patch, error: null })
9844
+ } catch (error) {
9845
+ if (alive === true) setState({ loading: false, patch: '', error: error instanceof Error ? error.message : String(error) })
9846
+ }
9847
+ })()
9848
+ return () => { alive = false }
9849
+ // eslint-disable-next-line react-hooks/exhaustive-deps
9850
+ }, [seedKey])
9851
+ const title = seed === null
9852
+ ? ''
9853
+ : (seed.hash !== undefined && seed.hash !== null
9854
+ ? String(seed.hash).slice(0, 7) + (seed.subject ? ' ' + seed.subject : '')
9855
+ : String(seed.path))
9856
+ return E('div', { className: 'dig-preview-root' },
9857
+ E('div', { className: 'dig-preview-head' },
9858
+ E('span', { className: 'dig-preview-title', title: title }, title === '' ? t('preview.title') : title)),
9859
+ seed === null
9860
+ ? E('div', { className: 'dig-preview-body' }, E('div', { className: 'dig-empty' }, t('preview.hint')))
9861
+ : state.error !== null
9862
+ ? E('div', { className: 'dig-preview-body' }, E('div', { className: 'dig-empty' }, state.error))
9863
+ : E('div', { className: 'dig-preview-body' }, E(DiffBody, { patch: state.patch, loading: state.loading, binary: false, t: t })))
9864
+ }
9865
+
9464
9866
  function apply(ctx) {
9465
9867
  const style = document.createElement('style')
9466
9868
  style.setAttribute('data-dsh-ide-git', '')
@@ -9524,7 +9926,24 @@ window.__ModuleLoader__.load({
9524
9926
  { name: 'sidebar.right.pane.tab', key: NATIVE_ID },
9525
9927
  (tabProps) => E(NativePanel, Object.assign({}, tabProps, { t: t, ctx: ctx })),
9526
9928
  ))
9929
+ /* The preview type has NO guide entry on purpose (start-page
9930
+ destinations are the main panel's job): it exists to receive the
9931
+ openTab this plugin fires when a diff verb runs without a
9932
+ better-sidebar to hand off to. Omitting `guide` is the registry's
9933
+ documented way to stay off the guide page. */
9934
+ const offPreviewType = tabs.register({
9935
+ id: NATIVE_PREVIEW_ID,
9936
+ kind: NATIVE_PREVIEW_KIND,
9937
+ title: () => t('preview.title'),
9938
+ keepMounted: true,
9939
+ })
9940
+ const offPreviewBody = slots.inject('sidebar.right.pane.tab', () => slots.register(
9941
+ { name: 'sidebar.right.pane.tab', key: NATIVE_PREVIEW_ID },
9942
+ (tabProps) => E(NativePreviewPanel, Object.assign({}, tabProps, { t: t, ctx: ctx })),
9943
+ ))
9527
9944
  return () => {
9945
+ try { offPreviewBody() } catch (error) { void error }
9946
+ try { offPreviewType() } catch (error) { void error }
9528
9947
  try { offBody() } catch (error) { void error }
9529
9948
  try { offType() } catch (error) { void error }
9530
9949
  }
@@ -9557,6 +9976,13 @@ window.__ModuleLoader__.load({
9557
9976
  scope: tabProps.scope,
9558
9977
  t: t,
9559
9978
  visible: tabProps.visible,
9979
+ /* The base's own diff-tab hand-off (OpenTabSeed type 'diff').
9980
+ Where the diff tab lands — bottom workbench vs right column —
9981
+ is the base's store decision, made where the click happened:
9982
+ exactly the "preview beside this panel" behavior the seat
9983
+ should have. Absent on an older base; previewDiff falls back
9984
+ to the service openTab, then to the inline pane. */
9985
+ openDiffExternal: typeof tabProps.onOpenDiff === 'function' ? tabProps.onOpenDiff : undefined,
9560
9986
  }),
9561
9987
  })
9562
9988
  /* The base can go away at RUNTIME (disabled in the plugin panel, or
package/src/index.js CHANGED
@@ -70,6 +70,13 @@ function railConfigSchema(schema) {
70
70
  conversation.view seat at all" — the sidebar and dock seats are not
71
71
  affected; a missing field means the default (shown). */
72
72
  shape.conversationTab = live(schema.boolean().default(true))
73
+ /* Immersive conversation seat (issue #8 round 3): on by default. false means
74
+ the seat falls back to the pin-to-scrollport layout with a visible
75
+ composer; a missing field means the default (on). NOTE: the field MUST be
76
+ declared here — the Host rejects row-Config writes for paths the schema
77
+ does not mark volatile ("Config field … is not volatile"), and the settings
78
+ switch then rolls back silently. */
79
+ shape.immersive = live(schema.boolean().default(true))
73
80
  return schema.object(shape)
74
81
  }
75
82