dsh-ide-git 0.12.1 → 0.13.1

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/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  简体中文 | [English](README_EN.md)
6
6
 
7
- > 给 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 的侧边栏装一个 **IDE 级的 Git 工具窗口**——左边分支树、中间提交图谱、右边变更与提交详情,操作方式对齐 JetBrains 系 IDE 的 Git 面板;以 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 原生 Tab 的形式注册,右侧栏与底部面板都能用。
7
+ > 给 [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) 的侧边栏装一个 **IDE 级的 Git 工具窗口**——左边分支树、中间提交图谱、右边变更与提交详情,操作方式对齐 JetBrains 系 IDE 的 Git 面板;以 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 原生 Tab 的形式注册,右侧栏与底部面板都能用;v0.13.0 起还可挂在**主对话区**(「对话 / 轨迹」旁边的 Git 标签页,设置里可关)。
8
8
 
9
9
  DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-ide-git` 补齐 IDE 用户习惯的那一层:**分支树 + 右键分支操作 + 提交图谱 + 提交详情 + 变更列表 + 提交框**。
10
10
 
@@ -64,6 +64,7 @@ DSH 自带的 Git 面板覆盖「暂存 / 提交 / 还原 / 看历史」;`dsh-id
64
64
  |---|---|---|
65
65
  | **dsh-better-sidebar**(底座,可选) | 装了它 | 它的 Tab 系统:右侧栏里的原生 Tab **加上底部工作台**;它的设置页 `侧边卡片` 里能看到本插件的卡片 |
66
66
  | **DSH 原生右侧栏** | 没装底座时自动接管 | 右侧栏 Guide 页里的 **Git** 胶囊 → 点开就是同一个面板 |
67
+ | **主对话区标签页**(v0.13.0 起) | 总是可用(可在设置里关闭) | 会话顶部「对话 / 轨迹」旁多一枚 **Git** 标签,点它整块主对话区变成 Git 工具窗口;开关在插件设置 →「面板放置」 |
67
68
 
68
69
  底座在场时以它为准(它还带底部工作台);底座**晚到**会顶掉原生注册再挂到它那边。所以**只装本插件也能用**,没有任何前置要求。
69
70
 
package/README_EN.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  [简体中文](README.md) | English
6
6
 
7
- > An **IDE-grade Git tool window** for the [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) sidebar — branch tree on the left, commit graph in the middle, changes and commit details on the right, with JetBrains-style actions. Registered as a native [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) tab, usable in both the native right sidebar and the bottom workbench.
7
+ > An **IDE-grade Git tool window** for the [DeepSeek Harness (DSH)](https://github.com/deepseek-ai/deepseek-harness) sidebar — branch tree on the left, commit graph in the middle, changes and commit details on the right, with JetBrains-style actions. Registered as a native [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) tab, usable in both the native right sidebar and the bottom workbench; since v0.13.0 also as a **conversation-area tab** (a Git tab beside Chat/Trajectory, switchable off in settings).
8
8
 
9
9
  DSH's built-in Git panel covers stage / commit / revert / history. `dsh-ide-git` adds the layer IDE users expect: a **branch tree with a full context menu**, a **commit graph**, **commit details with per-file diffs**, a **grouped changes list** and a commit box.
10
10
 
@@ -56,6 +56,7 @@ The plugin registers once; which surface it lands on is the **host's** decision,
56
56
  |---|---|---|
57
57
  | **dsh-better-sidebar** (optional) | whenever it is installed | its tab system: the tab in the right sidebar **plus the bottom workbench**; the plugin also shows up as a card in its settings page |
58
58
  | **DSH's own right sidebar** | automatically, when better-sidebar is absent | a **Git** capsule on the sidebar's guide page — the very same panel |
59
+ | **Conversation-area tab** (since v0.13.0) | always on (switchable in settings) | a **Git** tab beside Chat/Trajectory at the top of the conversation — clicking it turns the whole conversation area into the Git tool window; the switch lives in the plugin settings under Placement |
59
60
 
60
61
  better-sidebar wins while it is there (it also carries the bottom workbench); a late-arriving better-sidebar takes the native registration down and hosts the tab itself. Installing this plugin alone is fully supported — no prerequisite.
61
62
 
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.12.1",
3
+ "version": "0.13.1",
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.12.1",
3
+ "version": "0.13.1",
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": {
@@ -29,7 +29,8 @@
29
29
  "inject": [
30
30
  "@deepseek-ai/dsh-client-locale",
31
31
  "@deepseek-ai/dsh-client-ui-slots",
32
- "@deepseek-ai/dsh-client-modules"
32
+ "@deepseek-ai/dsh-client-modules",
33
+ "@deepseek-ai/dsh-client-ui-conversation"
33
34
  ],
34
35
  "platform": "web"
35
36
  }
