dsh-ide-git 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/client.js CHANGED
@@ -38,6 +38,9 @@ window.__ModuleLoader__.load({
38
38
  would escape the mount and 404 behind any strict proxy. */
39
39
  const API_BASE = 'dsh-ide-git/api'
40
40
  const TAB_ID = 'dsh-ide-git:panel'
41
+ /* The bundle's package name: the key the Plugins page uses for a bundle's own
42
+ page and its `plugins.bundle.config` slot. */
43
+ const SETTINGS_BUNDLE = 'dsh-ide-git'
41
44
  /* The namespace this plugin's dictionaries are published under in the DSH
42
45
  locale registry. */
43
46
  const LOCALE_NS = 'dsh-ide-git'
@@ -155,6 +158,28 @@ window.__ModuleLoader__.load({
155
158
  'toolbar.tree': '分支栏',
156
159
  'rail.more': '更多操作',
157
160
  'rail.settings': '动作条设置',
161
+ 'rail.openSettings': '打开插件设置',
162
+ 'settings.rail.title': '动作条按钮',
163
+ 'settings.rail.hint': '勾选要显示在动作条上的动作;顺序固定为默认顺序,取消勾选即隐藏。',
164
+ 'settings.rail.unavailable': '当前宿主没有设置服务,请用面板内的「动作条设置」。',
165
+ 'settings.commit.title': 'AI 提交信息',
166
+ 'settings.commit.model': '使用的模型',
167
+ 'settings.commit.modelHint': '留空 = 跟随当前会话(provider/model)',
168
+ 'settings.commit.reasoning': '思考强度',
169
+ 'settings.commit.reasoningHint': '留空 = 模型默认',
170
+ 'settings.commit.prompt': '补充提示词',
171
+ 'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超过 72 字符',
172
+ 'settings.commit.tooLong': '超过 {n} 个字符的部分不会保存',
173
+ 'changes.aiWrite': 'AI 写提交信息',
174
+ 'changes.aiBusy': '正在生成…',
175
+ 'changes.aiQuota': '会消耗你的额度',
176
+ 'changes.aiOverwriteTitle': '覆盖当前草稿?',
177
+ 'changes.aiOverwriteText': 'AI 生成的内容会替换你已经输入的提交信息(可以撤销)。',
178
+ 'changes.aiUndo': '撤销,恢复我的草稿',
179
+ 'changes.aiFailed': '生成失败:',
180
+ 'changes.aiNotice': '部分变更没有发给模型(已截断或丢弃),信息可能不完整。',
181
+ 'settings.rail.writeFailed': '设置没写进去,已退回原状态;请重试。',
182
+ 'settings.rail.readonly': '当前宿主把配置设为只读,开关不可修改。',
158
183
  'rail.settingsHint': '拖动或用箭头调整顺序,眼睛图标控制显示',
159
184
  'rail.reset': '恢复默认',
160
185
  'rail.up': '上移',
@@ -316,6 +341,28 @@ window.__ModuleLoader__.load({
316
341
  'toolbar.tree': 'Branch pane',
317
342
  'rail.more': 'More actions',
318
343
  'rail.settings': 'Action bar settings',
344
+ 'rail.openSettings': 'Open plugin settings',
345
+ 'settings.rail.title': 'Action rail buttons',
346
+ 'settings.rail.hint': 'Tick the actions shown on the rail. The order is fixed; unticking one hides it.',
347
+ 'settings.rail.unavailable': 'This host has no settings service; use the in-panel “Action rail settings”.',
348
+ 'settings.commit.title': 'AI commit message',
349
+ 'settings.commit.model': 'Model',
350
+ 'settings.commit.modelHint': 'Empty = follow this session (provider/model)',
351
+ 'settings.commit.reasoning': 'Reasoning effort',
352
+ 'settings.commit.reasoningHint': 'Empty = the model\'s default',
353
+ 'settings.commit.prompt': 'Extra instructions',
354
+ 'settings.commit.promptHint': 'e.g. Conventional Commits, Chinese subject, at most 72 characters',
355
+ 'settings.commit.tooLong': 'Only the first {n} characters are saved',
356
+ 'changes.aiWrite': 'Write with AI',
357
+ 'changes.aiBusy': 'Writing…',
358
+ 'changes.aiQuota': 'Uses your own quota',
359
+ 'changes.aiOverwriteTitle': 'Replace the current draft?',
360
+ 'changes.aiOverwriteText': 'The AI text will replace the commit message you typed (you can undo it).',
361
+ 'changes.aiUndo': 'Undo, bring my draft back',
362
+ 'changes.aiFailed': 'Could not write it:',
363
+ 'changes.aiNotice': 'Some changes were not sent to the model (truncated or dropped); the message may be incomplete.',
364
+ 'settings.rail.writeFailed': 'The setting did not save; the switch went back. Try again.',
365
+ 'settings.rail.readonly': 'This host serves the configuration read-only.',
319
366
  'rail.settingsHint': 'Drag or use the arrows to reorder; the eye toggles visibility',
320
367
  'rail.reset': 'Restore defaults',
321
368
  'rail.up': 'Move up',
@@ -485,6 +532,28 @@ window.__ModuleLoader__.load({
485
532
  'toolbar.tree': '分支面板',
486
533
  'rail.more': '更多操作',
487
534
  'rail.settings': '動作列設定',
535
+ 'rail.openSettings': '開啟外掛設定',
536
+ 'settings.rail.title': '動作列按鈕',
537
+ 'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
538
+ 'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
539
+ 'settings.commit.title': 'AI 提交訊息',
540
+ 'settings.commit.model': '使用嘅模型',
541
+ 'settings.commit.modelHint': '留空 = 跟隨當前對話(provider/model)',
542
+ 'settings.commit.reasoning': '思考強度',
543
+ 'settings.commit.reasoningHint': '留空 = 模型預設',
544
+ 'settings.commit.prompt': '補充提示詞',
545
+ 'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,唔超過 72 字',
546
+ 'settings.commit.tooLong': '超過 {n} 個字嘅部分唔會儲存',
547
+ 'changes.aiWrite': 'AI 寫提交訊息',
548
+ 'changes.aiBusy': '生成緊…',
549
+ 'changes.aiQuota': '會消耗你嘅額度',
550
+ 'changes.aiOverwriteTitle': '覆蓋當前草稿?',
551
+ 'changes.aiOverwriteText': 'AI 生成嘅內容會換走你打咗嘅提交訊息(可以撤銷)。',
552
+ 'changes.aiUndo': '撤銷,還原我嘅草稿',
553
+ 'changes.aiFailed': '生成失敗:',
554
+ 'changes.aiNotice': '有部分變更冇發俾模型(已截斷或丟棄),訊息可能唔完整。',
555
+ 'settings.rail.writeFailed': '設定寫唔入,已經退回原狀;請再試。',
556
+ 'settings.rail.readonly': '目前宿主將設定設為唯讀,開關改唔到。',
488
557
  'rail.settingsHint': '拖曳或用箭嘴排序;眼睛圖示切換顯示',
489
558
  'rail.reset': '還原預設',
490
559
  'rail.up': '上移',
@@ -646,6 +715,28 @@ window.__ModuleLoader__.load({
646
715
  'toolbar.tree': '分支面板',
647
716
  'rail.more': '更多操作',
648
717
  'rail.settings': '動作列設定',
718
+ 'rail.openSettings': '開啟外掛設定',
719
+ 'settings.rail.title': '動作列按鈕',
720
+ 'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
721
+ 'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
722
+ 'settings.commit.title': 'AI 提交訊息',
723
+ 'settings.commit.model': '使用的模型',
724
+ 'settings.commit.modelHint': '留空 = 跟隨目前工作階段(provider/model)',
725
+ 'settings.commit.reasoning': '思考強度',
726
+ 'settings.commit.reasoningHint': '留空 = 模型預設',
727
+ 'settings.commit.prompt': '補充提示詞',
728
+ 'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超過 72 字元',
729
+ 'settings.commit.tooLong': '超過 {n} 個字元的部分不會儲存',
730
+ 'changes.aiWrite': 'AI 寫提交訊息',
731
+ 'changes.aiBusy': '正在生成…',
732
+ 'changes.aiQuota': '會消耗你的額度',
733
+ 'changes.aiOverwriteTitle': '覆蓋目前的草稿?',
734
+ 'changes.aiOverwriteText': 'AI 生成的內容會取代你已輸入的提交訊息(可以復原)。',
735
+ 'changes.aiUndo': '復原,還原我的草稿',
736
+ 'changes.aiFailed': '生成失敗:',
737
+ 'changes.aiNotice': '部分變更沒有傳給模型(已截斷或捨棄),訊息可能不完整。',
738
+ 'settings.rail.writeFailed': '設定沒有寫入,已退回原狀態;請重試。',
739
+ 'settings.rail.readonly': '目前宿主將設定設為唯讀,開關無法修改。',
649
740
  'rail.settingsHint': '拖曳或用箭頭排序;眼睛圖示切換顯示',
650
741
  'rail.reset': '還原預設',
651
742
  'rail.up': '上移',
@@ -807,6 +898,28 @@ window.__ModuleLoader__.load({
807
898
  'toolbar.tree': '分支面板',
808
899
  'rail.more': '更多操作',
809
900
  'rail.settings': '動作列設定',
901
+ 'rail.openSettings': '開啟外掛設定',
902
+ 'settings.rail.title': '動作列按鈕',
903
+ 'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
904
+ 'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
905
+ 'settings.commit.title': 'AI 提交訊息',
906
+ 'settings.commit.model': '使用的模型',
907
+ 'settings.commit.modelHint': '留空 = 跟隨目前工作階段(provider/model)',
908
+ 'settings.commit.reasoning': '思考強度',
909
+ 'settings.commit.reasoningHint': '留空 = 模型預設',
910
+ 'settings.commit.prompt': '補充提示詞',
911
+ 'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超過 72 字元',
912
+ 'settings.commit.tooLong': '超過 {n} 個字元的部分不會儲存',
913
+ 'changes.aiWrite': 'AI 寫提交訊息',
914
+ 'changes.aiBusy': '正在生成…',
915
+ 'changes.aiQuota': '會消耗你的額度',
916
+ 'changes.aiOverwriteTitle': '覆蓋目前的草稿?',
917
+ 'changes.aiOverwriteText': 'AI 生成的內容會取代你已輸入的提交訊息(可以復原)。',
918
+ 'changes.aiUndo': '復原,還原我的草稿',
919
+ 'changes.aiFailed': '生成失敗:',
920
+ 'changes.aiNotice': '部分變更沒有傳給模型(已截斷或捨棄),訊息可能不完整。',
921
+ 'settings.rail.writeFailed': '設定沒有寫入,已退回原狀態;請重試。',
922
+ 'settings.rail.readonly': '目前宿主將設定設為唯讀,開關無法修改。',
810
923
  'rail.settingsHint': '拖曳或用箭嘴排序;眼睛圖示切換顯示',
811
924
  'rail.reset': '還原預設',
812
925
  'rail.up': '上移',
@@ -968,6 +1081,28 @@ window.__ModuleLoader__.load({
968
1081
  'toolbar.tree': 'ブランチペイン',
969
1082
  'rail.more': 'その他の操作',
970
1083
  'rail.settings': 'アクションバーの設定',
1084
+ 'rail.openSettings': 'プラグイン設定を開く',
1085
+ 'settings.rail.title': 'アクションレールのボタン',
1086
+ 'settings.rail.hint': 'レールに表示する操作にチェックを入れてください。順序は固定で、チェックを外すと非表示になります。',
1087
+ 'settings.rail.unavailable': 'このホストには設定サービスがないため、パネル内の「アクションレール設定」を使ってください。',
1088
+ 'settings.commit.title': 'AI コミットメッセージ',
1089
+ 'settings.commit.model': '使用するモデル',
1090
+ 'settings.commit.modelHint': '空欄 = 現在のセッションに従う(provider/model)',
1091
+ 'settings.commit.reasoning': '思考の強度',
1092
+ 'settings.commit.reasoningHint': '空欄 = モデルの既定値',
1093
+ 'settings.commit.prompt': '追加の指示',
1094
+ 'settings.commit.promptHint': '例:Conventional Commits、subject は日本語、72 文字以内',
1095
+ 'settings.commit.tooLong': '{n} 文字を超える部分は保存されません',
1096
+ 'changes.aiWrite': 'AI で書く',
1097
+ 'changes.aiBusy': '生成中…',
1098
+ 'changes.aiQuota': 'あなたの利用枠を消費します',
1099
+ 'changes.aiOverwriteTitle': '現在の下書きを置き換えますか?',
1100
+ 'changes.aiOverwriteText': 'AI の文章が入力済みのコミットメッセージを置き換えます(元に戻せます)。',
1101
+ 'changes.aiUndo': '元に戻す',
1102
+ 'changes.aiFailed': '生成できませんでした:',
1103
+ 'changes.aiNotice': '一部の変更はモデルに送られていません(切り詰めまたは除外)。内容が不完全な可能性があります。',
1104
+ 'settings.rail.writeFailed': '設定を保存できませんでした。元の状態に戻しました。もう一度お試しください。',
1105
+ 'settings.rail.readonly': 'このホストは設定を読み取り専用で提供しています。',
971
1106
  'rail.settingsHint': 'ドラッグまたは矢印で並べ替え、目のアイコンで表示を切り替え',
972
1107
  'rail.reset': '既定に戻す',
973
1108
  'rail.up': '上へ',
@@ -1129,6 +1264,28 @@ window.__ModuleLoader__.load({
1129
1264
  'toolbar.tree': '브랜치 창',
1130
1265
  'rail.more': '추가 작업',
1131
1266
  'rail.settings': '작업 표시줄 설정',
1267
+ 'rail.openSettings': '플러그인 설정 열기',
1268
+ 'settings.rail.title': '액션 레일 버튼',
1269
+ 'settings.rail.hint': '레일에 표시할 동작을 선택하세요. 순서는 고정이며 선택을 해제하면 숨겨집니다.',
1270
+ 'settings.rail.unavailable': '이 호스트에는 설정 서비스가 없으므로 패널의 "액션 레일 설정"을 사용하세요.',
1271
+ 'settings.commit.title': 'AI 커밋 메시지',
1272
+ 'settings.commit.model': '사용할 모델',
1273
+ 'settings.commit.modelHint': '비우면 현재 세션을 따릅니다(provider/model)',
1274
+ 'settings.commit.reasoning': '추론 강도',
1275
+ 'settings.commit.reasoningHint': '비우면 모델 기본값',
1276
+ 'settings.commit.prompt': '추가 지시',
1277
+ 'settings.commit.promptHint': '예: Conventional Commits, 제목은 한국어, 72자 이내',
1278
+ 'settings.commit.tooLong': '{n}자를 넘는 부분은 저장되지 않습니다',
1279
+ 'changes.aiWrite': 'AI로 작성',
1280
+ 'changes.aiBusy': '생성 중…',
1281
+ 'changes.aiQuota': '사용자의 할당량을 소모합니다',
1282
+ 'changes.aiOverwriteTitle': '현재 초안을 바꿀까요?',
1283
+ 'changes.aiOverwriteText': 'AI 텍스트가 입력한 커밋 메시지를 대체합니다(되돌릴 수 있습니다).',
1284
+ 'changes.aiUndo': '되돌리기',
1285
+ 'changes.aiFailed': '생성하지 못했습니다:',
1286
+ 'changes.aiNotice': '일부 변경이 모델에 전달되지 않았습니다(잘림 또는 제외). 내용이 불완전할 수 있습니다.',
1287
+ 'settings.rail.writeFailed': '설정이 저장되지 않아 원래 상태로 되돌렸습니다. 다시 시도하세요.',
1288
+ 'settings.rail.readonly': '이 호스트는 설정을 읽기 전용으로 제공합니다.',
1132
1289
  'rail.settingsHint': '끌어서 또는 화살표로 순서를 바꾸고, 눈 아이콘으로 표시를 전환합니다',
1133
1290
  'rail.reset': '기본값 복원',
1134
1291
  'rail.up': '위로',
@@ -1290,6 +1447,28 @@ window.__ModuleLoader__.load({
1290
1447
  'toolbar.tree': 'Branch-Bereich',
1291
1448
  'rail.more': 'Weitere Aktionen',
1292
1449
  'rail.settings': 'Einstellungen der Aktionsleiste',
1450
+ 'rail.openSettings': 'Plugin-Einstellungen öffnen',
1451
+ 'settings.rail.title': 'Schaltflächen der Aktionsleiste',
1452
+ 'settings.rail.hint': 'Die auf der Leiste sichtbaren Aktionen ankreuzen. Die Reihenfolge ist fest; abwählen blendet eine aus.',
1453
+ 'settings.rail.unavailable': 'Dieser Host hat keinen Einstellungsdienst; nutze die „Aktionsleisten-Einstellungen“ im Panel.',
1454
+ 'settings.commit.title': 'KI-Commit-Nachricht',
1455
+ 'settings.commit.model': 'Modell',
1456
+ 'settings.commit.modelHint': 'Leer = dieser Sitzung folgen (provider/model)',
1457
+ 'settings.commit.reasoning': 'Denkaufwand',
1458
+ 'settings.commit.reasoningHint': 'Leer = Standard des Modells',
1459
+ 'settings.commit.prompt': 'Zusätzliche Anweisungen',
1460
+ 'settings.commit.promptHint': 'z. B. Conventional Commits, Betreff auf Deutsch, höchstens 72 Zeichen',
1461
+ 'settings.commit.tooLong': 'Nur die ersten {n} Zeichen werden gespeichert',
1462
+ 'changes.aiWrite': 'Mit KI schreiben',
1463
+ 'changes.aiBusy': 'Wird geschrieben…',
1464
+ 'changes.aiQuota': 'Verbraucht dein eigenes Kontingent',
1465
+ 'changes.aiOverwriteTitle': 'Aktuellen Entwurf ersetzen?',
1466
+ 'changes.aiOverwriteText': 'Der KI-Text ersetzt deine eingegebene Commit-Nachricht (rückgängig möglich).',
1467
+ 'changes.aiUndo': 'Rückgängig',
1468
+ 'changes.aiFailed': 'Konnte nicht schreiben:',
1469
+ 'changes.aiNotice': 'Einige Änderungen wurden nicht ans Modell gesendet (gekürzt oder verworfen); die Nachricht kann unvollständig sein.',
1470
+ 'settings.rail.writeFailed': 'Die Einstellung wurde nicht gespeichert; der Schalter ist zurückgesprungen. Bitte erneut versuchen.',
1471
+ 'settings.rail.readonly': 'Dieser Host liefert die Konfiguration schreibgeschützt.',
1293
1472
  'rail.settingsHint': 'Ziehen oder Pfeile zum Sortieren, das Auge schaltet die Sichtbarkeit um',
1294
1473
  'rail.reset': 'Standard wiederherstellen',
1295
1474
  'rail.up': 'Nach oben',
@@ -1451,6 +1630,28 @@ window.__ModuleLoader__.load({
1451
1630
  'toolbar.tree': 'Volet des branches',
1452
1631
  'rail.more': 'Plus d\'actions',
1453
1632
  'rail.settings': 'Réglages de la barre d\'actions',
1633
+ 'rail.openSettings': 'Ouvrir les réglages du plugin',
1634
+ 'settings.rail.title': 'Boutons de la barre d’actions',
1635
+ 'settings.rail.hint': 'Cochez les actions affichées sur la barre. L’ordre est fixe ; décocher masque une action.',
1636
+ '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.',
1637
+ 'settings.commit.title': 'Message de commit par IA',
1638
+ 'settings.commit.model': 'Modèle',
1639
+ 'settings.commit.modelHint': 'Vide = suivre cette session (provider/model)',
1640
+ 'settings.commit.reasoning': 'Effort de raisonnement',
1641
+ 'settings.commit.reasoningHint': 'Vide = valeur par défaut du modèle',
1642
+ 'settings.commit.prompt': 'Instructions supplémentaires',
1643
+ 'settings.commit.promptHint': 'ex. Conventional Commits, sujet en français, 72 caractères maximum',
1644
+ 'settings.commit.tooLong': 'Seuls les {n} premiers caractères sont enregistrés',
1645
+ 'changes.aiWrite': 'Écrire avec l’IA',
1646
+ 'changes.aiBusy': 'Rédaction…',
1647
+ 'changes.aiQuota': 'Consomme votre propre quota',
1648
+ 'changes.aiOverwriteTitle': 'Remplacer le brouillon actuel ?',
1649
+ 'changes.aiOverwriteText': 'Le texte de l’IA remplacera le message saisi (annulable).',
1650
+ 'changes.aiUndo': 'Annuler, reprendre mon brouillon',
1651
+ 'changes.aiFailed': 'Échec de la rédaction :',
1652
+ 'changes.aiNotice': 'Certaines modifications n’ont pas été envoyées au modèle (tronquées ou écartées) ; le message peut être incomplet.',
1653
+ 'settings.rail.writeFailed': 'Le réglage n’a pas été enregistré ; l’interrupteur est revenu en place. Réessayez.',
1654
+ 'settings.rail.readonly': 'Cet hôte fournit la configuration en lecture seule.',
1454
1655
  'rail.settingsHint': 'Glisser ou utiliser les flèches pour réordonner ; l\'œil bascule la visibilité',
1455
1656
  'rail.reset': 'Rétablir les valeurs par défaut',
1456
1657
  'rail.up': 'Monter',
@@ -1612,6 +1813,28 @@ window.__ModuleLoader__.load({
1612
1813
  'toolbar.tree': 'Панель веток',
1613
1814
  'rail.more': 'Другие действия',
1614
1815
  'rail.settings': 'Настройки панели действий',
1816
+ 'rail.openSettings': 'Открыть настройки плагина',
1817
+ 'settings.rail.title': 'Кнопки панели действий',
1818
+ 'settings.rail.hint': 'Отметьте действия, показываемые на панели. Порядок фиксирован; снятие флажка скрывает действие.',
1819
+ 'settings.rail.unavailable': 'На этом хосте нет службы настроек; используйте «Настройки панели действий» в панели.',
1820
+ 'settings.commit.title': 'Сообщение коммита от ИИ',
1821
+ 'settings.commit.model': 'Модель',
1822
+ 'settings.commit.modelHint': 'Пусто = как в этой сессии (provider/model)',
1823
+ 'settings.commit.reasoning': 'Усилие рассуждения',
1824
+ 'settings.commit.reasoningHint': 'Пусто = по умолчанию у модели',
1825
+ 'settings.commit.prompt': 'Дополнительные указания',
1826
+ 'settings.commit.promptHint': 'напр. Conventional Commits, тема по-русски, до 72 символов',
1827
+ 'settings.commit.tooLong': 'Сохраняются только первые {n} символов',
1828
+ 'changes.aiWrite': 'Написать с ИИ',
1829
+ 'changes.aiBusy': 'Генерация…',
1830
+ 'changes.aiQuota': 'Расходует вашу квоту',
1831
+ 'changes.aiOverwriteTitle': 'Заменить текущий черновик?',
1832
+ 'changes.aiOverwriteText': 'Текст ИИ заменит введённое сообщение коммита (можно отменить).',
1833
+ 'changes.aiUndo': 'Отменить',
1834
+ 'changes.aiFailed': 'Не удалось сгенерировать:',
1835
+ 'changes.aiNotice': 'Часть изменений не отправлена модели (обрезана или отброшена); сообщение может быть неполным.',
1836
+ 'settings.rail.writeFailed': 'Настройка не сохранилась; переключатель вернулся назад. Попробуйте снова.',
1837
+ 'settings.rail.readonly': 'Этот хост отдаёт конфигурацию только для чтения.',
1615
1838
  'rail.settingsHint': 'Перетаскивайте или используйте стрелки для порядка; значок глаза переключает видимость',
1616
1839
  'rail.reset': 'Вернуть значения по умолчанию',
1617
1840
  'rail.up': 'Вверх',
@@ -1773,6 +1996,28 @@ window.__ModuleLoader__.load({
1773
1996
  'toolbar.tree': 'Painel de branches',
1774
1997
  'rail.more': 'Mais ações',
1775
1998
  'rail.settings': 'Configurações da barra de ações',
1999
+ 'rail.openSettings': 'Abrir configurações do plugin',
2000
+ 'settings.rail.title': 'Botões da barra de ações',
2001
+ 'settings.rail.hint': 'Marque as ações exibidas na barra. A ordem é fixa; desmarcar oculta uma ação.',
2002
+ 'settings.rail.unavailable': 'Este host não tem serviço de configurações; use «Configurações da barra de ações» no painel.',
2003
+ 'settings.commit.title': 'Mensagem de commit por IA',
2004
+ 'settings.commit.model': 'Modelo',
2005
+ 'settings.commit.modelHint': 'Vazio = seguir esta sessão (provider/model)',
2006
+ 'settings.commit.reasoning': 'Esforço de raciocínio',
2007
+ 'settings.commit.reasoningHint': 'Vazio = padrão do modelo',
2008
+ 'settings.commit.prompt': 'Instruções extras',
2009
+ 'settings.commit.promptHint': 'ex.: Conventional Commits, assunto em português, até 72 caracteres',
2010
+ 'settings.commit.tooLong': 'Só os primeiros {n} caracteres são salvos',
2011
+ 'changes.aiWrite': 'Escrever com IA',
2012
+ 'changes.aiBusy': 'Gerando…',
2013
+ 'changes.aiQuota': 'Consome a sua cota',
2014
+ 'changes.aiOverwriteTitle': 'Substituir o rascunho atual?',
2015
+ 'changes.aiOverwriteText': 'O texto da IA substituirá a mensagem que você digitou (dá para desfazer).',
2016
+ 'changes.aiUndo': 'Desfazer',
2017
+ 'changes.aiFailed': 'Não foi possível gerar:',
2018
+ 'changes.aiNotice': 'Algumas alterações não foram enviadas ao modelo (truncadas ou descartadas); a mensagem pode ficar incompleta.',
2019
+ 'settings.rail.writeFailed': 'A configuração não foi salva; o interruptor voltou ao lugar. Tente de novo.',
2020
+ 'settings.rail.readonly': 'Este host fornece a configuração somente para leitura.',
1776
2021
  'rail.settingsHint': 'Arraste ou use as setas para reordenar; o olho alterna a visibilidade',
1777
2022
  'rail.reset': 'Restaurar padrões',
1778
2023
  'rail.up': 'Mover para cima',
@@ -1934,6 +2179,28 @@ window.__ModuleLoader__.load({
1934
2179
  'toolbar.tree': 'Riquadro dei branch',
1935
2180
  'rail.more': 'Altre azioni',
1936
2181
  'rail.settings': 'Impostazioni della barra delle azioni',
2182
+ 'rail.openSettings': 'Apri le impostazioni del plugin',
2183
+ 'settings.rail.title': 'Pulsanti della barra azioni',
2184
+ 'settings.rail.hint': 'Seleziona le azioni mostrate sulla barra. L’ordine è fisso; deselezionando se ne nasconde una.',
2185
+ 'settings.rail.unavailable': 'Questo host non ha un servizio di impostazioni; usa «Impostazioni barra azioni» nel pannello.',
2186
+ 'settings.commit.title': 'Messaggio di commit con IA',
2187
+ 'settings.commit.model': 'Modello',
2188
+ 'settings.commit.modelHint': 'Vuoto = segui questa sessione (provider/model)',
2189
+ 'settings.commit.reasoning': 'Sforzo di ragionamento',
2190
+ 'settings.commit.reasoningHint': 'Vuoto = predefinito del modello',
2191
+ 'settings.commit.prompt': 'Istruzioni aggiuntive',
2192
+ 'settings.commit.promptHint': 'es. Conventional Commits, oggetto in italiano, max 72 caratteri',
2193
+ 'settings.commit.tooLong': 'Solo i primi {n} caratteri vengono salvati',
2194
+ 'changes.aiWrite': 'Scrivi con l’IA',
2195
+ 'changes.aiBusy': 'Generazione…',
2196
+ 'changes.aiQuota': 'Consuma la tua quota',
2197
+ 'changes.aiOverwriteTitle': 'Sostituire la bozza attuale?',
2198
+ 'changes.aiOverwriteText': 'Il testo dell’IA sostituirà il messaggio digitato (puoi annullare).',
2199
+ 'changes.aiUndo': 'Annulla',
2200
+ 'changes.aiFailed': 'Scrittura non riuscita:',
2201
+ 'changes.aiNotice': 'Alcune modifiche non sono state inviate al modello (troncate o scartate); il messaggio può essere incompleto.',
2202
+ 'settings.rail.writeFailed': 'L’impostazione non è stata salvata; l’interruttore è tornato indietro. Riprova.',
2203
+ 'settings.rail.readonly': 'Questo host fornisce la configurazione in sola lettura.',
1937
2204
  'rail.settingsHint': 'Trascina o usa le frecce per riordinare; l’occhio attiva o disattiva la visibilità',
1938
2205
  'rail.reset': 'Ripristina i valori predefiniti',
1939
2206
  'rail.up': 'Sposta su',
@@ -2095,6 +2362,28 @@ window.__ModuleLoader__.load({
2095
2362
  'toolbar.tree': 'Branchpaneel',
2096
2363
  'rail.more': 'Meer acties',
2097
2364
  'rail.settings': 'Instellingen van de actiebalk',
2365
+ 'rail.openSettings': 'Plugin-instellingen openen',
2366
+ 'settings.rail.title': 'Knoppen van de actiebalk',
2367
+ 'settings.rail.hint': 'Vink de acties aan die op de balk verschijnen. De volgorde ligt vast; uitvinken verbergt er een.',
2368
+ 'settings.rail.unavailable': 'Deze host heeft geen instellingenservice; gebruik "Actiebalk-instellingen" in het paneel.',
2369
+ 'settings.commit.title': 'AI-commitbericht',
2370
+ 'settings.commit.model': 'Model',
2371
+ 'settings.commit.modelHint': 'Leeg = deze sessie volgen (provider/model)',
2372
+ 'settings.commit.reasoning': 'Redeneerinspanning',
2373
+ 'settings.commit.reasoningHint': 'Leeg = standaard van het model',
2374
+ 'settings.commit.prompt': 'Extra instructies',
2375
+ 'settings.commit.promptHint': 'bv. Conventional Commits, onderwerp in het Nederlands, max. 72 tekens',
2376
+ 'settings.commit.tooLong': 'Alleen de eerste {n} tekens worden opgeslagen',
2377
+ 'changes.aiWrite': 'Met AI schrijven',
2378
+ 'changes.aiBusy': 'Bezig…',
2379
+ 'changes.aiQuota': 'Verbruikt je eigen quota',
2380
+ 'changes.aiOverwriteTitle': 'Huidig concept vervangen?',
2381
+ 'changes.aiOverwriteText': 'De AI-tekst vervangt je getypte commitbericht (ongedaan te maken).',
2382
+ 'changes.aiUndo': 'Ongedaan maken',
2383
+ 'changes.aiFailed': 'Kon niet schrijven:',
2384
+ 'changes.aiNotice': 'Sommige wijzigingen zijn niet naar het model gestuurd (afgekapt of overgeslagen); het bericht kan onvolledig zijn.',
2385
+ 'settings.rail.writeFailed': 'De instelling is niet opgeslagen; de schakelaar sprong terug. Probeer opnieuw.',
2386
+ 'settings.rail.readonly': 'Deze host levert de configuratie alleen-lezen.',
2098
2387
  'rail.settingsHint': 'Sleep of gebruik de pijlen om te sorteren; het oog schakelt de zichtbaarheid',
2099
2388
  'rail.reset': 'Standaardwaarden herstellen',
2100
2389
  'rail.up': 'Omhoog',
@@ -2256,6 +2545,28 @@ window.__ModuleLoader__.load({
2256
2545
  'toolbar.tree': 'Panel gałęzi',
2257
2546
  'rail.more': 'Więcej działań',
2258
2547
  'rail.settings': 'Ustawienia paska działań',
2548
+ 'rail.openSettings': 'Otwórz ustawienia wtyczki',
2549
+ 'settings.rail.title': 'Przyciski paska akcji',
2550
+ 'settings.rail.hint': 'Zaznacz akcje widoczne na pasku. Kolejność jest stała; odznaczenie ukrywa akcję.',
2551
+ 'settings.rail.unavailable': 'Ten host nie ma usługi ustawień; użyj „Ustawienia paska akcji” w panelu.',
2552
+ 'settings.commit.title': 'Wiadomość commita od AI',
2553
+ 'settings.commit.model': 'Model',
2554
+ 'settings.commit.modelHint': 'Puste = zgodnie z tą sesją (provider/model)',
2555
+ 'settings.commit.reasoning': 'Poziom rozumowania',
2556
+ 'settings.commit.reasoningHint': 'Puste = domyślne modelu',
2557
+ 'settings.commit.prompt': 'Dodatkowe instrukcje',
2558
+ 'settings.commit.promptHint': 'np. Conventional Commits, temat po polsku, maks. 72 znaki',
2559
+ 'settings.commit.tooLong': 'Zapisywane jest tylko pierwszych {n} znaków',
2560
+ 'changes.aiWrite': 'Napisz z AI',
2561
+ 'changes.aiBusy': 'Generowanie…',
2562
+ 'changes.aiQuota': 'Zużywa twój limit',
2563
+ 'changes.aiOverwriteTitle': 'Zastąpić bieżący szkic?',
2564
+ 'changes.aiOverwriteText': 'Tekst AI zastąpi wpisaną wiadomość commita (można cofnąć).',
2565
+ 'changes.aiUndo': 'Cofnij',
2566
+ 'changes.aiFailed': 'Nie udało się wygenerować:',
2567
+ 'changes.aiNotice': 'Część zmian nie trafiła do modelu (przycięta lub pominięta); wiadomość może być niepełna.',
2568
+ 'settings.rail.writeFailed': 'Ustawienie nie zostało zapisane; przełącznik wrócił na miejsce. Spróbuj ponownie.',
2569
+ 'settings.rail.readonly': 'Ten host udostępnia konfigurację tylko do odczytu.',
2259
2570
  'rail.settingsHint': 'Przeciągnij lub użyj strzałek, aby zmienić kolejność; oko przełącza widoczność',
2260
2571
  'rail.reset': 'Przywróć domyślne',
2261
2572
  'rail.up': 'W górę',
@@ -2417,6 +2728,28 @@ window.__ModuleLoader__.load({
2417
2728
  'toolbar.tree': 'Grenpanel',
2418
2729
  'rail.more': 'Fler åtgärder',
2419
2730
  'rail.settings': 'Inställningar för åtgärdsfältet',
2731
+ 'rail.openSettings': 'Öppna plugininställningar',
2732
+ 'settings.rail.title': 'Knappar i åtgärdsfältet',
2733
+ 'settings.rail.hint': 'Markera de åtgärder som visas i fältet. Ordningen är fast; avmarkering döljer en.',
2734
+ 'settings.rail.unavailable': 'Denna värd saknar inställningstjänst; använd ”Åtgärdsfältets inställningar” i panelen.',
2735
+ 'settings.commit.title': 'AI-commitmeddelande',
2736
+ 'settings.commit.model': 'Modell',
2737
+ 'settings.commit.modelHint': 'Tomt = följ denna session (provider/model)',
2738
+ 'settings.commit.reasoning': 'Resonemangsinsats',
2739
+ 'settings.commit.reasoningHint': 'Tomt = modellens standard',
2740
+ 'settings.commit.prompt': 'Extra instruktioner',
2741
+ 'settings.commit.promptHint': 't.ex. Conventional Commits, ämne på svenska, högst 72 tecken',
2742
+ 'settings.commit.tooLong': 'Endast de första {n} tecknen sparas',
2743
+ 'changes.aiWrite': 'Skriv med AI',
2744
+ 'changes.aiBusy': 'Skriver…',
2745
+ 'changes.aiQuota': 'Använder din egen kvot',
2746
+ 'changes.aiOverwriteTitle': 'Ersätt nuvarande utkast?',
2747
+ 'changes.aiOverwriteText': 'AI-texten ersätter commitmeddelandet du skrev (går att ångra).',
2748
+ 'changes.aiUndo': 'Ångra',
2749
+ 'changes.aiFailed': 'Kunde inte skriva:',
2750
+ 'changes.aiNotice': 'Vissa ändringar skickades inte till modellen (kapade eller utelämnade); meddelandet kan vara ofullständigt.',
2751
+ 'settings.rail.writeFailed': 'Inställningen sparades inte; reglaget gick tillbaka. Försök igen.',
2752
+ 'settings.rail.readonly': 'Denna värd tillhandahåller konfigurationen skrivskyddad.',
2420
2753
  'rail.settingsHint': 'Dra eller använd pilarna för att sortera; ögat växlar synlighet',
2421
2754
  'rail.reset': 'Återställ standard',
2422
2755
  'rail.up': 'Flytta upp',
@@ -2578,6 +2911,28 @@ window.__ModuleLoader__.load({
2578
2911
  'toolbar.tree': 'Dal paneli',
2579
2912
  'rail.more': 'Diğer eylemler',
2580
2913
  'rail.settings': 'Eylem çubuğu ayarları',
2914
+ 'rail.openSettings': 'Eklenti ayarlarını aç',
2915
+ 'settings.rail.title': 'Eylem çubuğu düğmeleri',
2916
+ 'settings.rail.hint': 'Çubukta gösterilecek eylemleri işaretleyin. Sıra sabittir; işareti kaldırmak eylemi gizler.',
2917
+ 'settings.rail.unavailable': 'Bu ana bilgisayarda ayar hizmeti yok; paneldeki "Eylem çubuğu ayarları"nı kullanın.',
2918
+ 'settings.commit.title': 'AI commit mesajı',
2919
+ 'settings.commit.model': 'Model',
2920
+ 'settings.commit.modelHint': 'Boş = bu oturumu izle (provider/model)',
2921
+ 'settings.commit.reasoning': 'Düşünme yoğunluğu',
2922
+ 'settings.commit.reasoningHint': 'Boş = modelin varsayılanı',
2923
+ 'settings.commit.prompt': 'Ek yönergeler',
2924
+ 'settings.commit.promptHint': 'örn. Conventional Commits, konu Türkçe, en fazla 72 karakter',
2925
+ 'settings.commit.tooLong': 'Yalnızca ilk {n} karakter kaydedilir',
2926
+ 'changes.aiWrite': 'AI ile yaz',
2927
+ 'changes.aiBusy': 'Yazılıyor…',
2928
+ 'changes.aiQuota': 'Kendi kotanızı harcar',
2929
+ 'changes.aiOverwriteTitle': 'Geçerli taslağın yerine geçilsin mi?',
2930
+ 'changes.aiOverwriteText': 'AI metni yazdığınız commit mesajının yerine geçer (geri alınabilir).',
2931
+ 'changes.aiUndo': 'Geri al',
2932
+ 'changes.aiFailed': 'Yazılamadı:',
2933
+ 'changes.aiNotice': 'Bazı değişiklikler modele gönderilmedi (kısaltıldı veya atlandı); mesaj eksik olabilir.',
2934
+ 'settings.rail.writeFailed': 'Ayar kaydedilemedi; anahtar eski konumuna döndü. Yeniden deneyin.',
2935
+ 'settings.rail.readonly': 'Bu ana bilgisayar yapılandırmayı salt okunur sunuyor.',
2581
2936
  'rail.settingsHint': 'Sıralamak için sürükleyin veya okları kullanın; göz simgesi görünürlüğü değiştirir',
2582
2937
  'rail.reset': 'Varsayılanlara dön',
2583
2938
  'rail.up': 'Yukarı taşı',
@@ -2739,6 +3094,28 @@ window.__ModuleLoader__.load({
2739
3094
  'toolbar.tree': 'Panel branch',
2740
3095
  'rail.more': 'Tindakan lain',
2741
3096
  'rail.settings': 'Pengaturan bilah tindakan',
3097
+ 'rail.openSettings': 'Buka pengaturan plugin',
3098
+ 'settings.rail.title': 'Tombol bilah aksi',
3099
+ 'settings.rail.hint': 'Centang aksi yang ditampilkan di bilah. Urutannya tetap; menghapus centang menyembunyikannya.',
3100
+ 'settings.rail.unavailable': 'Host ini tidak punya layanan pengaturan; gunakan "Pengaturan bilah aksi" di panel.',
3101
+ 'settings.commit.title': 'Pesan commit AI',
3102
+ 'settings.commit.model': 'Model',
3103
+ 'settings.commit.modelHint': 'Kosong = ikuti sesi ini (provider/model)',
3104
+ 'settings.commit.reasoning': 'Intensitas penalaran',
3105
+ 'settings.commit.reasoningHint': 'Kosong = bawaan model',
3106
+ 'settings.commit.prompt': 'Instruksi tambahan',
3107
+ 'settings.commit.promptHint': 'mis. Conventional Commits, subjek bahasa Indonesia, maks 72 karakter',
3108
+ 'settings.commit.tooLong': 'Hanya {n} karakter pertama yang disimpan',
3109
+ 'changes.aiWrite': 'Tulis dengan AI',
3110
+ 'changes.aiBusy': 'Menulis…',
3111
+ 'changes.aiQuota': 'Memakai kuota Anda',
3112
+ 'changes.aiOverwriteTitle': 'Ganti draf saat ini?',
3113
+ 'changes.aiOverwriteText': 'Teks AI akan menggantikan pesan commit yang Anda tulis (bisa dibatalkan).',
3114
+ 'changes.aiUndo': 'Batalkan',
3115
+ 'changes.aiFailed': 'Gagal menulis:',
3116
+ 'changes.aiNotice': 'Sebagian perubahan tidak dikirim ke model (dipotong atau dibuang); pesan bisa tidak lengkap.',
3117
+ 'settings.rail.writeFailed': 'Pengaturan tidak tersimpan; tombol kembali ke posisi semula. Coba lagi.',
3118
+ 'settings.rail.readonly': 'Host ini menyajikan konfigurasi hanya-baca.',
2742
3119
  'rail.settingsHint': 'Seret atau pakai panah untuk mengurutkan; ikon mata mengalihkan visibilitas',
2743
3120
  'rail.reset': 'Kembalikan default',
2744
3121
  'rail.up': 'Naikkan',
@@ -2900,6 +3277,28 @@ window.__ModuleLoader__.load({
2900
3277
  'toolbar.tree': 'Bảng nhánh',
2901
3278
  'rail.more': 'Thao tác khác',
2902
3279
  'rail.settings': 'Cài đặt thanh thao tác',
3280
+ 'rail.openSettings': 'Mở cài đặt plugin',
3281
+ 'settings.rail.title': 'Nút thanh tác vụ',
3282
+ '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ụ đó.',
3283
+ '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.',
3284
+ 'settings.commit.title': 'Thông điệp commit do AI viết',
3285
+ 'settings.commit.model': 'Mô hình',
3286
+ 'settings.commit.modelHint': 'Trống = theo phiên này (provider/model)',
3287
+ 'settings.commit.reasoning': 'Mức suy luận',
3288
+ 'settings.commit.reasoningHint': 'Trống = mặc định của mô hình',
3289
+ 'settings.commit.prompt': 'Chỉ dẫn bổ sung',
3290
+ 'settings.commit.promptHint': 'ví dụ Conventional Commits, tiêu đề tiếng Việt, tối đa 72 ký tự',
3291
+ 'settings.commit.tooLong': 'Chỉ {n} ký tự đầu được lưu',
3292
+ 'changes.aiWrite': 'Viết bằng AI',
3293
+ 'changes.aiBusy': 'Đang viết…',
3294
+ 'changes.aiQuota': 'Dùng hạn mức của bạn',
3295
+ 'changes.aiOverwriteTitle': 'Thay bản nháp hiện tại?',
3296
+ 'changes.aiOverwriteText': 'Nội dung AI sẽ thay thế thông điệp commit bạn đã nhập (có thể hoàn tác).',
3297
+ 'changes.aiUndo': 'Hoàn tác',
3298
+ 'changes.aiFailed': 'Không viết được:',
3299
+ 'changes.aiNotice': 'Một số thay đổi chưa gửi tới mô hình (bị cắt hoặc bỏ); thông điệp có thể chưa đầy đủ.',
3300
+ 'settings.rail.writeFailed': 'Cài đặt không lưu được; công tắc đã trở về trạng thái cũ. Hãy thử lại.',
3301
+ 'settings.rail.readonly': 'Máy chủ này chỉ cung cấp cấu hình ở chế độ chỉ đọc.',
2903
3302
  'rail.settingsHint': 'Kéo hoặc dùng mũi tên để sắp xếp; biểu tượng con mắt bật tắt hiển thị',
2904
3303
  'rail.reset': 'Khôi phục mặc định',
2905
3304
  'rail.up': 'Di chuyển lên',
@@ -3061,6 +3460,28 @@ window.__ModuleLoader__.load({
3061
3460
  'toolbar.tree': 'لوحة الفروع',
3062
3461
  'rail.more': 'مزيد من الإجراءات',
3063
3462
  'rail.settings': 'إعدادات شريط الإجراءات',
3463
+ 'rail.openSettings': 'فتح إعدادات الإضافة',
3464
+ 'settings.rail.title': 'أزرار شريط الإجراءات',
3465
+ 'settings.rail.hint': 'حدّد الإجراءات المعروضة على الشريط. الترتيب ثابت، وإلغاء التحديد يخفي الإجراء.',
3466
+ 'settings.rail.unavailable': 'لا تتوفر خدمة إعدادات على هذا المضيف؛ استخدم «إعدادات شريط الإجراءات» في اللوحة.',
3467
+ 'settings.commit.title': 'رسالة إيداع بالذكاء الاصطناعي',
3468
+ 'settings.commit.model': 'النموذج',
3469
+ 'settings.commit.modelHint': 'فارغ = اتبع هذه الجلسة (provider/model)',
3470
+ 'settings.commit.reasoning': 'مستوى التفكير',
3471
+ 'settings.commit.reasoningHint': 'فارغ = الافتراضي للنموذج',
3472
+ 'settings.commit.prompt': 'تعليمات إضافية',
3473
+ 'settings.commit.promptHint': 'مثال: Conventional Commits، والعنوان بالعربية، وبحد أقصى 72 حرفًا',
3474
+ 'settings.commit.tooLong': 'يُحفظ أول {n} حرفًا فقط',
3475
+ 'changes.aiWrite': 'اكتب بالذكاء الاصطناعي',
3476
+ 'changes.aiBusy': 'جارٍ الكتابة…',
3477
+ 'changes.aiQuota': 'يستهلك حصتك',
3478
+ 'changes.aiOverwriteTitle': 'استبدال المسودة الحالية؟',
3479
+ 'changes.aiOverwriteText': 'سيستبدل نص الذكاء الاصطناعي رسالة الإيداع التي كتبتها (يمكن التراجع).',
3480
+ 'changes.aiUndo': 'تراجع',
3481
+ 'changes.aiFailed': 'تعذّرت الكتابة:',
3482
+ 'changes.aiNotice': 'لم تُرسل بعض التغييرات إلى النموذج (مقتطعة أو متروكة)؛ قد تكون الرسالة ناقصة.',
3483
+ 'settings.rail.writeFailed': 'لم يتم حفظ الإعداد؛ عاد المفتاح إلى حاله. حاول مرة أخرى.',
3484
+ 'settings.rail.readonly': 'يوفّر هذا المضيف الإعدادات للقراءة فقط.',
3064
3485
  'rail.settingsHint': 'اسحب أو استخدم الأسهم لإعادة الترتيب؛ وأيقونة العين تبدّل الظهور',
3065
3486
  'rail.reset': 'استعادة الإعدادات الافتراضية',
3066
3487
  'rail.up': 'تحريك لأعلى',
@@ -3222,6 +3643,28 @@ window.__ModuleLoader__.load({
3222
3643
  'toolbar.tree': 'शाखा पैनल',
3223
3644
  'rail.more': 'अधिक क्रियाएँ',
3224
3645
  'rail.settings': 'क्रिया पट्टी सेटिंग्स',
3646
+ 'rail.openSettings': 'प्लगइन सेटिंग खोलें',
3647
+ 'settings.rail.title': 'एक्शन रेल बटन',
3648
+ 'settings.rail.hint': 'रेल पर दिखने वाली क्रियाएँ चुनें। क्रम निश्चित है; चुनाव हटाने पर वह छिप जाती है।',
3649
+ 'settings.rail.unavailable': 'इस होस्ट में सेटिंग सेवा नहीं है; पैनल में «एक्शन रेल सेटिंग» का उपयोग करें।',
3650
+ 'settings.commit.title': 'AI कमिट संदेश',
3651
+ 'settings.commit.model': 'मॉडल',
3652
+ 'settings.commit.modelHint': 'खाली = इस सत्र का अनुसरण करें (provider/model)',
3653
+ 'settings.commit.reasoning': 'तर्क की तीव्रता',
3654
+ 'settings.commit.reasoningHint': 'खाली = मॉडल का डिफ़ॉल्ट',
3655
+ 'settings.commit.prompt': 'अतिरिक्त निर्देश',
3656
+ 'settings.commit.promptHint': 'जैसे Conventional Commits, विषय हिंदी में, अधिकतम 72 अक्षर',
3657
+ 'settings.commit.tooLong': 'केवल पहले {n} अक्षर सहेजे जाते हैं',
3658
+ 'changes.aiWrite': 'AI से लिखें',
3659
+ 'changes.aiBusy': 'लिखा जा रहा है…',
3660
+ 'changes.aiQuota': 'आपका कोटा खर्च होता है',
3661
+ 'changes.aiOverwriteTitle': 'मौजूदा ड्राफ़्ट बदलें?',
3662
+ 'changes.aiOverwriteText': 'AI पाठ आपके लिखे कमिट संदेश की जगह लेगा (पूर्ववत कर सकते हैं)।',
3663
+ 'changes.aiUndo': 'पूर्ववत करें',
3664
+ 'changes.aiFailed': 'लिख नहीं सका:',
3665
+ 'changes.aiNotice': 'कुछ बदलाव मॉडल को नहीं भेजे गए (काटे या छोड़े गए); संदेश अधूरा हो सकता है।',
3666
+ 'settings.rail.writeFailed': 'सेटिंग सहेजी नहीं गई; स्विच वापस पुरानी स्थिति में आ गया। फिर से कोशिश करें।',
3667
+ 'settings.rail.readonly': 'यह होस्ट कॉन्फ़िगरेशन को केवल-पढ़ने के लिए देता है।',
3225
3668
  'rail.settingsHint': 'क्रम बदलने के लिए खींचें या तीरों का उपयोग करें; आँख आइकन दृश्यता बदलता है',
3226
3669
  'rail.reset': 'डिफ़ॉल्ट बहाल करें',
3227
3670
  'rail.up': 'ऊपर ले जाएँ',
@@ -3383,6 +3826,28 @@ window.__ModuleLoader__.load({
3383
3826
  'toolbar.tree': 'แผงแบรนช์',
3384
3827
  'rail.more': 'การทำงานเพิ่มเติม',
3385
3828
  'rail.settings': 'ตั้งค่าแถบการทำงาน',
3829
+ 'rail.openSettings': 'เปิดการตั้งค่าปลั๊กอิน',
3830
+ 'settings.rail.title': 'ปุ่มแถบการทำงาน',
3831
+ 'settings.rail.hint': 'ทำเครื่องหมายการทำงานที่จะแสดงบนแถบ ลำดับคงที่; ยกเลิกเครื่องหมายเพื่อซ่อน',
3832
+ 'settings.rail.unavailable': 'โฮสต์นี้ไม่มีบริการตั้งค่า ให้ใช้ «ตั้งค่าแถบการทำงาน» ในแผง',
3833
+ 'settings.commit.title': 'ข้อความคอมมิตโดย AI',
3834
+ 'settings.commit.model': 'โมเดล',
3835
+ 'settings.commit.modelHint': 'ว่าง = ตามเซสชันนี้ (provider/model)',
3836
+ 'settings.commit.reasoning': 'ระดับการใช้เหตุผล',
3837
+ 'settings.commit.reasoningHint': 'ว่าง = ค่าเริ่มต้นของโมเดล',
3838
+ 'settings.commit.prompt': 'คำสั่งเพิ่มเติม',
3839
+ 'settings.commit.promptHint': 'เช่น Conventional Commits หัวข้อเป็นภาษาไทย ไม่เกิน 72 ตัวอักษร',
3840
+ 'settings.commit.tooLong': 'บันทึกเพียง {n} ตัวอักษรแรก',
3841
+ 'changes.aiWrite': 'เขียนด้วย AI',
3842
+ 'changes.aiBusy': 'กำลังเขียน…',
3843
+ 'changes.aiQuota': 'ใช้โควตาของคุณ',
3844
+ 'changes.aiOverwriteTitle': 'แทนที่ฉบับร่างปัจจุบันหรือไม่',
3845
+ 'changes.aiOverwriteText': 'ข้อความจาก AI จะแทนที่ข้อความคอมมิตที่คุณพิมพ์ (เลิกทำได้)',
3846
+ 'changes.aiUndo': 'เลิกทำ',
3847
+ 'changes.aiFailed': 'เขียนไม่สำเร็จ:',
3848
+ 'changes.aiNotice': 'มีการเปลี่ยนแปลงบางส่วนที่ไม่ได้ส่งให้โมเดล (ถูกตัดหรือทิ้ง) ข้อความอาจไม่ครบถ้วน',
3849
+ 'settings.rail.writeFailed': 'บันทึกการตั้งค่าไม่สำเร็จ สวิตช์กลับไปสถานะเดิม ลองอีกครั้ง',
3850
+ 'settings.rail.readonly': 'โฮสต์นี้ให้บริการการตั้งค่าแบบอ่านอย่างเดียว',
3386
3851
  'rail.settingsHint': 'ลากหรือใช้ลูกศรเพื่อจัดลำดับ; ไอคอนตาใช้สลับการแสดง',
3387
3852
  'rail.reset': 'คืนค่าเริ่มต้น',
3388
3853
  'rail.up': 'เลื่อนขึ้น',
@@ -3639,7 +4104,78 @@ window.__ModuleLoader__.load({
3639
4104
  return { order, hidden }
3640
4105
  }
3641
4106
 
4107
+ /* ---------------------------------------------------------------------
4108
+ Where the rail config LIVES. On dsh >= 0.1.7 a plugin's configuration is
4109
+ the row Config (the official settings surface, persisted by the host under
4110
+ the loader row id) and the browser reads it through ctx.configForms — so
4111
+ the settings card and the rail read ONE source and nothing is written
4112
+ twice. An older host has no such service: there the rail keeps its
4113
+ localStorage home and its in-panel editor, which is the only editor those
4114
+ hosts have. `railForm` is that switch; null means legacy.
4115
+ --------------------------------------------------------------------- */
4116
+ const RAIL_NS = 'ide-git'
4117
+ /* One write window for the settings card: a burst of switches becomes one
4118
+ atomic mutation instead of a stream the Host can lose. */
4119
+ const FLUSH_MS = 250
4120
+ const RAIL_MIGRATED_KEY = 'dsh-ide-git.rail.v1.migrated'
4121
+ let railForm = null
4122
+ let railMigrating = false
4123
+ const railWatchers = new Set()
4124
+
4125
+ /* ---- rail config core (pure: tests/smoke.mjs slices this block) ---- */
4126
+ function railFieldOf(id) {
4127
+ return 'rail' + id.charAt(0).toUpperCase() + id.slice(1)
4128
+ }
4129
+
4130
+ /* Stored booleans -> the { order, hidden } shape the rail has always used.
4131
+ The order is the code order now: the settings card edits one switch per
4132
+ action, so a stored permutation would have no editor left, and dropping it
4133
+ keeps this surface honest rather than half-editable. */
4134
+ function railConfigOfValues(values) {
4135
+ const source = values !== null && typeof values === 'object' ? values : {}
4136
+ const hidden = RAIL_IDS.filter((id) => source[railFieldOf(id)] === false)
4137
+ return { order: RAIL_IDS.slice(), hidden: hidden }
4138
+ }
4139
+
4140
+ /* The inverse of railFieldOf, for the one-field write path. Returns null for a
4141
+ field no rail action owns, so a caller cannot invent a row-Config key. */
4142
+ function railIdOfField(field) {
4143
+ if (typeof field !== 'string' || field.indexOf('rail') !== 0 || field.length <= 4) return null
4144
+ const name = field.slice(4)
4145
+ const id = name.charAt(0).toLowerCase() + name.slice(1)
4146
+ return RAIL_IDS.indexOf(id) < 0 ? null : id
4147
+ }
4148
+
4149
+ function railValuesOfConfig(config) {
4150
+ const values = {}
4151
+ for (const id of RAIL_IDS) values[railFieldOf(id)] = config.hidden.indexOf(id) < 0
4152
+ return values
4153
+ }
4154
+
4155
+ /* ---- end rail config core ---- */
4156
+
4157
+ /* The three AI-commit settings live in the same row Config document the
4158
+ settings card writes; the panel reads them per render, so a change in the
4159
+ card applies to the next click without a reload. */
4160
+ function readCommitSettings() {
4161
+ let values = {}
4162
+ const ready = railFormReady()
4163
+ if (ready !== null && ready.value !== null && typeof ready.value === 'object') values = ready.value
4164
+ const text = (field, max) => (typeof values[field] === 'string' ? values[field].trim().slice(0, max) : '')
4165
+ return { model: text('commitModel', 120), reasoning: text('commitReasoning', 60), prompt: text('commitPrompt', 2000) }
4166
+ }
4167
+
4168
+ function railFormReady() {
4169
+ if (railForm === null) return null
4170
+ const snapshot = railForm.getSnapshot()
4171
+ if (snapshot === null || snapshot === undefined || snapshot.status !== 'ready' || snapshot.value === undefined) return null
4172
+ return snapshot
4173
+ }
4174
+
3642
4175
  function readRailConfig() {
4176
+ const ready = railFormReady()
4177
+ if (ready !== null) return railConfigOfValues(ready.value)
4178
+ // Legacy home (and the state before the host answers): localStorage.
3643
4179
  try {
3644
4180
  const raw = window.localStorage.getItem(RAIL_KEY)
3645
4181
  if (raw === null || raw === '') return normalizeRail(null)
@@ -3648,10 +4184,151 @@ window.__ModuleLoader__.load({
3648
4184
  return normalizeRail(null)
3649
4185
  }
3650
4186
 
3651
- function writeRailConfig(config) {
4187
+ /* `form` lets a caller write through the exact handle it read from (the
4188
+ settings card gets the framework's own form); without it the module handle
4189
+ is used. When a config surface EXISTS but is not ready yet, the write is
4190
+ refused instead of silently landing in the legacy localStorage home — two
4191
+ sources would be worse than a refused edit. */
4192
+ function writeRailConfig(config, form) {
4193
+ const active = form !== undefined && form !== null && typeof form.getSnapshot === 'function' ? form : railForm
4194
+ let ready = null
4195
+ try {
4196
+ const snapshot = active === null || active === undefined ? null : active.getSnapshot()
4197
+ if (snapshot !== null && snapshot !== undefined && snapshot.status === 'ready' && snapshot.value !== undefined) ready = snapshot
4198
+ } catch (error) { void error }
4199
+ if (ready !== null) {
4200
+ if (ready.writable !== true || typeof active.set !== 'function') return
4201
+ railForm = active
4202
+ const current = ready.value !== null && typeof ready.value === 'object' ? ready.value : {}
4203
+ // Only the fields that actually changed: one write, not sixteen.
4204
+ for (const id of RAIL_IDS) {
4205
+ const field = railFieldOf(id)
4206
+ const visible = config.hidden.indexOf(id) < 0
4207
+ if (current[field] === visible) continue
4208
+ const written = active.set(field, visible)
4209
+ if (written !== undefined && written !== null && typeof written.then === 'function') {
4210
+ void written.then((accepted) => {
4211
+ if (accepted === false) console.warn('[dsh-ide-git] rail setting "' + field + '" was refused by the host')
4212
+ }, (error) => { console.warn('[dsh-ide-git] rail setting write failed: ' + String(error && error.message ? error.message : error)) })
4213
+ }
4214
+ }
4215
+ return
4216
+ }
4217
+ if (railForm !== null) return
3652
4218
  try { window.localStorage.setItem(RAIL_KEY, JSON.stringify(config)) } catch (error) { void error }
3653
4219
  }
3654
4220
 
4221
+ /* One switch, one field, one write — computed from the value the user just
4222
+ chose rather than from a whole-config snapshot that may predate the click.
4223
+ The card edits one field at a time, and a full-config write derived from a
4224
+ stale render clobbers whichever sibling was toggled a moment earlier
4225
+ (measured on 16 rapid toggles: 4 fields never reached the row Config and
4226
+ reverted on reload).
4227
+ Returns the write's disposition so the caller can roll its optimistic value
4228
+ back and say so instead of leaving a switch that lies:
4229
+ sent:true + settled:Promise<boolean> when the Host answers
4230
+ sent:false, reason 'read-only' | 'not-ready' | 'unknown'. */
4231
+ function writeRailFields(ops, form) {
4232
+ const active = form !== undefined && form !== null && typeof form.getSnapshot === 'function' ? form : railForm
4233
+ let ready = null
4234
+ try {
4235
+ const snapshot = active === null || active === undefined ? null : active.getSnapshot()
4236
+ if (snapshot !== null && snapshot !== undefined && snapshot.status === 'ready' && snapshot.value !== undefined) ready = snapshot
4237
+ } catch (error) { void error }
4238
+ if (ready !== null) {
4239
+ if (ready.writable !== true || typeof active.mutate !== 'function') return { sent: false, reason: 'read-only' }
4240
+ railForm = active
4241
+ const settled = Promise.resolve(active.mutate(ops)).then(
4242
+ (accepted) => accepted !== false,
4243
+ (error) => {
4244
+ console.warn('[dsh-ide-git] rail setting write failed: ' + String(error && error.message ? error.message : error))
4245
+ return false
4246
+ },
4247
+ )
4248
+ return { sent: true, settled: settled }
4249
+ }
4250
+ /* A config surface that exists but has not answered yet must refuse rather
4251
+ than fall through to the legacy home: two sources would be worse. */
4252
+ if (railForm !== null) return { sent: false, reason: 'not-ready' }
4253
+ const current = readRailConfig()
4254
+ let hidden = current.hidden.slice()
4255
+ for (const op of ops) {
4256
+ const id = railIdOfField(String(op.path[0]))
4257
+ if (id === null) return { sent: false, reason: 'unknown' }
4258
+ const wanted = op.value === true
4259
+ if (wanted === true) hidden = hidden.filter((entry) => entry !== id)
4260
+ else if (hidden.indexOf(id) < 0) hidden = hidden.concat([id])
4261
+ }
4262
+ try { window.localStorage.setItem(RAIL_KEY, JSON.stringify({ order: current.order, hidden: hidden })) } catch (error) { void error }
4263
+ notifyRailConfig()
4264
+ return { sent: true }
4265
+ }
4266
+
4267
+ /* One switch's write. The card batches a burst through writeRailFields so the
4268
+ Host sees one atomic mutation instead of a stream of overlapping ones. */
4269
+ function writeRailField(field, value, form) {
4270
+ return writeRailFields([{ op: 'set', path: [field], value: value }], form)
4271
+ }
4272
+
4273
+ /* One-shot move of the pre-settings rail config into the row Config: read the
4274
+ legacy value ONCE, write it through, mark it done. After this the row Config
4275
+ is the only source (the legacy key is left untouched as a paper trail, but
4276
+ nothing writes it any more on this host). */
4277
+ function migrateRailConfig(form) {
4278
+ try {
4279
+ if (window.localStorage.getItem(RAIL_MIGRATED_KEY) !== null) return
4280
+ if (railMigrating === true) return
4281
+ const done = () => {
4282
+ railMigrating = false
4283
+ try { window.localStorage.setItem(RAIL_MIGRATED_KEY, '1') } catch (error) { void error }
4284
+ }
4285
+ const raw = window.localStorage.getItem(RAIL_KEY)
4286
+ if (raw === null || raw === '') { done(); return }
4287
+ const values = railValuesOfConfig(normalizeRail(JSON.parse(raw)))
4288
+ const ready = railFormReady()
4289
+ const accepted = ready !== null && ready.value !== null && typeof ready.value === 'object' ? ready.value : {}
4290
+ const ops = []
4291
+ for (const id of RAIL_IDS) {
4292
+ const field = railFieldOf(id)
4293
+ if (accepted[field] === values[field]) continue
4294
+ ops.push({ op: 'set', path: [field], value: values[field] === true })
4295
+ }
4296
+ if (ops.length === 0) { done(); return }
4297
+ /* ONE atomic mutation, and the marker ONLY once the Host accepted it.
4298
+ The per-field set() loop this replaces fired sixteen unawaited writes
4299
+ and stamped the marker right away, so on a real profile fifteen of the
4300
+ sixteen fields (and in one case all of them) never reached the row
4301
+ Config and were never retried — an upgrade silently lost the user's
4302
+ rail settings while the marker claimed success. A refusal, a failed
4303
+ write or a superseded one now leaves the marker unwritten so the next
4304
+ load migrates again. */
4305
+ railMigrating = true
4306
+ const outcome = writeRailFields(ops, form)
4307
+ if (outcome.sent !== true) {
4308
+ railMigrating = false
4309
+ console.warn('[dsh-ide-git] legacy rail migration was refused (' + String(outcome.reason) + '); it stays unmarked and is retried')
4310
+ return
4311
+ }
4312
+ if (outcome.settled === undefined) { done(); return }
4313
+ void outcome.settled.then((acceptedWrite) => {
4314
+ if (acceptedWrite === true) { done(); return }
4315
+ railMigrating = false
4316
+ console.warn('[dsh-ide-git] legacy rail migration was not accepted; it stays unmarked and is retried')
4317
+ })
4318
+ } catch (error) { void error }
4319
+ }
4320
+
4321
+ function subscribeRailConfig(listener) {
4322
+ railWatchers.add(listener)
4323
+ return () => { railWatchers.delete(listener) }
4324
+ }
4325
+
4326
+ function notifyRailConfig() {
4327
+ for (const listener of Array.from(railWatchers)) {
4328
+ try { listener() } catch (error) { void error }
4329
+ }
4330
+ }
4331
+
3655
4332
  /* ============================== pane sizes ============================== */
3656
4333
 
3657
4334
  /* ---- pane sizing core (pure: tests/smoke.mjs slices this block and drives
@@ -4157,7 +4834,11 @@ window.__ModuleLoader__.load({
4157
4834
  compare: ['M4 4h7', 'M4 12h7', 'M6.5 1.5 4 4l2.5 2.5', 'M9.5 9.5 12 12l-2.5 2.5'],
4158
4835
  stash: ['M2.5 3.5h11V6h-11z', 'M3.5 6v6.5h9V6', 'M6.5 8.5h3'],
4159
4836
  tag: ['M2.5 7.5 7.5 2.5h6v6l-5 5z', 'M10.5 5.5h.01'],
4160
- settings: ['M8 6.4a1.6 1.6 0 1 0 0 3.2 1.6 1.6 0 0 0 0-3.2Z', 'M8 1.8v1.5', 'M8 12.7v1.5', 'M1.8 8h1.5', 'M12.7 8h1.5', 'M3.6 3.6l1.1 1.1', 'M11.3 11.3l1.1 1.1', 'M12.4 3.6l-1.1 1.1', 'M4.7 11.3l-1.1 1.1'],
4837
+ // A real gear: the toothed contour plus the centre hole, borrowed from the
4838
+ // official product icon set (ui-primitives IconSettingsOutlineMedium, same
4839
+ // 16x16 grid and currentColor stroke) because the previous entry was a
4840
+ // centre dot with eight rays — a sunburst, not a settings control.
4841
+ settings: ['M13.0107 7.79377C12.9505 7.89401 12.9205 7.94413 12.9205 7.99951C12.9205 8.0549 12.9505 8.10502 13.0106 8.20528L13.9849 9.83006C14.045 9.93029 14.0751 9.9804 14.0751 10.0358C14.0751 10.0911 14.045 10.1413 13.9849 10.2415L13.0037 11.8777C12.9468 11.9726 12.9184 12.0201 12.8725 12.0461C12.8267 12.072 12.7713 12.072 12.6607 12.072H10.6704C10.5598 12.072 10.5045 12.072 10.4586 12.098C10.4128 12.1239 10.3843 12.1714 10.3274 12.2662L9.33825 13.9142C9.28133 14.009 9.25287 14.0564 9.20703 14.0823C9.16118 14.1083 9.10588 14.1083 8.99529 14.1083H7.00486C6.89426 14.1083 6.83896 14.1083 6.79312 14.0823C6.74727 14.0564 6.71881 14.009 6.6619 13.9142L5.67273 12.2662C5.61581 12.1714 5.58735 12.1239 5.54151 12.098C5.49566 12.072 5.44036 12.072 5.32977 12.072H3.33945C3.2288 12.072 3.17347 12.072 3.12761 12.0461C3.08176 12.0201 3.0533 11.9726 2.9964 11.8777L2.0152 10.2415C1.9551 10.1413 1.92505 10.0911 1.92505 10.0358C1.92505 9.9804 1.9551 9.93029 2.0152 9.83006L2.98951 8.20528C3.04963 8.10502 3.07969 8.0549 3.07969 7.99951C3.07968 7.94413 3.04961 7.89401 2.98946 7.79377L2.01529 6.17011C1.95514 6.06987 1.92507 6.01975 1.92507 5.96437C1.92506 5.90899 1.95512 5.85886 2.01524 5.7586L2.9964 4.1224C3.0533 4.0275 3.08176 3.98005 3.12761 3.95408C3.17347 3.92811 3.2288 3.92811 3.33945 3.92811H5.32977C5.44036 3.92811 5.49566 3.92811 5.54151 3.90216C5.58735 3.87621 5.61581 3.82879 5.67273 3.73397L6.6619 2.08599C6.71881 1.99116 6.74727 1.94375 6.79312 1.9178C6.83896 1.89185 6.89426 1.89185 7.00486 1.89185H8.99529C9.10588 1.89185 9.16118 1.89185 9.20703 1.9178C9.25287 1.94375 9.28133 1.99116 9.33825 2.08599L10.3274 3.73397C10.3843 3.82879 10.4128 3.87621 10.4586 3.90216C10.5045 3.92811 10.5598 3.92811 10.6704 3.92811H12.6607C12.7713 3.92811 12.8267 3.92811 12.8725 3.95408C12.9184 3.98005 12.9468 4.0275 13.0037 4.1224L13.9849 5.7586C14.045 5.85886 14.0751 5.90899 14.0751 5.96437C14.0751 6.01975 14.045 6.06987 13.9849 6.17011L13.0107 7.79377Z', 'M8 9.75012C8.9665 9.75012 9.75 8.96662 9.75 8.00012C9.75 7.03362 8.9665 6.25012 8 6.25012C7.0335 6.25012 6.25 7.03362 6.25 8.00012C6.25 8.96662 7.0335 9.75012 8 9.75012Z'],
4161
4842
  more: ['M3.6 8h0.01', 'M8 8h0.01', 'M12.4 8h0.01'],
4162
4843
  eye: ['M1.5 8S4.1 3.8 8 3.8 14.5 8 14.5 8 11.9 12.2 8 12.2 1.5 8 1.5 8Z', 'M8 6.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Z'],
4163
4844
  eyeOff: ['M1.6 8S4.2 4 8 4c1 0 1.9.2 2.7.6', 'M13 5.6c.9 1.1 1.5 2.4 1.5 2.4S11.9 12 8 12c-.9 0-1.7-.2-2.4-.5', 'M2.6 2.6l10.8 10.8'],
@@ -4263,6 +4944,264 @@ window.__ModuleLoader__.load({
4263
4944
  list is drag-sortable and arrow-sortable (the arrows also work with a
4264
4945
  keyboard); visibility is a per-action toggle, not a delete, so a hidden
4265
4946
  action keeps its position. */
4947
+ /* The plugin's settings card. It is the SAME surface on the plugin's detail
4948
+ page (slot `plugins.bundle.config`, keyed by the PACKAGE name) and in
4949
+ Settings → Plugins (slot `settings.plugin.item`, keyed by the settings
4950
+ namespace = the loader row id), and it is the only editor when the host
4951
+ serves the row Config — the in-panel editor is not reachable there.
4952
+
4953
+ A host without that service still renders the card (the seat exists), so it
4954
+ says so instead of pretending to have settings. */
4955
+ function RailSettingsCard(props) {
4956
+ const t = props.t
4957
+ const [tick, setTick] = useState(0)
4958
+ const [pending, setPending] = useState({})
4959
+ const [note, setNote] = useState('')
4960
+ /* The three AI-commit settings are free text, so they are drafted locally
4961
+ and written on blur / Enter — one write per edit, never one per keystroke. */
4962
+ const [textDraft, setTextDraft] = useState({})
4963
+ /* The user's latest intent per field, written SYNCHRONOUSLY. React state is
4964
+ not enough: two clicks inside one frame would both read the pre-click
4965
+ snapshot, so the second click recomputed the same target and the toggle
4966
+ was lost (measured: a fast double click on one switch netted one step
4967
+ instead of returning to where it started). */
4968
+ const intent = useRef({})
4969
+ const timers = useRef(new Set())
4970
+ const dirty = useRef(new Map())
4971
+ const pendingFlush = useRef(null)
4972
+ const bump = () => setTick((value) => value + 1)
4973
+ /* Writes the Host answers ok for can still be lost when several overlap its
4974
+ persist cycle: measured on this surface, 16 switches toggled 55ms apart
4975
+ left 3 fields at their old value in the row Config while every request
4976
+ answered 200, and the same 16 at 500ms apart all landed. So a switch is
4977
+ only "done" when the accepted document agrees — checked once after the
4978
+ burst settles, re-issued once, and otherwise rolled back and reported. */
4979
+ const reconcile = (field, want, attempt) => {
4980
+ const timer = setTimeout(() => {
4981
+ timers.current.delete(timer)
4982
+ let hostValue
4983
+ try {
4984
+ const snapshot = scope === null || scope === undefined ? null : scope.getSnapshot()
4985
+ hostValue = snapshot !== null && snapshot !== undefined && snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value[field] : undefined
4986
+ } catch (error) { hostValue = undefined }
4987
+ if (hostValue === want) return
4988
+ if (attempt === 0) {
4989
+ const again = writeRailField(field, want, scope)
4990
+ if (again.sent === true) {
4991
+ if (again.settled !== undefined) void again.settled.then(() => { reconcile(field, want, 1) })
4992
+ else reconcile(field, want, 1)
4993
+ return
4994
+ }
4995
+ }
4996
+ delete intent.current[field]
4997
+ setPending((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
4998
+ setNote(t('settings.rail.writeFailed'))
4999
+ }, 600)
5000
+ timers.current.add(timer)
5001
+ }
5002
+ const rollbackFields = (fields) => {
5003
+ for (const field of fields) {
5004
+ delete intent.current[field]
5005
+ setPending((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5006
+ }
5007
+ }
5008
+ /* One atomic mutation for the whole burst. Sixteen separate writes 55ms
5009
+ apart lost 3 fields inside the Host's persist cycle while every request
5010
+ answered 200 (the same sixteen 500ms apart all landed), so the fix is to
5011
+ stop producing a burst: collect the fields the user touched and submit
5012
+ them together, at most one mutation per window. */
5013
+ const commitTextField = (field, max) => {
5014
+ const accepted = values[field] === undefined || values[field] === null ? '' : String(values[field])
5015
+ const shown = Object.prototype.hasOwnProperty.call(textDraft, field) ? textDraft[field] : accepted
5016
+ const wanted = shown.slice(0, max)
5017
+ if (wanted === accepted) return
5018
+ const outcome = writeRailFields([{ op: 'set', path: [field], value: wanted }], scope)
5019
+ if (outcome.sent !== true) {
5020
+ setNote(t(outcome.reason === 'read-only' ? 'settings.rail.readonly' : 'settings.rail.writeFailed'))
5021
+ return
5022
+ }
5023
+ if (outcome.settled === undefined) return
5024
+ void outcome.settled.then((ok) => {
5025
+ if (ok !== true) { setNote(t('settings.rail.writeFailed')); return }
5026
+ /* The accepted document now carries it: drop the draft so the control
5027
+ shows the Host's own value again. */
5028
+ setTextDraft((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5029
+ })
5030
+ }
5031
+ const textRow = (field, labelKey, hintKey, max, multiline) => {
5032
+ const accepted = values[field] === undefined || values[field] === null ? '' : String(values[field])
5033
+ const shown = Object.prototype.hasOwnProperty.call(textDraft, field) ? textDraft[field] : accepted
5034
+ const id = 'dig-' + field
5035
+ const common = {
5036
+ id: id, className: 'dig-input dig-settings-input', value: shown, spellCheck: false,
5037
+ disabled: readOnly === true, placeholder: t(hintKey), 'aria-labelledby': id + '-label',
5038
+ onChange: (event) => setTextDraft((now) => Object.assign({}, now, { [field]: event.target.value })),
5039
+ onBlur: () => commitTextField(field, max),
5040
+ }
5041
+ if (multiline === true) common.onKeyDown = (event) => { if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) commitTextField(field, max) }
5042
+ else common.onKeyDown = (event) => { if (event.key === 'Enter') commitTextField(field, max) }
5043
+ return E('div', { className: 'dig-settings-text', key: field },
5044
+ E('label', { id: id + '-label', className: 'dig-settings-text-label', htmlFor: id }, t(labelKey)),
5045
+ multiline === true ? E('textarea', Object.assign({ rows: 3 }, common)) : E('input', Object.assign({ type: 'text' }, common)),
5046
+ shown.length >= max ? E('div', { className: 'dig-settings-error' }, fill(t('settings.commit.tooLong'), { n: String(max) })) : null)
5047
+ }
5048
+ const flush = () => {
5049
+ pendingFlush.current = null
5050
+ /* The pending TARGET per field, not "whatever the render-time intent
5051
+ happens to hold now": the accepted document can already agree with the
5052
+ user's new value, which clears the optimistic intent before this flush
5053
+ runs — reading it here would then submit the opposite of the click and
5054
+ write the switch back to the wrong state. */
5055
+ const targets = new Map(dirty.current)
5056
+ dirty.current.clear()
5057
+ const fields = Array.from(targets.keys())
5058
+ if (fields.length === 0) return
5059
+ const ops = fields.map((field) => ({ op: 'set', path: [field], value: targets.get(field) === true }))
5060
+ const outcome = writeRailFields(ops, scope)
5061
+ if (outcome.sent !== true) {
5062
+ rollbackFields(fields)
5063
+ setNote(t(outcome.reason === 'read-only' ? 'settings.rail.readonly' : 'settings.rail.writeFailed'))
5064
+ return
5065
+ }
5066
+ if (outcome.settled === undefined) return
5067
+ void outcome.settled.then((accepted) => {
5068
+ if (accepted !== true) {
5069
+ rollbackFields(fields)
5070
+ setNote(t('settings.rail.writeFailed'))
5071
+ return
5072
+ }
5073
+ for (const field of fields) reconcile(field, targets.get(field) === true, 0)
5074
+ })
5075
+ }
5076
+ const scheduleFlush = () => {
5077
+ if (pendingFlush.current !== null) return
5078
+ pendingFlush.current = setTimeout(() => { pendingFlush.current = null; flush() }, FLUSH_MS)
5079
+ timers.current.add(pendingFlush.current)
5080
+ }
5081
+ /* Two handles, one document: the slot hands the card the framework's own
5082
+ form for this entry (`props.scope`), and our ctx.configForms handle is the
5083
+ fallback — both address the same namespace, so a write through either is
5084
+ the same row Config write. */
5085
+ const scope = props.scope !== undefined && props.scope !== null && typeof props.scope.getSnapshot === 'function'
5086
+ ? props.scope
5087
+ : railForm
5088
+ useEffect(() => {
5089
+ const offRail = subscribeRailConfig(bump)
5090
+ let offScope
5091
+ if (scope !== null && scope !== undefined && typeof scope.subscribe === 'function') {
5092
+ const unsubscribe = scope.subscribe(bump)
5093
+ if (typeof unsubscribe === 'function') offScope = unsubscribe
5094
+ }
5095
+ return () => {
5096
+ offRail()
5097
+ if (offScope !== undefined) offScope()
5098
+ for (const timer of Array.from(timers.current)) clearTimeout(timer)
5099
+ timers.current.clear()
5100
+ }
5101
+ }, [])
5102
+ void tick
5103
+ const head = E('div', { className: 'dig-settings-head' },
5104
+ E('div', { className: 'dig-settings-title' }, t('settings.rail.title')),
5105
+ E('div', { className: 'dig-settings-hint' }, t('settings.rail.hint')))
5106
+ let snapshot = null
5107
+ try { snapshot = scope === null || scope === undefined ? null : scope.getSnapshot() } catch (error) { void error }
5108
+ const status = snapshot === null || snapshot === undefined || typeof snapshot.status !== 'string' ? 'absent' : snapshot.status
5109
+ const values = snapshot !== null && snapshot !== undefined && snapshot.value !== null && typeof snapshot.value === 'object' ? snapshot.value : null
5110
+ // Drop optimistic values the host has since confirmed (post-commit effect).
5111
+ useEffect(() => {
5112
+ setPending((current) => {
5113
+ const keys = Object.keys(current)
5114
+ if (keys.length === 0 || values === null) return current
5115
+ const next = {}
5116
+ let changed = false
5117
+ for (const key of keys) {
5118
+ if (values[key] === current[key]) {
5119
+ changed = true
5120
+ /* The accepted document agrees: this intent is no longer pending.
5121
+ Deleting twice is harmless, so a re-invoked updater stays safe. */
5122
+ delete intent.current[key]
5123
+ continue
5124
+ }
5125
+ next[key] = current[key]
5126
+ }
5127
+ return changed ? next : current
5128
+ })
5129
+ })
5130
+ // Switches exist only where they can actually write: a host that does not
5131
+ // serve this entry's settings gets a sentence instead of dead controls.
5132
+ if (status !== 'ready' || values === null) {
5133
+ return E('div', { className: 'dig-settings', 'data-settings-status': status }, head,
5134
+ E('div', { className: 'dig-settings-note' }, t('settings.rail.unavailable')))
5135
+ }
5136
+ const config = railConfigOfValues(values)
5137
+ /* A toggle is a host round trip, so the switch would snap back to the old
5138
+ value for a moment and read as "stuck". The optimistic value wins until
5139
+ the accepted document agrees with it (then it is dropped). */
5140
+ const shown = (spec) => {
5141
+ const field = railFieldOf(spec.id)
5142
+ if (Object.prototype.hasOwnProperty.call(intent.current, field)) return intent.current[field]
5143
+ return config.hidden.indexOf(spec.id) < 0
5144
+ }
5145
+ /* A host that serves the document but refuses writes gets disabled switches
5146
+ and a sentence, not controls that silently do nothing. */
5147
+ const readOnly = snapshot.writable !== true
5148
+ return E('div', { className: 'dig-settings', 'data-settings-status': status, 'data-settings-readonly': readOnly === true ? 'true' : 'false' }, head,
5149
+ E('div', { className: readOnly === true ? 'dig-settings-note' : 'dig-settings-note dig-settings-note-hidden', 'data-settings-readonly-note': '' }, t('settings.rail.readonly')),
5150
+ note === '' ? null : E('div', { className: 'dig-settings-error', 'data-settings-error': '' }, note),
5151
+ E('div', { className: 'dig-settings-list' }, RAIL_SPECS.map((spec) => {
5152
+ const inputId = 'dig-rail-' + spec.id
5153
+ const visible = shown(spec)
5154
+ /* A row, not a <label>: a label that both carries htmlFor and wraps its
5155
+ own input toggles twice per click (once for the input, once for the
5156
+ label activation), which reads as a stuck switch and writes twice. */
5157
+ const toggle = (event) => {
5158
+ if (readOnly === true) { setNote(t('settings.rail.readonly')); return }
5159
+ const field = railFieldOf(spec.id)
5160
+ /* The control's own answer when the browser gives it to us: a click that
5161
+ lands while the previous one is still rendering would otherwise
5162
+ compose from the pre-click snapshot and be lost (measured: a fast
5163
+ double click netted one step instead of returning to its start). The
5164
+ row's label span is not a control, so it falls back to the
5165
+ synchronous intent — never to the render snapshot alone. */
5166
+ const fromControl = event !== undefined && event !== null && event.target !== null
5167
+ && event.target !== undefined && typeof event.target.checked === 'boolean'
5168
+ ? event.target.checked
5169
+ : null
5170
+ /* The control already holds the state the user just asked for, so it is
5171
+ the TARGET — inverting it here would write the opposite of the click.
5172
+ Without a control (the row's label span) the target is the inverse of
5173
+ the current intent, falling back to the accepted document. */
5174
+ const wanted = fromControl !== null
5175
+ ? fromControl
5176
+ : (Object.prototype.hasOwnProperty.call(intent.current, field)
5177
+ ? intent.current[field] !== true
5178
+ : config.hidden.indexOf(spec.id) < 0 !== true)
5179
+ intent.current[field] = wanted
5180
+ dirty.current.set(field, wanted)
5181
+ setPending((now) => Object.assign({}, now, { [field]: wanted }))
5182
+ setNote('')
5183
+ bump()
5184
+ scheduleFlush()
5185
+ }
5186
+ return E('div', { key: spec.id, className: 'dig-settings-row', 'data-rail-action': spec.id },
5187
+ E('input', {
5188
+ id: inputId,
5189
+ type: 'checkbox',
5190
+ className: 'dig-settings-check',
5191
+ 'aria-labelledby': inputId + '-label',
5192
+ checked: visible,
5193
+ disabled: readOnly === true,
5194
+ onChange: toggle,
5195
+ }),
5196
+ E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: spec.icon, size: 13 })),
5197
+ E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t(spec.key)))
5198
+ })),
5199
+ E('div', { className: 'dig-settings-sub' }, t('settings.commit.title')),
5200
+ textRow('commitModel', 'settings.commit.model', 'settings.commit.modelHint', 120, false),
5201
+ textRow('commitReasoning', 'settings.commit.reasoning', 'settings.commit.reasoningHint', 60, false),
5202
+ textRow('commitPrompt', 'settings.commit.prompt', 'settings.commit.promptHint', 2000, true))
5203
+ }
5204
+
4266
5205
  function RailSettings(props) {
4267
5206
  const t = props.t
4268
5207
  const [dragId, setDragId] = useState(null)
@@ -4691,6 +5630,12 @@ window.__ModuleLoader__.load({
4691
5630
  const [message, setMessage] = useState('')
4692
5631
  const [amend, setAmend] = useState(false)
4693
5632
  const [collapsed, setCollapsed] = useState(false)
5633
+ /* AI-written message (issue #6). The draft is kept so the write can be
5634
+ undone, and an existing draft is never overwritten without a word. */
5635
+ const [aiBusy, setAiBusy] = useState(false)
5636
+ const [aiNote, setAiNote] = useState(null)
5637
+ const [aiAsk, setAiAsk] = useState(false)
5638
+ const [aiUndo, setAiUndo] = useState(null)
4694
5639
  // 'flat' keeps git's own grouping (staged / changes / untracked); 'dir'
4695
5640
  // clusters each group's files under their folder, the way "Group by:
4696
5641
  // Directory" does in an IDE. Folders are addressed by <group>|<dir>, so a
@@ -4704,6 +5649,32 @@ window.__ModuleLoader__.load({
4704
5649
  const untracked = changes === null ? [] : changes.untracked
4705
5650
  const ignored = changes === null || changes.ignored === undefined ? [] : changes.ignored
4706
5651
  const total = conflicted.length + staged.length + unstaged.length + untracked.length
5652
+ const aiRun = async () => {
5653
+ setAiAsk(false)
5654
+ setAiBusy(true)
5655
+ setAiNote(null)
5656
+ try {
5657
+ const settings = props.commitSettings === undefined ? { model: '', reasoning: '', prompt: '' } : props.commitSettings
5658
+ const data = await props.request('commit-message', {
5659
+ cwd: props.cwd,
5660
+ sessionId: props.sessionId,
5661
+ model: settings.model,
5662
+ reasoningEffort: settings.reasoning,
5663
+ prompt: settings.prompt,
5664
+ })
5665
+ setAiUndo(message === '' ? null : message)
5666
+ setMessage(typeof data.message === 'string' ? data.message : '')
5667
+ /* Say what the model did NOT see, instead of quietly sending less. */
5668
+ const input = data.input === undefined ? {} : data.input
5669
+ const incomplete = input.truncated === true || (typeof input.dropped === 'number' && input.dropped > 0) || data.promptTruncated === true
5670
+ setAiNote(incomplete === true ? t('changes.aiNotice') : null)
5671
+ } catch (error) {
5672
+ const code = error !== null && error !== undefined && typeof error.code === 'string' ? error.code : 'unknown'
5673
+ setAiNote(t('changes.aiFailed') + ' [' + code + '] ' + String(error && error.message ? error.message : error))
5674
+ } finally {
5675
+ setAiBusy(false)
5676
+ }
5677
+ }
4707
5678
  const submit = (push) => {
4708
5679
  if (message.trim() === '' || props.busy === true) return false
4709
5680
  props.onCommit(message, amend, push === true)
@@ -4756,6 +5727,15 @@ window.__ModuleLoader__.load({
4756
5727
  key === 'staged' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onUnstageAll() }, t('changes.unstageAll')) : null,
4757
5728
  key === 'unstaged' || key === 'untracked' ? E('button', { type: 'button', className: 'dig-link', onClick: () => props.onStageAll(entries) }, t('changes.stageAll')) : null),
4758
5729
  rowsOf(key, entries))
5730
+ const aiConfirmDialog = aiAsk !== true ? null : E(ConfirmDialog, {
5731
+ t: t,
5732
+ title: t('changes.aiOverwriteTitle'),
5733
+ text: t('changes.aiOverwriteText'),
5734
+ okLabel: t('confirm.ok'),
5735
+ cancelLabel: t('confirm.cancel'),
5736
+ onConfirm: () => { void aiRun() },
5737
+ onCancel: () => setAiAsk(false),
5738
+ })
4759
5739
  const composer = props.compact === true
4760
5740
  ? E('div', { className: 'dig-commit-box dig-commit-box-compact' },
4761
5741
  E('input', {
@@ -4766,11 +5746,18 @@ window.__ModuleLoader__.load({
4766
5746
  if (event.key === 'Enter') { event.preventDefault(); submit() }
4767
5747
  },
4768
5748
  }),
5749
+ E('button', {
5750
+ type: 'button', className: 'dig-btn dig-btn-small dig-btn-ai',
5751
+ disabled: aiBusy === true || props.busy === true,
5752
+ title: t('changes.aiQuota'),
5753
+ onClick: () => { if (message.trim() === '') void aiRun(); else setAiAsk(true) },
5754
+ }, aiBusy === true ? t('changes.aiBusy') : t('changes.aiWrite')),
4769
5755
  E('button', {
4770
5756
  type: 'button', className: 'dig-btn dig-btn-primary dig-btn-small',
4771
5757
  disabled: message.trim() === '' || props.busy === true,
4772
5758
  onClick: () => { submit(false) },
4773
- }, t('changes.commit')))
5759
+ }, t('changes.commit')),
5760
+ E('span', { className: 'dig-commit-ai-hint dig-commit-ai-hint-block' }, t('changes.aiQuota')))
4774
5761
  : E('div', { className: 'dig-commit-box' },
4775
5762
  E('textarea', {
4776
5763
  className: 'dig-textarea', value: message, spellCheck: false,
@@ -4780,6 +5767,18 @@ window.__ModuleLoader__.load({
4780
5767
  if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); submit() }
4781
5768
  },
4782
5769
  }),
5770
+ E('div', { className: 'dig-commit-ai' },
5771
+ E('button', {
5772
+ type: 'button', className: 'dig-btn dig-btn-small dig-btn-ai',
5773
+ disabled: aiBusy === true || props.busy === true,
5774
+ onClick: () => { if (message.trim() === '') void aiRun(); else setAiAsk(true) },
5775
+ }, aiBusy === true ? t('changes.aiBusy') : t('changes.aiWrite')),
5776
+ E('span', { className: 'dig-commit-ai-hint' }, t('changes.aiQuota')),
5777
+ aiUndo === null ? null : E('button', {
5778
+ type: 'button', className: 'dig-link',
5779
+ onClick: () => { setMessage(aiUndo); setAiUndo(null); setAiNote(null) },
5780
+ }, t('changes.aiUndo'))),
5781
+ aiNote === null ? null : E('div', { className: 'dig-commit-ai-note', 'data-ai-note': '' }, aiNote),
4783
5782
  E('div', { className: 'dig-commit-actions' },
4784
5783
  E('label', { className: 'dig-check' },
4785
5784
  E('input', { type: 'checkbox', checked: amend, onChange: (event) => setAmend(event.target.checked) }),
@@ -4829,6 +5828,7 @@ window.__ModuleLoader__.load({
4829
5828
  onClick: () => props.onToggleIgnored(),
4830
5829
  }, E(Icon, { name: props.showIgnored === true ? 'eye' : 'eyeOff', size: 13 })))
4831
5830
  return E('div', { className: 'dig-changes' },
5831
+ aiConfirmDialog,
4832
5832
  head,
4833
5833
  E('div', { className: 'dig-changes-body' },
4834
5834
  E('div', { className: 'dig-changes-list' },
@@ -5100,6 +6100,7 @@ window.__ModuleLoader__.load({
5100
6100
  const [favTick, setFavTick] = useState(0)
5101
6101
  const [pathFilter, setPathFilter] = useState('')
5102
6102
  const [railConfig, setRailConfig] = useState(readRailConfig)
6103
+ const [railTick, setRailTick] = useState(0)
5103
6104
  const [railSettings, setRailSettings] = useState(false)
5104
6105
  const [railBox, setRailBox] = useState({ width: 0, height: 0 })
5105
6106
  const [toasts, setToasts] = useState([])
@@ -5135,6 +6136,15 @@ window.__ModuleLoader__.load({
5135
6136
  // its own — v0.1.3 already made that mistake with a height < 330 test (a
5136
6137
  // 1500x300 workbench came out single-column) and v0.3.5 still let
5137
6138
  // height < 200 flip a 1320x180 workbench into the right-sidebar chrome.
6139
+ /* Where the gear goes: the plugin's own settings page when this host has
6140
+ both the row Config and the navigation to it, the in-panel editor
6141
+ otherwise (see RAIL_NS / railForm). */
6142
+ const railSettingsTarget = () => (railForm === null || props.openSettings === null || props.openSettings === undefined ? 'panel' : 'page')
6143
+ const openSettingsPage = () => {
6144
+ if (props.openSettings === null || props.openSettings === undefined) return
6145
+ props.openSettings()
6146
+ }
6147
+
5138
6148
  const compact = size.width > 0 && size.width < COMPACT_MAX_WIDTH
5139
6149
  const columns = !compact && size.width >= 600 && size.width >= size.height * 1.15
5140
6150
 
@@ -5692,8 +6702,10 @@ window.__ModuleLoader__.load({
5692
6702
  E('span', { className: 'dig-banner-text' }, note),
5693
6703
  E('button', { type: 'button', className: 'dig-link', onClick: () => setNote(null) }, t('error.dismiss')))
5694
6704
 
6705
+ const commitSettings = readCommitSettings()
5695
6706
  const changesPane = E(ChangesPanel, {
5696
6707
  t: t, summary: summary, busy: busy, compact: compact, hideHeader: compact,
6708
+ request: request, cwd: cwd, sessionId: sessionId, commitSettings: commitSettings,
5697
6709
  showIgnored: showIgnored,
5698
6710
  onToggleIgnored: () => setShowIgnored((value) => !value),
5699
6711
  // Commit-and-push carries the confirm flag with it: the button already
@@ -5848,6 +6860,14 @@ window.__ModuleLoader__.load({
5848
6860
  .filter((action) => action !== undefined && action.available !== false)
5849
6861
  const railVisible = railOrdered.filter((action) => railConfig.hidden.indexOf(action.id) < 0)
5850
6862
 
6863
+ /* The rail is drawn from the row Config when the host serves it: a change
6864
+ made in the settings card must show up here without a reload. */
6865
+ useEffect(() => subscribeRailConfig(() => {
6866
+ setRailConfig(readRailConfig())
6867
+ setRailTick((value) => value + 1)
6868
+ }), [])
6869
+ void railTick
6870
+
5851
6871
  const applyRail = (next) => { setRailConfig(next); writeRailConfig(next) }
5852
6872
  const moveRailAction = (id, delta) => {
5853
6873
  const order = railConfig.order.slice()
@@ -5926,8 +6946,16 @@ window.__ModuleLoader__.load({
5926
6946
  key: 'settings',
5927
6947
  type: 'button',
5928
6948
  className: 'dig-rail-btn dig-rail-settings',
5929
- title: t('rail.settings'),
5930
- onClick: () => setRailSettings(true),
6949
+ 'data-action': 'settings',
6950
+ // On a host that serves the row Config this button opens the plugin's own
6951
+ // settings page (where the card below the description edits the rail);
6952
+ // on an older host it keeps the in-panel editor. Never both.
6953
+ title: railSettingsTarget() === 'page' ? t('rail.openSettings') : t('rail.settings'),
6954
+ onClick: () => {
6955
+ const target = railSettingsTarget()
6956
+ if (target === 'page') { openSettingsPage(); return }
6957
+ setRailSettings(true)
6958
+ },
5931
6959
  }, E(Icon, { name: 'settings', size: 15 })))
5932
6960
  return E('div', { className: vertical ? 'dig-rail' : 'dig-rail-row', ref: railHostRef }, children)
5933
6961
  }
@@ -6189,6 +7217,30 @@ window.__ModuleLoader__.load({
6189
7217
  // A panel dragged narrower than COMPACT_MAX_WIDTH (the compact chrome) is the
6190
7218
  // case where 'nowrap + ellipsis' stops being readable: the overlays clamp
6191
7219
  // their own width, and their text wraps inside that box instead of clipping.
7220
+ // The settings card (the host's plugin page renders it inside its own
7221
+ // section): one row per rail action, themed entirely by tokens.
7222
+ '.dig-settings{display:flex;flex-direction:column;gap:8px;min-width:0}',
7223
+ '.dig-settings-head{display:flex;flex-direction:column;gap:2px}',
7224
+ '.dig-settings-title{font-weight:600;color:var(--dsw-alias-label-primary)}',
7225
+ '.dig-settings-hint{color:var(--dsw-alias-label-tertiary);font-weight:400}',
7226
+ '.dig-settings-note{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:400}',
7227
+ '.dig-settings-note-hidden{display:none}',
7228
+ '.dig-settings-error{color:var(--dsw-alias-state-error-primary);font-weight:400}',
7229
+ '.dig-settings-sub{margin-top:6px;font-weight:600;color:var(--dsw-alias-label-primary)}',
7230
+ '.dig-settings-text{display:flex;flex-direction:column;gap:3px;min-width:0}',
7231
+ '.dig-settings-text-label{color:var(--dsw-alias-label-secondary);font-weight:400}',
7232
+ '.dig-settings-input{width:100%;box-sizing:border-box;font:inherit}',
7233
+ '.dig-commit-ai{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:4px}',
7234
+ '.dig-commit-ai-hint{color:var(--dsw-alias-label-tertiary);font-size:11px}',
7235
+ '.dig-commit-ai-hint-block{flex:1 0 100%;font-size:11px}',
7236
+ '.dig-commit-ai-note{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-size:11px;margin-bottom:4px;word-break:break-word}',
7237
+ '.dig-settings-list{display:flex;flex-direction:column;gap:2px;max-height:min(46vh,360px);overflow:auto;padding:2px;border:1px solid var(--dsw-alias-hairline,var(--dsw-alias-border-l1));border-radius:var(--dsw-radius-sm,8px)}',
7238
+ '.dig-settings-row{display:flex;align-items:center;gap:8px;padding:3px 6px;border-radius:calc(var(--dsw-radius-sm,8px) - 2px);cursor:pointer;min-width:0}',
7239
+ '.dig-settings-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
7240
+ '.dig-settings-check{flex:none;accent-color:var(--dsw-alias-brand-primary)}',
7241
+ '.dig-settings-glyph{display:inline-flex;flex:none;color:var(--dsw-alias-label-secondary)}',
7242
+ '.dig-settings-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}',
7243
+ '.dig-root-narrow .dig-settings-label{white-space:normal}',
6192
7244
  '.dig-root-narrow .dig-menu-item{white-space:normal}',
6193
7245
  '.dig-root-narrow .dig-toast-text{white-space:normal}',
6194
7246
  '.dig-toasts{position:absolute;right:8px;bottom:8px;display:flex;flex-direction:column;gap:6px;z-index:80;max-width:min(340px,92%)}',
@@ -6334,7 +7386,7 @@ window.__ModuleLoader__.load({
6334
7386
  '.dig-row:hover .dig-mini{opacity:1}',
6335
7387
  '.dig-mini:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
6336
7388
  '.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))}',
6337
- '.dig-commit-box-compact{flex-direction:row;align-items:center;gap:6px;padding:4px 6px}',
7389
+ '.dig-commit-box-compact{flex-direction:row;align-items:center;gap:6px;padding:4px 6px;flex-wrap:wrap}',
6338
7390
  '.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}',
6339
7391
  '.dig-textarea::placeholder{color:var(--dsw-alias-label-tertiary)}',
6340
7392
  '.dig-textarea:focus{border-color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary))}',
@@ -6415,7 +7467,16 @@ window.__ModuleLoader__.load({
6415
7467
  return typeof unsubscribe === 'function' ? unsubscribe : undefined
6416
7468
  }, [])
6417
7469
  void tick
6418
- return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock })
7470
+ /* Read at RENDER time (the service may arrive after this plugin's apply) and
7471
+ soft: a host without the plugin page simply keeps the in-panel editor. */
7472
+ let navigation
7473
+ try {
7474
+ navigation = props.ctx === undefined || props.ctx === null ? undefined : props.ctx.get('pluginNavigation')
7475
+ } catch (error) { navigation = undefined }
7476
+ const openSettings = navigation !== undefined && navigation !== null && typeof navigation.openBundle === 'function'
7477
+ ? () => { navigation.openBundle(SETTINGS_BUNDLE) }
7478
+ : null
7479
+ return E(Panel, { scope: props.scope, t: props.t, visible: props.visible, dock: props.dock, openSettings: openSettings })
6419
7480
  }
6420
7481
 
6421
7482
  function NativePanel(props) {
@@ -6555,6 +7616,59 @@ window.__ModuleLoader__.load({
6555
7616
  /* ctx.inject runs its callback straight away when the service is already
6556
7617
  there, so only a host without better-sidebar reaches the native seats. */
6557
7618
  if (!hostedByBetterSidebar) hostNatively()
7619
+
7620
+ /* ---- settings: the row Config is the rail's source of truth ---- */
7621
+ /* dsh >= 0.1.7 serves a plugin's row Config to the browser through
7622
+ ctx.configForms; from then on the settings card and the rail read that one
7623
+ document (and the legacy localStorage value is migrated once). A host
7624
+ without the service never fires this callback: railForm stays null and the
7625
+ localStorage home + in-panel editor keep working there. */
7626
+ ctx.inject(['configForms'], (formCtx) => {
7627
+ const forms = formCtx.get('configForms')
7628
+ if (forms === undefined || forms === null || typeof forms.get !== 'function') return
7629
+ let form
7630
+ try { form = forms.get(RAIL_NS) } catch (error) { void error }
7631
+ if (form === undefined || form === null || typeof form.getSnapshot !== 'function') return
7632
+ formCtx.effect(() => {
7633
+ railForm = form
7634
+ migrateRailConfig(form)
7635
+ /* The row Config is still 'loading' at bind time, so the migration is
7636
+ attempted here AND on every notification until it lands — it is
7637
+ idempotent (marker-gated, re-entrancy-guarded) and only stamps the
7638
+ marker once the Host accepted the write. */
7639
+ const unsubscribe = typeof form.subscribe === 'function' ? form.subscribe(() => { notifyRailConfig(); migrateRailConfig(form) }) : undefined
7640
+ notifyRailConfig()
7641
+ return () => {
7642
+ railForm = null
7643
+ if (typeof unsubscribe === 'function') unsubscribe()
7644
+ notifyRailConfig()
7645
+ }
7646
+ }, 'dsh-ide-git: row config')
7647
+ })
7648
+
7649
+ /* The settings card, on both official seats: the plugin's detail page
7650
+ (keyed by the PACKAGE name) and Settings → Plugins (keyed by the settings
7651
+ namespace = the loader row id). Both seats are optional — a host without
7652
+ the slots keeps whatever surface it has. */
7653
+ ctx.inject(['slots'], (slotCtx) => {
7654
+ const slots = slotCtx.get('slots')
7655
+ if (slots === undefined || slots === null || typeof slots.inject !== 'function') return
7656
+ slotCtx.effect(() => {
7657
+ const card = (cardProps) => E(RailSettingsCard, Object.assign({}, cardProps, { t: t }))
7658
+ const offBundle = slots.inject('plugins.bundle.config', () => slots.register(
7659
+ { name: 'plugins.bundle.config', key: SETTINGS_BUNDLE, locale: LOCALE_NS },
7660
+ card,
7661
+ ))
7662
+ const offItem = slots.inject('settings.plugin.item', () => slots.register(
7663
+ { name: 'settings.plugin.item', key: RAIL_NS, locale: LOCALE_NS },
7664
+ card,
7665
+ ))
7666
+ return () => {
7667
+ try { offBundle() } catch (error) { void error }
7668
+ try { offItem() } catch (error) { void error }
7669
+ }
7670
+ }, 'dsh-ide-git: settings card')
7671
+ })
6558
7672
  }
6559
7673
 
6560
7674
  return { name: 'dsh-ide-git', inject: [], apply }