package/src/client.js CHANGED
@@ -27,6 +27,7 @@ window.__ModuleLoader__.load({
27
27
  const useRef = React.useRef
28
28
  const useCallback = React.useCallback
29
29
  const useLayoutEffect = React.useLayoutEffect
30
+ const memo = React.memo
30
31
 
31
32
  /* API base is MOUNT-RELATIVE (no leading slash). dsh 0.1.7 serves the
32
33
  shell with <base href="./">, so document.baseURI is the mount the page
@@ -160,7 +161,10 @@ window.__ModuleLoader__.load({
160
161
  'rail.settings': '动作条设置',
161
162
  'rail.openSettings': '打开插件设置',
162
163
  'settings.card.title': 'Git 工具窗口设置',
163
- 'settings.card.desc': '动作条按钮与 AI 提交信息',
164
+ 'settings.card.desc': '面板放置、动作条按钮与 AI 提交信息',
165
+ 'settings.placement.title': '面板放置',
166
+ 'settings.placement.tab': '在主对话区显示 Git 标签页',
167
+ 'settings.placement.tabHint': '关闭后仍可从右侧栏或底部面板打开。',
164
168
  'settings.rail.title': '动作条按钮',
165
169
  'settings.rail.hint': '勾选要显示在动作条上的动作;顺序固定为默认顺序,取消勾选即隐藏。',
166
170
  'settings.rail.unavailable': '当前宿主没有设置服务,请用面板内的「动作条设置」。',
@@ -348,7 +352,10 @@ window.__ModuleLoader__.load({
348
352
  'rail.settings': 'Action bar settings',
349
353
  'rail.openSettings': 'Open plugin settings',
350
354
  'settings.card.title': 'Git tool window settings',
351
- 'settings.card.desc': 'Action rail buttons and AI commit message',
355
+ 'settings.card.desc': 'Placement, action rail buttons and AI commit message',
356
+ 'settings.placement.title': 'Placement',
357
+ 'settings.placement.tab': 'Show the Git tab in the conversation area',
358
+ 'settings.placement.tabHint': 'Off, the panel stays reachable from the right sidebar and the bottom panel.',
352
359
  'settings.rail.title': 'Action rail buttons',
353
360
  'settings.rail.hint': 'Tick the actions shown on the rail. The order is fixed; unticking one hides it.',
354
361
  'settings.rail.unavailable': 'This host has no settings service; use the in-panel “Action rail settings”.',
@@ -544,7 +551,10 @@ window.__ModuleLoader__.load({
544
551
  'rail.settings': '動作列設定',
545
552
  'rail.openSettings': '開啟外掛設定',
546
553
  'settings.card.title': 'Git 工具視窗設定',
547
- 'settings.card.desc': '動作列按鈕與 AI 提交訊息',
554
+ 'settings.card.desc': '面板放置、動作列按鈕與 AI 提交訊息',
555
+ 'settings.placement.title': '面板放置',
556
+ 'settings.placement.tab': '喺主對話區顯示 Git 分頁',
557
+ 'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
548
558
  'settings.rail.title': '動作列按鈕',
549
559
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
550
560
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -732,7 +742,10 @@ window.__ModuleLoader__.load({
732
742
  'rail.settings': '動作列設定',
733
743
  'rail.openSettings': '開啟外掛設定',
734
744
  'settings.card.title': 'Git 工具視窗設定',
735
- 'settings.card.desc': '動作列按鈕與 AI 提交訊息',
745
+ 'settings.card.desc': '面板放置、動作列按鈕與 AI 提交訊息',
746
+ 'settings.placement.title': '面板放置',
747
+ 'settings.placement.tab': '在主對話區顯示 Git 分頁',
748
+ 'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
736
749
  'settings.rail.title': '動作列按鈕',
737
750
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
738
751
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -920,7 +933,10 @@ window.__ModuleLoader__.load({
920
933
  'rail.settings': '動作列設定',
921
934
  'rail.openSettings': '開啟外掛設定',
922
935
  'settings.card.title': 'Git 工具視窗設定',
923
- 'settings.card.desc': '動作列按鈕與 AI 提交訊息',
936
+ 'settings.card.desc': '面板放置、動作列按鈕與 AI 提交訊息',
937
+ 'settings.placement.title': '面板放置',
938
+ 'settings.placement.tab': '喺主對話區顯示 Git 分頁',
939
+ 'settings.placement.tabHint': '關閉之後仍可從右側欄或底部面板開啟。',
924
940
  'settings.rail.title': '動作列按鈕',
925
941
  'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
926
942
  'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
@@ -1108,7 +1124,10 @@ window.__ModuleLoader__.load({
1108
1124
  'rail.settings': 'アクションバーの設定',
1109
1125
  'rail.openSettings': 'プラグイン設定を開く',
1110
1126
  'settings.card.title': 'Gitツールウィンドウの設定',
1111
- 'settings.card.desc': 'アクションレールのボタンと AI コミットメッセージ',
1127
+ 'settings.card.desc': 'パネル配置、アクションレールのボタンと AI コミットメッセージ',
1128
+ 'settings.placement.title': 'パネル配置',
1129
+ 'settings.placement.tab': '会話エリアに Git タブを表示',
1130
+ 'settings.placement.tabHint': 'オフにしても右サイドバーと下部パネルから開けます。',
1112
1131
  'settings.rail.title': 'アクションレールのボタン',
1113
1132
  'settings.rail.hint': 'レールに表示する操作にチェックを入れてください。順序は固定で、チェックを外すと非表示になります。',
1114
1133
  'settings.rail.unavailable': 'このホストには設定サービスがないため、パネル内の「アクションレール設定」を使ってください。',
@@ -1296,7 +1315,10 @@ window.__ModuleLoader__.load({
1296
1315
  'rail.settings': '작업 표시줄 설정',
1297
1316
  'rail.openSettings': '플러그인 설정 열기',
1298
1317
  'settings.card.title': 'Git 도구 창 설정',
1299
- 'settings.card.desc': '액션 레일 버튼 및 AI 커밋 메시지',
1318
+ 'settings.card.desc': '패널 배치, 액션 레일 버튼 및 AI 커밋 메시지',
1319
+ 'settings.placement.title': '패널 배치',
1320
+ 'settings.placement.tab': '대화 영역에 Git 탭 표시',
1321
+ 'settings.placement.tabHint': '꺼도 오른쪽 사이드바와 하단 패널에서 열 수 있습니다.',
1300
1322
  'settings.rail.title': '액션 레일 버튼',
1301
1323
  'settings.rail.hint': '레일에 표시할 동작을 선택하세요. 순서는 고정이며 선택을 해제하면 숨겨집니다.',
1302
1324
  'settings.rail.unavailable': '이 호스트에는 설정 서비스가 없으므로 패널의 "액션 레일 설정"을 사용하세요.',
@@ -1484,7 +1506,10 @@ window.__ModuleLoader__.load({
1484
1506
  'rail.settings': 'Einstellungen der Aktionsleiste',
1485
1507
  'rail.openSettings': 'Plugin-Einstellungen öffnen',
1486
1508
  'settings.card.title': 'Git-Werkzeugfenster-Einstellungen',
1487
- 'settings.card.desc': 'Aktionsleisten-Schaltflächen und KI-Commit-Nachricht',
1509
+ 'settings.card.desc': 'Panel-Platzierung, Aktionsleisten-Schaltflächen und KI-Commit-Nachricht',
1510
+ 'settings.placement.title': 'Panel-Platzierung',
1511
+ 'settings.placement.tab': 'Git-Tab im Gesprächsbereich anzeigen',
1512
+ 'settings.placement.tabHint': 'Ausgeschaltet bleibt das Panel über rechte Seitenleiste und unteres Panel erreichbar.',
1488
1513
  'settings.rail.title': 'Schaltflächen der Aktionsleiste',
1489
1514
  'settings.rail.hint': 'Die auf der Leiste sichtbaren Aktionen ankreuzen. Die Reihenfolge ist fest; abwählen blendet eine aus.',
1490
1515
  'settings.rail.unavailable': 'Dieser Host hat keinen Einstellungsdienst; nutze die „Aktionsleisten-Einstellungen“ im Panel.',
@@ -1672,7 +1697,10 @@ window.__ModuleLoader__.load({
1672
1697
  'rail.settings': 'Réglages de la barre d\'actions',
1673
1698
  'rail.openSettings': 'Ouvrir les réglages du plugin',
1674
1699
  'settings.card.title': 'Paramètres de la fenêtre d’outils Git',
1675
- 'settings.card.desc': 'Boutons de la barre d’actions et message de commit par IA',
1700
+ 'settings.card.desc': 'Placement du panneau, boutons de la barre d’actions et message de commit par IA',
1701
+ 'settings.placement.title': 'Placement du panneau',
1702
+ 'settings.placement.tab': 'Afficher l’onglet Git dans la zone de conversation',
1703
+ 'settings.placement.tabHint': 'Désactivé, le panneau reste accessible depuis la barre latérale droite et le panneau inférieur.',
1676
1704
  'settings.rail.title': 'Boutons de la barre d’actions',
1677
1705
  'settings.rail.hint': 'Cochez les actions affichées sur la barre. L’ordre est fixe ; décocher masque une action.',
1678
1706
  '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.',
@@ -1860,7 +1888,10 @@ window.__ModuleLoader__.load({
1860
1888
  'rail.settings': 'Настройки панели действий',
1861
1889
  'rail.openSettings': 'Открыть настройки плагина',
1862
1890
  'settings.card.title': 'Настройки окна инструментов Git',
1863
- 'settings.card.desc': 'Кнопки панели действий и сообщение коммита от ИИ',
1891
+ 'settings.card.desc': 'Размещение панели, кнопки панели действий и сообщение коммита от ИИ',
1892
+ 'settings.placement.title': 'Размещение панели',
1893
+ 'settings.placement.tab': 'Показывать вкладку Git в области диалога',
1894
+ 'settings.placement.tabHint': 'В выключенном виде панель доступна из правой боковой панели и нижней панели.',
1864
1895
  'settings.rail.title': 'Кнопки панели действий',
1865
1896
  'settings.rail.hint': 'Отметьте действия, показываемые на панели. Порядок фиксирован; снятие флажка скрывает действие.',
1866
1897
  'settings.rail.unavailable': 'На этом хосте нет службы настроек; используйте «Настройки панели действий» в панели.',
@@ -2048,7 +2079,10 @@ window.__ModuleLoader__.load({
2048
2079
  'rail.settings': 'Configurações da barra de ações',
2049
2080
  'rail.openSettings': 'Abrir configurações do plugin',
2050
2081
  'settings.card.title': 'Configurações da janela de ferramentas Git',
2051
- 'settings.card.desc': 'Botões da barra de ações e mensagem de commit por IA',
2082
+ 'settings.card.desc': 'Posição do painel, botões da barra de ações e mensagem de commit por IA',
2083
+ 'settings.placement.title': 'Posição do painel',
2084
+ 'settings.placement.tab': 'Mostrar a aba Git na área de conversa',
2085
+ 'settings.placement.tabHint': 'Desligado, o painel continua disponível na barra lateral direita e no painel inferior.',
2052
2086
  'settings.rail.title': 'Botões da barra de ações',
2053
2087
  'settings.rail.hint': 'Marque as ações exibidas na barra. A ordem é fixa; desmarcar oculta uma ação.',
2054
2088
  'settings.rail.unavailable': 'Este host não tem serviço de configurações; use «Configurações da barra de ações» no painel.',
@@ -2236,7 +2270,10 @@ window.__ModuleLoader__.load({
2236
2270
  'rail.settings': 'Impostazioni della barra delle azioni',
2237
2271
  'rail.openSettings': 'Apri le impostazioni del plugin',
2238
2272
  'settings.card.title': 'Impostazioni della finestra strumenti Git',
2239
- 'settings.card.desc': 'Pulsanti della barra azioni e messaggio di commit con IA',
2273
+ 'settings.card.desc': 'Posizione del pannello, pulsanti della barra azioni e messaggio di commit con IA',
2274
+ 'settings.placement.title': 'Posizione del pannello',
2275
+ 'settings.placement.tab': 'Mostra la scheda Git nell’area conversazione',
2276
+ 'settings.placement.tabHint': 'Da spento, il pannello resta raggiungibile dalla barra laterale destra e dal pannello inferiore.',
2240
2277
  'settings.rail.title': 'Pulsanti della barra azioni',
2241
2278
  'settings.rail.hint': 'Seleziona le azioni mostrate sulla barra. L’ordine è fisso; deselezionando se ne nasconde una.',
2242
2279
  'settings.rail.unavailable': 'Questo host non ha un servizio di impostazioni; usa «Impostazioni barra azioni» nel pannello.',
@@ -2424,7 +2461,10 @@ window.__ModuleLoader__.load({
2424
2461
  'rail.settings': 'Instellingen van de actiebalk',
2425
2462
  'rail.openSettings': 'Plugin-instellingen openen',
2426
2463
  'settings.card.title': 'Instellingen van het Git-werkvenster',
2427
- 'settings.card.desc': 'Knoppen van de actiebalk en AI-commitbericht',
2464
+ 'settings.card.desc': 'Paneelplaatsing, knoppen van de actiebalk en AI-commitbericht',
2465
+ 'settings.placement.title': 'Paneelplaatsing',
2466
+ 'settings.placement.tab': 'Git-tabblad in het gespreksgebied tonen',
2467
+ 'settings.placement.tabHint': 'Uitgeschakeld blijft het paneel bereikbaar via de rechterzijbalk en het onderpaneel.',
2428
2468
  'settings.rail.title': 'Knoppen van de actiebalk',
2429
2469
  'settings.rail.hint': 'Vink de acties aan die op de balk verschijnen. De volgorde ligt vast; uitvinken verbergt er een.',
2430
2470
  'settings.rail.unavailable': 'Deze host heeft geen instellingenservice; gebruik "Actiebalk-instellingen" in het paneel.',
@@ -2612,7 +2652,10 @@ window.__ModuleLoader__.load({
2612
2652
  'rail.settings': 'Ustawienia paska działań',
2613
2653
  'rail.openSettings': 'Otwórz ustawienia wtyczki',
2614
2654
  'settings.card.title': 'Ustawienia okna narzędzi Git',
2615
- 'settings.card.desc': 'Przyciski paska akcji i wiadomość commita od AI',
2655
+ 'settings.card.desc': 'Rozmieszczenie panelu, przyciski paska akcji i wiadomość commita od AI',
2656
+ 'settings.placement.title': 'Rozmieszczenie panelu',
2657
+ 'settings.placement.tab': 'Pokazuj kartę Git w obszarze rozmowy',
2658
+ 'settings.placement.tabHint': 'Po wyłączeniu panel pozostaje dostępny z prawego paska bocznego i dolnego panelu.',
2616
2659
  'settings.rail.title': 'Przyciski paska akcji',
2617
2660
  'settings.rail.hint': 'Zaznacz akcje widoczne na pasku. Kolejność jest stała; odznaczenie ukrywa akcję.',
2618
2661
  'settings.rail.unavailable': 'Ten host nie ma usługi ustawień; użyj „Ustawienia paska akcji” w panelu.',
@@ -2800,7 +2843,10 @@ window.__ModuleLoader__.load({
2800
2843
  'rail.settings': 'Inställningar för åtgärdsfältet',
2801
2844
  'rail.openSettings': 'Öppna plugininställningar',
2802
2845
  'settings.card.title': 'Inställningar för Git-verktygsfönstret',
2803
- 'settings.card.desc': 'Knappar i åtgärdsfältet och AI-commitmeddelande',
2846
+ 'settings.card.desc': 'Panelplacering, knappar i åtgärdsfältet och AI-commitmeddelande',
2847
+ 'settings.placement.title': 'Panelplacering',
2848
+ 'settings.placement.tab': 'Visa Git-fliken i konversationsytan',
2849
+ 'settings.placement.tabHint': 'Avstängd finns panelen kvar via höger sidopanel och nedre panelen.',
2804
2850
  'settings.rail.title': 'Knappar i åtgärdsfältet',
2805
2851
  'settings.rail.hint': 'Markera de åtgärder som visas i fältet. Ordningen är fast; avmarkering döljer en.',
2806
2852
  'settings.rail.unavailable': 'Denna värd saknar inställningstjänst; använd ”Åtgärdsfältets inställningar” i panelen.',
@@ -2988,7 +3034,10 @@ window.__ModuleLoader__.load({
2988
3034
  'rail.settings': 'Eylem çubuğu ayarları',
2989
3035
  'rail.openSettings': 'Eklenti ayarlarını aç',
2990
3036
  'settings.card.title': 'Git araç penceresi ayarları',
2991
- 'settings.card.desc': 'Eylem çubuğu düğmeleri ve AI commit mesajı',
3037
+ 'settings.card.desc': 'Panel yerleşimi, eylem çubuğu düğmeleri ve AI commit mesajı',
3038
+ 'settings.placement.title': 'Panel yerleşimi',
3039
+ 'settings.placement.tab': 'Konuşma alanında Git sekmesini göster',
3040
+ 'settings.placement.tabHint': 'Kapatıldığında panel sağ kenar çubuğundan ve alt panelden açılabilir.',
2992
3041
  'settings.rail.title': 'Eylem çubuğu düğmeleri',
2993
3042
  'settings.rail.hint': 'Çubukta gösterilecek eylemleri işaretleyin. Sıra sabittir; işareti kaldırmak eylemi gizler.',
2994
3043
  'settings.rail.unavailable': 'Bu ana bilgisayarda ayar hizmeti yok; paneldeki "Eylem çubuğu ayarları"nı kullanın.',
@@ -3176,7 +3225,10 @@ window.__ModuleLoader__.load({
3176
3225
  'rail.settings': 'Pengaturan bilah tindakan',
3177
3226
  'rail.openSettings': 'Buka pengaturan plugin',
3178
3227
  'settings.card.title': 'Pengaturan jendela alat Git',
3179
- 'settings.card.desc': 'Tombol bilah aksi dan pesan commit AI',
3228
+ 'settings.card.desc': 'Penempatan panel, tombol bilah aksi dan pesan commit AI',
3229
+ 'settings.placement.title': 'Penempatan panel',
3230
+ 'settings.placement.tab': 'Tampilkan tab Git di area percakapan',
3231
+ 'settings.placement.tabHint': 'Dimatikan, panel tetap bisa dibuka dari bilah sisi kanan dan panel bawah.',
3180
3232
  'settings.rail.title': 'Tombol bilah aksi',
3181
3233
  'settings.rail.hint': 'Centang aksi yang ditampilkan di bilah. Urutannya tetap; menghapus centang menyembunyikannya.',
3182
3234
  'settings.rail.unavailable': 'Host ini tidak punya layanan pengaturan; gunakan "Pengaturan bilah aksi" di panel.',
@@ -3364,7 +3416,10 @@ window.__ModuleLoader__.load({
3364
3416
  'rail.settings': 'Cài đặt thanh thao tác',
3365
3417
  'rail.openSettings': 'Mở cài đặt plugin',
3366
3418
  'settings.card.title': 'Cài đặt cửa sổ công cụ Git',
3367
- 'settings.card.desc': 'Nút thanh tác vụ và thông điệp commit do AI viết',
3419
+ 'settings.card.desc': 'Vị trí bảng, nút thanh tác vụ và thông điệp commit do AI viết',
3420
+ 'settings.placement.title': 'Vị trí bảng',
3421
+ 'settings.placement.tab': 'Hiện thẻ Git trong vùng hội thoại',
3422
+ 'settings.placement.tabHint': 'Tắt rồi, bảng vẫn mở được từ thanh bên phải và bảng dưới.',
3368
3423
  'settings.rail.title': 'Nút thanh tác vụ',
3369
3424
  '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ụ đó.',
3370
3425
  '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.',
@@ -3552,7 +3607,10 @@ window.__ModuleLoader__.load({
3552
3607
  'rail.settings': 'إعدادات شريط الإجراءات',
3553
3608
  'rail.openSettings': 'فتح إعدادات الإضافة',
3554
3609
  'settings.card.title': 'إعدادات نافذة أدوات Git',
3555
- 'settings.card.desc': 'أزرار شريط الإجراءات ورسالة إيداع بالذكاء الاصطناعي',
3610
+ 'settings.card.desc': 'موضع اللوحة، أزرار شريط الإجراءات ورسالة إيداع بالذكاء الاصطناعي',
3611
+ 'settings.placement.title': 'موضع اللوحة',
3612
+ 'settings.placement.tab': 'إظهار تبويب Git في منطقة المحادثة',
3613
+ 'settings.placement.tabHint': 'عند الإيقاف تبقى اللوحة متاحة من الشريط الجانبي الأيمن واللوحة السفلية.',
3556
3614
  'settings.rail.title': 'أزرار شريط الإجراءات',
3557
3615
  'settings.rail.hint': 'حدّد الإجراءات المعروضة على الشريط. الترتيب ثابت، وإلغاء التحديد يخفي الإجراء.',
3558
3616
  'settings.rail.unavailable': 'لا تتوفر خدمة إعدادات على هذا المضيف؛ استخدم «إعدادات شريط الإجراءات» في اللوحة.',
@@ -3740,7 +3798,10 @@ window.__ModuleLoader__.load({
3740
3798
  'rail.settings': 'क्रिया पट्टी सेटिंग्स',
3741
3799
  'rail.openSettings': 'प्लगइन सेटिंग खोलें',
3742
3800
  'settings.card.title': 'Git टूल विंडो सेटिंग्स',
3743
- 'settings.card.desc': 'एक्शन रेल बटन और AI कमिट संदेश',
3801
+ 'settings.card.desc': 'पैनल प्लेसमेंट, एक्शन रेल बटन और AI कमिट संदेश',
3802
+ 'settings.placement.title': 'पैनल प्लेसमेंट',
3803
+ 'settings.placement.tab': 'संवाद क्षेत्र में Git टैब दिखाएँ',
3804
+ 'settings.placement.tabHint': 'बंद करने पर भी पैनल दाएँ साइडबार और निचले पैनल से खुलता है।',
3744
3805
  'settings.rail.title': 'एक्शन रेल बटन',
3745
3806
  'settings.rail.hint': 'रेल पर दिखने वाली क्रियाएँ चुनें। क्रम निश्चित है; चुनाव हटाने पर वह छिप जाती है।',
3746
3807
  'settings.rail.unavailable': 'इस होस्ट में सेटिंग सेवा नहीं है; पैनल में «एक्शन रेल सेटिंग» का उपयोग करें।',
@@ -3928,7 +3989,10 @@ window.__ModuleLoader__.load({
3928
3989
  'rail.settings': 'ตั้งค่าแถบการทำงาน',
3929
3990
  'rail.openSettings': 'เปิดการตั้งค่าปลั๊กอิน',
3930
3991
  'settings.card.title': 'การตั้งค่าหน้าต่างเครื่องมือ Git',
3931
- 'settings.card.desc': 'ปุ่มแถบการทำงานและข้อความคอมมิตโดย AI',
3992
+ 'settings.card.desc': 'ตำแหน่งแผง, ปุ่มแถบการทำงานและข้อความคอมมิตโดย AI',
3993
+ 'settings.placement.title': 'ตำแหน่งแผง',
3994
+ 'settings.placement.tab': 'แสดงแท็บ Git ในพื้นที่บทสนทนา',
3995
+ 'settings.placement.tabHint': 'ปิดแล้วยังเปิดแผงจากแถบข้างขวาหรือแผงล่างได้',
3932
3996
  'settings.rail.title': 'ปุ่มแถบการทำงาน',
3933
3997
  'settings.rail.hint': 'ทำเครื่องหมายการทำงานที่จะแสดงบนแถบ ลำดับคงที่; ยกเลิกเครื่องหมายเพื่อซ่อน',
3934
3998
  'settings.rail.unavailable': 'โฮสต์นี้ไม่มีบริการตั้งค่า ให้ใช้ «ตั้งค่าแถบการทำงาน» ในแผง',
@@ -4088,12 +4152,76 @@ window.__ModuleLoader__.load({
4088
4152
 
4089
4153
  /* ============================== api ============================== */
4090
4154
 
4155
+ /* ---- remote channel fallback core (pure: tests/smoke.mjs slices this
4156
+ block) ---- */
4157
+ /* dsh-remote-web-ui's browser channel rewrites only the host's own /api/,
4158
+ /sidebar/, /git/ and /pet/ prefixes onto its /remote/ proxy; a
4159
+ plugin-owned route like ours is not on that list, so from a paired
4160
+ remote client the direct call reaches the harness browser-auth gate and
4161
+ comes back 401/403 "forbidden" (issue #9: both sidebar and dock panels
4162
+ refused to open remotely). The proxy itself serves any loopback path
4163
+ under /remote/<full pathname> with the paired device's credentials, so
4164
+ the fallback is to retry the very same request through it — the exact
4165
+ URL shape the boot patch produces for the host prefixes ('/remote' +
4166
+ the request's full pathname, mount prefix included, query preserved). */
4167
+ function remoteFallbackUrl(ofPath) {
4168
+ /* Resolve through the SAME base the direct fetch used — document.baseURI
4169
+ (which honours <base href>; dsh 0.1.7 mounts the shell with one), so
4170
+ the retry lands on the proxy copy of the very URL that was rejected. */
4171
+ let resolved = null
4172
+ try {
4173
+ let base
4174
+ if (typeof document === 'object' && document !== null && typeof document.baseURI === 'string' && document.baseURI !== '') {
4175
+ base = document.baseURI
4176
+ } else if (typeof window === 'object' && window !== null && window.location !== undefined && window.location !== null) {
4177
+ base = window.location.href
4178
+ }
4179
+ if (base !== undefined) resolved = new URL(ofPath, base)
4180
+ } catch (error) { resolved = null }
4181
+ if (resolved === null) return '/remote/' + String(ofPath).replace(/^\//, '')
4182
+ return '/remote' + resolved.pathname + resolved.search
4183
+ }
4184
+
4185
+ /* A rejection worth retrying through the remote channel: the auth gate
4186
+ answers 401 (unauthenticated) or 403 (forbidden); every other status —
4187
+ including this route's own JSON errors, which answer 200/4xx with a
4188
+ parsed body — is a real answer, not a transport fence. */
4189
+ function isAuthRejection(status) {
4190
+ return status === 401 || status === 403
4191
+ }
4192
+ /* ---- end remote channel fallback core ---- */
4193
+
4194
+ /* Sticky remote-channel mode: once the proxy has answered, later calls go
4195
+ there directly — otherwise every call from a remote client pays the
4196
+ 401/403 round trip first. Loopback desktop browsing never trips the
4197
+ fallback at all (same-origin requests are always admitted). */
4198
+ let viaRemoteChannel = false
4199
+
4091
4200
  async function request(method, payload) {
4092
- const response = await fetch(API_BASE + '/' + method, {
4201
+ const init = {
4093
4202
  method: 'POST',
4094
4203
  headers: { 'content-type': 'application/json' },
4095
4204
  body: JSON.stringify(payload === undefined ? {} : payload),
4096
- })
4205
+ }
4206
+ const direct = API_BASE + '/' + method
4207
+ let response = null
4208
+ if (viaRemoteChannel === true) {
4209
+ response = await fetch(remoteFallbackUrl(direct), init)
4210
+ } else {
4211
+ response = await fetch(direct, init)
4212
+ if (isAuthRejection(response.status) === true) {
4213
+ /* Off the origin's trust path: try the pairing proxy once. With no
4214
+ remote-web-ui installed the proxy path 404s — the retry then also
4215
+ fails, and the ORIGINAL rejection is what the caller sees (a
4216
+ proxy answer that is itself an auth rejection is discarded the
4217
+ same way, so a genuine cross-site refusal can never be masked). */
4218
+ const retried = await fetch(remoteFallbackUrl(direct), init)
4219
+ if (isAuthRejection(retried.status) !== true) {
4220
+ viaRemoteChannel = true
4221
+ response = retried
4222
+ }
4223
+ }
4224
+ }
4097
4225
  let body = null
4098
4226
  try { body = await response.json() } catch (error) { void error }
4099
4227
  if (body === null || typeof body !== 'object') throw new Error('HTTP ' + response.status)
@@ -4106,6 +4234,52 @@ window.__ModuleLoader__.load({
4106
4234
  return body.data
4107
4235
  }
4108
4236
 
4237
+ /* ============================== first-paint cache ============================== */
4238
+
4239
+ /* Session-scoped seats (the conversation view, and every sidebar seat that
4240
+ follows the active session) REMOUNT their panel on each session switch:
4241
+ the slot subtree belongs to the session. Re-running the whole discovery
4242
+ (repos scan) + first page (summary / branches / log — three git spawns)
4243
+ on every switch made the chrome re-appear piece by piece (reported
4244
+ against v0.13.0). Same workspace ⇒ same repository ⇒ the previous paint
4245
+ is still essentially correct, so it seeds the states and a background
4246
+ refresh lands the fresh data a moment later. Pure functions; the panel
4247
+ only ever reads them at effect time and writes after a fetch. */
4248
+ const PAINT_TTL_MS = 30_000
4249
+ const paintRepos = new Map() /* cwd -> { at, data } */
4250
+ const paintPage = new Map() /* repoRoot -> { at, summary, branches, commits, hasMore } */
4251
+ const paintRoot = new Map() /* cwd -> last repoRoot the user was looking at */
4252
+
4253
+ function readPaintRepos(cwd) {
4254
+ const entry = paintRepos.get(cwd)
4255
+ if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4256
+ return entry.data
4257
+ }
4258
+
4259
+ function writePaintRepos(cwd, data) {
4260
+ paintRepos.set(cwd, { at: Date.now(), data: data })
4261
+ if (paintRepos.size > 16) paintRepos.delete(paintRepos.keys().next().value)
4262
+ }
4263
+
4264
+ function readPaintPage(repoRoot) {
4265
+ const entry = paintPage.get(repoRoot)
4266
+ if (entry === undefined || Date.now() - entry.at > PAINT_TTL_MS) return undefined
4267
+ return entry
4268
+ }
4269
+
4270
+ function writePaintPage(repoRoot, page) {
4271
+ paintPage.set(repoRoot, Object.assign({ at: Date.now() }, page))
4272
+ if (paintPage.size > 16) paintPage.delete(paintPage.keys().next().value)
4273
+ }
4274
+
4275
+ function rememberPaintRoot(cwd, repoRoot) {
4276
+ if (typeof cwd === 'string' && cwd !== '' && repoRoot !== null) paintRoot.set(cwd, repoRoot)
4277
+ }
4278
+
4279
+ function readPaintRoot(cwd) {
4280
+ return paintRoot.get(cwd)
4281
+ }
4282
+
4109
4283
  /* ============================== storage ============================== */
4110
4284
 
4111
4285
  /* ---- ai commit pickers (issue #7): the model catalog and per-model
@@ -5447,6 +5621,49 @@ window.__ModuleLoader__.load({
5447
5621
  E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: spec.icon, size: 13 })),
5448
5622
  E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t(spec.key)))
5449
5623
  })),
5624
+ /* Placement (issue #8): one switch, the same optimistic-write contract
5625
+ the rail rows keep. The conversation tab registers/unregisters live
5626
+ (see door 3 in apply), so the flip is visible without a reload. */
5627
+ E('div', { className: 'dig-settings-sub' }, t('settings.placement.title')),
5628
+ (() => {
5629
+ const field = 'conversationTab'
5630
+ const inputId = 'dig-placement-' + field
5631
+ const shown = Object.prototype.hasOwnProperty.call(intent.current, field)
5632
+ ? intent.current[field] === true
5633
+ : values[field] !== false
5634
+ const toggle = (event) => {
5635
+ if (readOnly === true) { setNote(t('settings.rail.readonly')); return }
5636
+ const fromControl = event !== undefined && event !== null && event.target !== null
5637
+ && event.target !== undefined && typeof event.target.checked === 'boolean'
5638
+ ? event.target.checked
5639
+ : null
5640
+ const wanted = fromControl !== null
5641
+ ? fromControl
5642
+ : (Object.prototype.hasOwnProperty.call(intent.current, field)
5643
+ ? intent.current[field] !== true
5644
+ : values[field] !== false) !== true
5645
+ intent.current[field] = wanted
5646
+ dirty.current.set(field, wanted)
5647
+ setPending((now) => Object.assign({}, now, { [field]: wanted }))
5648
+ setNote('')
5649
+ bump()
5650
+ scheduleFlush()
5651
+ }
5652
+ return E('div', { key: field },
5653
+ E('div', { className: 'dig-settings-row', 'data-placement-row': field },
5654
+ E('input', {
5655
+ id: inputId,
5656
+ type: 'checkbox',
5657
+ className: 'dig-settings-check',
5658
+ 'aria-labelledby': inputId + '-label',
5659
+ checked: shown,
5660
+ disabled: readOnly === true,
5661
+ onChange: toggle,
5662
+ }),
5663
+ E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: 'commit', size: 13 })),
5664
+ E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t('settings.placement.tab'))),
5665
+ E('div', { className: 'dig-settings-subnote' }, t('settings.placement.tabHint')))
5666
+ })(),
5450
5667
  E('div', { className: 'dig-settings-sub' }, t('settings.commit.title')),
5451
5668
  (() => {
5452
5669
  /* The model row is a picker over the host's provider catalog; while
@@ -6628,18 +6845,30 @@ window.__ModuleLoader__.load({
6628
6845
  as "still loading" and would spin forever). */
6629
6846
  if (cwd === undefined) { setRepoState({ repos: [], isRepo: false }); return undefined }
6630
6847
  let cancelled = false
6848
+ /* First-paint cache: a session switch through the same workspace
6849
+ reuses the previous discovery synchronously (repo bar and pick land
6850
+ with the first paint), then the background scan re-proves it. */
6851
+ const apply = (data, cachedPick) => {
6852
+ setRepoState(data)
6853
+ const paths = data.repos.map((repo) => repo.path)
6854
+ const remembered = readRememberedRepo(sessionId)
6855
+ let pick = null
6856
+ if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
6857
+ else if (cachedPick !== undefined && paths.indexOf(cachedPick) >= 0) pick = cachedPick
6858
+ else if (data.isRepo === true) pick = data.cwd
6859
+ else if (paths.length > 0) pick = paths[0]
6860
+ setRepoRoot(pick)
6861
+ rememberPaintRoot(cwd, pick)
6862
+ return pick
6863
+ }
6864
+ const cached = readPaintRepos(cwd)
6865
+ if (cached !== undefined) apply(cached, readPaintRoot(cwd))
6631
6866
  void (async () => {
6632
6867
  try {
6633
6868
  const data = await request('repos', { cwd: cwd })
6634
6869
  if (cancelled) return
6635
- setRepoState(data)
6636
- const paths = data.repos.map((repo) => repo.path)
6637
- const remembered = readRememberedRepo(sessionId)
6638
- let pick = null
6639
- if (remembered !== undefined && paths.indexOf(remembered) >= 0) pick = remembered
6640
- else if (data.isRepo === true) pick = data.cwd
6641
- else if (paths.length > 0) pick = paths[0]
6642
- setRepoRoot(pick)
6870
+ writePaintRepos(cwd, data)
6871
+ apply(data, readPaintRoot(cwd))
6643
6872
  } catch (caught) {
6644
6873
  if (!cancelled) setError(caught instanceof Error ? caught.message : String(caught))
6645
6874
  }
@@ -6654,7 +6883,9 @@ window.__ModuleLoader__.load({
6654
6883
  }, [base, showIgnored])
6655
6884
 
6656
6885
  const loadBranches = useCallback(async () => {
6657
- setBranches(await request('branches', base))
6886
+ const data = await request('branches', base)
6887
+ setBranches(data)
6888
+ return data
6658
6889
  }, [base])
6659
6890
 
6660
6891
  const loadCommits = useCallback(async (skip, overridePath) => {
@@ -6664,19 +6895,39 @@ window.__ModuleLoader__.load({
6664
6895
  const data = await request('log', payload)
6665
6896
  setCommits((previous) => (skip === undefined || skip === 0 ? data.commits : previous.concat(data.commits)))
6666
6897
  setHasMore(data.hasMore)
6898
+ return data
6667
6899
  }, [base, pathFilter])
6668
6900
 
6669
6901
  const refresh = useCallback(async () => {
6670
6902
  if (repoRoot === null) return
6671
6903
  await guard(async () => {
6672
- await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
6904
+ const [summaryData, branchesData, page] = await Promise.all([loadSummary(), loadBranches(), loadCommits(0)])
6905
+ /* The write side of the first-paint cache: exactly the FIRST page
6906
+ (a paged fetch never reaches this path), replaced on every
6907
+ refresh — the auto-refresh included — so a session switch through
6908
+ the same workspace seeds fresh data, not the mount-time
6909
+ snapshot. */
6910
+ writePaintPage(repoRoot, { summary: summaryData, branches: branchesData, commits: page.commits, hasMore: page.hasMore })
6673
6911
  })
6674
6912
  }, [guard, loadSummary, loadBranches, loadCommits, repoRoot])
6675
6913
 
6676
6914
  useEffect(() => {
6677
6915
  if (repoRoot === null) return undefined
6916
+ /* First-paint cache: seed the first page synchronously so a session
6917
+ switch through the same workspace paints the full chrome (rail,
6918
+ branch bar, history) at once instead of re-growing it piece by
6919
+ piece; the refresh below re-proves every field right after. Only a
6920
+ cold mount seeds — a later effect run (tick / repo switch) must not
6921
+ roll live state back to a stale snapshot. */
6922
+ const cachedPage = readPaintPage(repoRoot)
6923
+ if (cachedPage !== undefined) {
6924
+ if (summary === null) setSummary(cachedPage.summary)
6925
+ if (branches === null) setBranches(cachedPage.branches)
6926
+ if (commits.length === 0) { setCommits(cachedPage.commits); setHasMore(cachedPage.hasMore) }
6927
+ }
6678
6928
  void refresh()
6679
6929
  return undefined
6930
+ // eslint-disable-next-line react-hooks/exhaustive-deps
6680
6931
  }, [repoRoot, refresh, tick])
6681
6932
 
6682
6933
  useEffect(() => {
@@ -7466,7 +7717,7 @@ window.__ModuleLoader__.load({
7466
7717
 
7467
7718
  const CSS = [
7468
7719
  '.dig-root{position:relative;display:flex;flex-direction:column;height:100%;min-height:0;background:transparent;color:var(--dsw-alias-label-primary);font-family:var(--dsw-font-family,system-ui,sans-serif);font-size:13px;line-height:1.5;font-weight:600;overflow:hidden}',
7469
- '.dig-topbar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none;min-width:0;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1));overflow:hidden}',
7720
+ '.dig-topbar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none;min-width:0;border-bottom:1px solid var(--dsw-alias-border-l2);overflow:hidden}',
7470
7721
  '.dig-topbar-spacer{flex:1;min-width:4px}',
7471
7722
  '.dig-busy{color:var(--dsw-alias-label-tertiary);flex:none;white-space:nowrap}',
7472
7723
  '.dig-track{color:var(--dsw-alias-label-secondary);flex:none;font-variant-numeric:tabular-nums;white-space:nowrap}',
@@ -7489,10 +7740,10 @@ window.__ModuleLoader__.load({
7489
7740
  '.dig-note{display:flex;align-items:center;gap:8px;padding:4px 8px;flex:none;color:var(--dsw-alias-label-secondary)}',
7490
7741
  '.dig-banner-text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis}',
7491
7742
  '.dig-shell{flex:1;min-height:0;display:flex;overflow:hidden}',
7492
- '.dig-rail{width:32px;flex:none;display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 0;overflow:hidden;border-right:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7743
+ '.dig-rail{width:32px;flex:none;display:flex;flex-direction:column;align-items:center;gap:2px;padding:4px 0;overflow:hidden;border-right:1px solid var(--dsw-alias-border-l2)}',
7493
7744
  '.dig-rail-gap{flex:1;min-height:2px}',
7494
7745
  '.dig-rail-gap-x{flex:1;min-width:2px}',
7495
- '.dig-rail-row{display:flex;align-items:center;gap:2px;padding:3px 6px;flex:none;overflow:hidden;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7746
+ '.dig-rail-row{display:flex;align-items:center;gap:2px;padding:3px 6px;flex:none;overflow:hidden;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7496
7747
  '.dig-rail-btn{display:inline-flex;align-items:center;justify-content:center;width:24px;height:24px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;padding:0;flex:none}',
7497
7748
  '.dig-rail-btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
7498
7749
  '.dig-rail-btn:disabled{opacity:.28;cursor:default}',
@@ -7575,13 +7826,13 @@ window.__ModuleLoader__.load({
7575
7826
  '.dig-body-stack{flex-direction:column}',
7576
7827
  '.dig-body-compact{flex-direction:column}',
7577
7828
  '.dig-pane{display:flex;flex-direction:column;min-height:0;min-width:0}',
7578
- '.dig-pane-tree{width:200px;flex:none;border-right:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7579
- '.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7829
+ '.dig-pane-tree{width:200px;flex:none;border-right:1px solid var(--dsw-alias-border-l2)}',
7830
+ '.dig-pane-tree-stack{max-height:36%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7580
7831
  '.dig-pane-main{flex:1;min-width:0}',
7581
- '.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7582
- '.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7832
+ '.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-border-l2)}',
7833
+ '.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7583
7834
  '.dig-compact-bar{display:flex;align-items:center;gap:6px;padding:4px 6px;flex:none}',
7584
- '.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7835
+ '.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7585
7836
  // A divider: an 8px hit area (a fingertip needs it) painted as a 1px hairline
7586
7837
  // that lights up on hover, focus or while dragging. touch-action:none is what
7587
7838
  // makes a touch drag resize the pane instead of scrolling the panel.
@@ -7665,7 +7916,7 @@ window.__ModuleLoader__.load({
7665
7916
  '.dig-ref-remote{color:#b083f0}',
7666
7917
  '.dig-ref-tag{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
7667
7918
  '.dig-ref-more{color:var(--dsw-alias-label-tertiary)}',
7668
- '.dig-filters{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;min-width:0;overflow-x:auto;overflow-y:hidden;border-bottom:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7919
+ '.dig-filters{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;min-width:0;overflow-x:auto;overflow-y:hidden;border-bottom:1px solid var(--dsw-alias-border-l2)}',
7669
7920
  '.dig-filter-text{flex:1 1 90px;min-width:80px;width:auto}',
7670
7921
  '.dig-filter-select{flex:none;max-width:118px;appearance:none;-webkit-appearance:none;background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);color:var(--dsw-alias-label-secondary);font:inherit;font-weight:500;height:22px;padding:0 4px;cursor:pointer;text-overflow:ellipsis}',
7671
7922
  '.dig-filter-select:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
@@ -7695,7 +7946,7 @@ window.__ModuleLoader__.load({
7695
7946
  '.dig-mini{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border:none;border-radius:var(--dsw-radius-xs,4px);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer;opacity:0;flex:none;padding:0}',
7696
7947
  '.dig-row:hover .dig-mini{opacity:1}',
7697
7948
  '.dig-mini:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
7698
- '.dig-commit-box{flex:none;padding:6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7949
+ '.dig-commit-box{flex:none;padding:6px;display:flex;flex-direction:column;gap:6px;border-top:1px solid var(--dsw-alias-border-l2)}',
7699
7950
  '.dig-commit-box-compact{flex-direction:row;align-items:center;gap:6px;padding:4px 6px;flex-wrap:wrap}',
7700
7951
  '.dig-textarea{width:100%;box-sizing:border-box;min-height:50px;resize:vertical;padding:5px 6px;border-radius:var(--dsw-radius-sm,8px);border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;outline:none}',
7701
7952
  '.dig-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}',
@@ -7717,7 +7968,7 @@ window.__ModuleLoader__.load({
7717
7968
  '.dig-detail-files-head{font-weight:600;margin-top:4px}',
7718
7969
  '.dig-detail-files{display:flex;flex-direction:column}',
7719
7970
  '.dig-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
7720
- '.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1))}',
7971
+ '.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-border-l2)}',
7721
7972
  '.dig-diff-head{display:flex;align-items:center;gap:8px;padding:3px 8px;color:var(--dsw-alias-label-tertiary);flex:none;min-width:0}',
7722
7973
  '.dig-diff-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
7723
7974
  '.dig-diff{flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;line-height:1.5;font-weight:500;min-height:0}',
@@ -7757,6 +8008,10 @@ window.__ModuleLoader__.load({
7757
8008
  const NATIVE_ID = 'dsh-ide-git'
7758
8009
  const NATIVE_KIND = 'ide-git'
7759
8010
  const NATIVE_WORKSPACE_ITEMS = (state) => state.items
8011
+ /* The conversation.view slot id (issue #8). Order 30 sits the tab after
8012
+ Chat (0) and Trajectory (10), and after dsh-context's tab (20) when both
8013
+ are installed. */
8014
+ const CONVERSATION_VIEW_ID = 'ide-git'
7760
8015
 
7761
8016
  /* The native seat hands a tab the standard props; the panel wants
7762
8017
  `{ scope, t }`. A session's working directory is the path of the
@@ -7789,6 +8044,67 @@ window.__ModuleLoader__.load({
7789
8044
  return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings })
7790
8045
  }
7791
8046
 
8047
+ /* The conversation-area seat (issue #8): the same panel as a view tab
8048
+ beside 对话/轨迹. The session's own cwd decides the repository — the
8049
+ harness records it when the session starts in a workspace, so this is
8050
+ the same source the sidebar seats resolve through. A session without a
8051
+ cwd (blank workspace, or a host without the sessions service) shows the
8052
+ panel's repo-pick state, never an error. */
8053
+ function sessionCwdOf(ctx, sessionId) {
8054
+ if (typeof sessionId !== 'string' || sessionId === '') return undefined
8055
+ try {
8056
+ const sessions = ctx.get('sessions')
8057
+ const list = sessions !== undefined && sessions !== null ? sessions.list : undefined
8058
+ if (list === undefined || typeof list.getSnapshot !== 'function') return undefined
8059
+ const snapshot = list.getSnapshot()
8060
+ const entry = snapshot !== null && typeof snapshot === 'object' && snapshot.byId !== undefined
8061
+ && snapshot.byId[sessionId] !== undefined && snapshot.byId[sessionId] !== null
8062
+ ? snapshot.byId[sessionId] : undefined
8063
+ if (entry !== undefined && typeof entry.cwd === 'string' && entry.cwd !== '') return entry.cwd
8064
+ } catch (error) { void error }
8065
+ return undefined
8066
+ }
8067
+
8068
+ /* The conversation shell re-renders on EVERY keystroke (the composer owns
8069
+ the draft state), and the view area re-renders with it. Without a bail-out
8070
+ at this boundary that cascade dragged the ENTIRE Git panel tree — branch
8071
+ rows, graph lanes, changes — through a synchronous render per keypress
8072
+ while the Git view was the active one (typing lag, reported against
8073
+ v0.13.0). The component consumes exactly three stable props (sessionId,
8074
+ t, ctx); everything else the slot passes (inspectCall, viewRequest, …)
8075
+ changes identity per shell render and is deliberately ignored, so the
8076
+ comparator is: same session, same bindings → skip the subtree. cwd moves
8077
+ through the sessions subscription inside, not through props. */
8078
+ const ConversationPanel = memo(function ConversationPanel(props) {
8079
+ const ctx = props.ctx
8080
+ const sessionId = typeof props.sessionId === 'string' ? props.sessionId : ''
8081
+ const [cwd, setCwd] = useState(() => sessionCwdOf(ctx, sessionId))
8082
+ useEffect(() => {
8083
+ let alive = true
8084
+ const sync = () => { if (alive === true) setCwd(sessionCwdOf(ctx, sessionId)) }
8085
+ sync()
8086
+ /* The session list is a live store; without a subscription a later
8087
+ start (first message in a blank session) would leave the tab on the
8088
+ no-repo chrome until the next remount. The notify walks are cheap:
8089
+ getSnapshot + a string compare, and setCwd with an identical string
8090
+ bails out without a render. */
8091
+ let unsubscribe
8092
+ try {
8093
+ const sessions = ctx.get('sessions')
8094
+ const list = sessions !== undefined && sessions !== null ? sessions.list : undefined
8095
+ if (list !== undefined && typeof list.subscribe === 'function') unsubscribe = list.subscribe(sync)
8096
+ } catch (error) { void error }
8097
+ return () => {
8098
+ alive = false
8099
+ if (typeof unsubscribe === 'function') {
8100
+ try { unsubscribe() } catch (error) { void error }
8101
+ }
8102
+ }
8103
+ }, [ctx, sessionId])
8104
+ const scope = useMemo(() => ({ cwd: cwd, sessionId: sessionId }), [cwd, sessionId])
8105
+ return E(LocaleLive, { ctx: ctx, scope: scope, t: props.t, visible: true })
8106
+ }, (prev, next) => prev.sessionId === next.sessionId && prev.t === next.t && prev.ctx === next.ctx)
8107
+
7792
8108
  function NativePanel(props) {
7793
8109
  const workspaces = typeof props.useWorkspaces === 'function'
7794
8110
  ? props.useWorkspaces(NATIVE_WORKSPACE_ITEMS)
@@ -7927,6 +8243,60 @@ window.__ModuleLoader__.load({
7927
8243
  there, so only a host without better-sidebar reaches the native seats. */
7928
8244
  if (!hostedByBetterSidebar) hostNatively()
7929
8245
 
8246
+ /* ---- door 3: the conversation-area view tab (issue #8) ---- */
8247
+ /* A third seat beside 对话/轨迹, registered into ui-conversation's
8248
+ `conversation.view` slot (the same contract dsh-context rides). It is
8249
+ ON by default and follows the plugin settings card's switch (row
8250
+ Config field `conversationTab`) live — no reload. Hosts without the
8251
+ row Config keep the tab: there is no editor surface there to turn it
8252
+ off with, and the card says as much for the rail already. The seat
8253
+ rides `slots` (already a declared inject), so a host without
8254
+ ui-conversation simply never mounts it — nothing throws. */
8255
+ const conversationTabEnabled = () => {
8256
+ const ready = railFormReady()
8257
+ if (ready === null) return true
8258
+ const value = ready.value !== null && typeof ready.value === 'object' ? ready.value.conversationTab : undefined
8259
+ return value !== false
8260
+ }
8261
+ ctx.inject(['slots'], (viewCtx) => {
8262
+ const slots = viewCtx.get('slots')
8263
+ if (slots === undefined || slots === null || typeof slots.inject !== 'function') return
8264
+ viewCtx.effect(() => {
8265
+ let offView = null
8266
+ const view = (viewProps) => E(ConversationPanel, Object.assign({}, viewProps, { t: t, ctx: ctx }))
8267
+ const sync = () => {
8268
+ const enabled = conversationTabEnabled()
8269
+ if (enabled === true && offView === null) {
8270
+ try {
8271
+ offView = slots.inject('conversation.view', () => slots.register(
8272
+ { name: 'conversation.view', id: CONVERSATION_VIEW_ID, order: 30, locale: LOCALE_NS, label: () => t('title') },
8273
+ view,
8274
+ ))
8275
+ } catch (error) {
8276
+ /* A late re-registration can lose a race with the previous
8277
+ entry's asynchronous disposal (same keyed cell, same
8278
+ priority): better one missed re-registration — the next
8279
+ config change or reload retries — than a sync that throws
8280
+ inside every rail-config notification. */
8281
+ console.warn('[dsh-ide-git] conversation seat registration failed: ' + String(error && error.message ? error.message : error))
8282
+ }
8283
+ } else if (enabled !== true && offView !== null) {
8284
+ try { offView() } catch (error) { void error }
8285
+ offView = null
8286
+ }
8287
+ }
8288
+ sync()
8289
+ const unsubscribe = subscribeRailConfig(sync)
8290
+ return () => {
8291
+ unsubscribe()
8292
+ if (offView !== null) {
8293
+ try { offView() } catch (error) { void error }
8294
+ offView = null
8295
+ }
8296
+ }
8297
+ }, 'dsh-ide-git: conversation view tab')
8298
+ })
8299
+
7930
8300
  /* ---- settings: the row Config is the rail's source of truth ---- */
7931
8301
  /* dsh >= 0.1.7 serves a plugin's row Config to the browser through
7932
8302
  ctx.configForms; from then on the settings card and the rail read that one
package/src/index.js CHANGED
@@ -65,6 +65,11 @@ function railConfigSchema(schema) {
65
65
  shape.commitModel = live(schema.string().default(''))
66
66
  shape.commitReasoning = live(schema.string().default(''))
67
67
  shape.commitPrompt = live(schema.string().default(''))
68
+ /* Conversation-area view tab (issue #8): on by default, switchable off from
69
+ the plugin settings card. false means "do not register the
70
+ conversation.view seat at all" — the sidebar and dock seats are not
71
+ affected; a missing field means the default (shown). */
72
+ shape.conversationTab = live(schema.boolean().default(true))
68
73
  return schema.object(shape)
69
74
  }
70
75