dsh-ide-git 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dsh.plugin.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-ide-git",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "main": "./src/index.js",
5
5
  "description": "IDE 级 Git 操作面板:分支树 + 提交图谱 + 变更/暂存/提交 + 提交详情。装了 dsh-better-sidebar 时挂成它的原生 tab(右侧栏 + 底部工作台);没装时直接挂 DSH 原生右侧栏,单独安装即可用。JetBrains 风格 Git 工具窗口 for DeepSeek Harness.",
6
6
  "engines": {
7
- "dsh": ">=0.1.2-0"
7
+ "dsh": ">=0.1.6-alpha.2"
8
8
  },
9
9
  "contributes": {
10
10
  "tools": [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-ide-git",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
4
4
  "description": "DSH plugin: an IDE-grade Git tool window as a native dsh-better-sidebar tab — branch tree, commit graph, changes, commit details, JetBrains-style actions, in the right sidebar and the bottom panel. | DSH 插件:IDE 级 Git 工具窗口,以 dsh-better-sidebar 原生 Tab 挂载——分支树 / 提交图谱 / 变更与提交详情 / JetBrains 风格操作,右侧栏与底部面板双布局自适应。",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,7 +19,7 @@
19
19
  },
20
20
  "engines": {
21
21
  "node": ">=18",
22
- "dsh": ">=0.1.2-0"
22
+ "dsh": ">=0.1.6-alpha.2"
23
23
  },
24
24
  "dsh": {
25
25
  "bundle": {
@@ -66,7 +66,7 @@
66
66
  "license": "MIT",
67
67
  "peerDependencies": {
68
68
  "@deepseek-ai/cordis": "^4.0.1",
69
- "@deepseek-ai/dsh": ">=0.1.2-0",
69
+ "@deepseek-ai/dsh": ">=0.1.6-alpha.2",
70
70
  "dsh-better-sidebar": ">=0.12.0"
71
71
  },
72
72
  "peerDependenciesMeta": {
package/src/client.js CHANGED
@@ -167,8 +167,11 @@ window.__ModuleLoader__.load({
167
167
  'settings.commit.title': 'AI 提交信息',
168
168
  'settings.commit.model': '使用的模型',
169
169
  'settings.commit.modelHint': '留空 = 跟随当前会话(provider/model)',
170
+ 'settings.commit.modelFollow': '跟随当前会话',
170
171
  'settings.commit.reasoning': '思考强度',
171
- 'settings.commit.reasoningHint': '留空 = 模型默认',
172
+ 'settings.commit.reasoningHint': '留空 = 不思考',
173
+ 'settings.commit.reasoningDefault': '不思考(默认)',
174
+ 'settings.commit.reasoningFollowHint': '跟随会话时固定不思考;选择固定模型后可再选思考强度',
172
175
  'settings.commit.prompt': '补充提示词',
173
176
  'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超过 72 字符',
174
177
  'settings.commit.tooLong': '超过 {n} 个字符的部分不会保存',
@@ -352,8 +355,11 @@ window.__ModuleLoader__.load({
352
355
  'settings.commit.title': 'AI commit message',
353
356
  'settings.commit.model': 'Model',
354
357
  'settings.commit.modelHint': 'Empty = follow this session (provider/model)',
358
+ 'settings.commit.modelFollow': 'Follow the current session',
355
359
  'settings.commit.reasoning': 'Reasoning effort',
356
- 'settings.commit.reasoningHint': 'Empty = the model\'s default',
360
+ 'settings.commit.reasoningHint': 'Empty = no reasoning',
361
+ 'settings.commit.reasoningDefault': 'No reasoning (default)',
362
+ 'settings.commit.reasoningFollowHint': 'Following the session always uses no reasoning; pick a fixed model to choose an effort',
357
363
  'settings.commit.prompt': 'Extra instructions',
358
364
  'settings.commit.promptHint': 'e.g. Conventional Commits, Chinese subject, at most 72 characters',
359
365
  'settings.commit.tooLong': 'Only the first {n} characters are saved',
@@ -545,8 +551,11 @@ window.__ModuleLoader__.load({
545
551
  'settings.commit.title': 'AI 提交訊息',
546
552
  'settings.commit.model': '使用嘅模型',
547
553
  'settings.commit.modelHint': '留空 = 跟隨當前對話(provider/model)',
554
+ 'settings.commit.modelFollow': '跟隨當前對話',
548
555
  'settings.commit.reasoning': '思考強度',
549
- 'settings.commit.reasoningHint': '留空 = 模型預設',
556
+ 'settings.commit.reasoningHint': '留空 = 不思考',
557
+ 'settings.commit.reasoningDefault': '不思考(預設)',
558
+ 'settings.commit.reasoningFollowHint': '跟隨對話時固定不思考;選擇固定模型後可再選思考強度',
550
559
  'settings.commit.prompt': '補充提示詞',
551
560
  'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,唔超過 72 字',
552
561
  'settings.commit.tooLong': '超過 {n} 個字嘅部分唔會儲存',
@@ -730,8 +739,11 @@ window.__ModuleLoader__.load({
730
739
  'settings.commit.title': 'AI 提交訊息',
731
740
  'settings.commit.model': '使用的模型',
732
741
  'settings.commit.modelHint': '留空 = 跟隨目前工作階段(provider/model)',
742
+ 'settings.commit.modelFollow': '跟隨目前工作階段',
733
743
  'settings.commit.reasoning': '思考強度',
734
- 'settings.commit.reasoningHint': '留空 = 模型預設',
744
+ 'settings.commit.reasoningHint': '留空 = 不思考',
745
+ 'settings.commit.reasoningDefault': '不思考(預設)',
746
+ 'settings.commit.reasoningFollowHint': '跟隨工作階段時固定不思考;選擇固定模型後可再選思考強度',
735
747
  'settings.commit.prompt': '補充提示詞',
736
748
  'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超過 72 字元',
737
749
  'settings.commit.tooLong': '超過 {n} 個字元的部分不會儲存',
@@ -915,8 +927,11 @@ window.__ModuleLoader__.load({
915
927
  'settings.commit.title': 'AI 提交訊息',
916
928
  'settings.commit.model': '使用的模型',
917
929
  'settings.commit.modelHint': '留空 = 跟隨目前工作階段(provider/model)',
930
+ 'settings.commit.modelFollow': '跟隨目前工作階段',
918
931
  'settings.commit.reasoning': '思考強度',
919
- 'settings.commit.reasoningHint': '留空 = 模型預設',
932
+ 'settings.commit.reasoningHint': '留空 = 不思考',
933
+ 'settings.commit.reasoningDefault': '不思考(預設)',
934
+ 'settings.commit.reasoningFollowHint': '跟隨工作階段時固定不思考;選擇固定模型後可再選思考強度',
920
935
  'settings.commit.prompt': '補充提示詞',
921
936
  'settings.commit.promptHint': '例如:用 Conventional Commits,subject 用中文,不超過 72 字元',
922
937
  'settings.commit.tooLong': '超過 {n} 個字元的部分不會儲存',
@@ -1100,8 +1115,11 @@ window.__ModuleLoader__.load({
1100
1115
  'settings.commit.title': 'AI コミットメッセージ',
1101
1116
  'settings.commit.model': '使用するモデル',
1102
1117
  'settings.commit.modelHint': '空欄 = 現在のセッションに従う(provider/model)',
1118
+ 'settings.commit.modelFollow': '現在のセッションに従う',
1103
1119
  'settings.commit.reasoning': '思考の強度',
1104
- 'settings.commit.reasoningHint': '空欄 = モデルの既定値',
1120
+ 'settings.commit.reasoningHint': '空欄 = 思考なし',
1121
+ 'settings.commit.reasoningDefault': '思考なし(既定)',
1122
+ 'settings.commit.reasoningFollowHint': 'セッション追従では常に思考なし。固定モデルを選ぶと強度を選べます',
1105
1123
  'settings.commit.prompt': '追加の指示',
1106
1124
  'settings.commit.promptHint': '例:Conventional Commits、subject は日本語、72 文字以内',
1107
1125
  'settings.commit.tooLong': '{n} 文字を超える部分は保存されません',
@@ -1285,8 +1303,11 @@ window.__ModuleLoader__.load({
1285
1303
  'settings.commit.title': 'AI 커밋 메시지',
1286
1304
  'settings.commit.model': '사용할 모델',
1287
1305
  'settings.commit.modelHint': '비우면 현재 세션을 따릅니다(provider/model)',
1306
+ 'settings.commit.modelFollow': '현재 세션 따르기',
1288
1307
  'settings.commit.reasoning': '추론 강도',
1289
- 'settings.commit.reasoningHint': '비우면 모델 기본값',
1308
+ 'settings.commit.reasoningHint': '비워 두면 추론 없음',
1309
+ 'settings.commit.reasoningDefault': '추론 없음(기본값)',
1310
+ 'settings.commit.reasoningFollowHint': '세션을 따를 때는 항상 추론을 건너뜁니다. 고정 모델을 선택하면 강도를 고를 수 있습니다',
1290
1311
  'settings.commit.prompt': '추가 지시',
1291
1312
  'settings.commit.promptHint': '예: Conventional Commits, 제목은 한국어, 72자 이내',
1292
1313
  'settings.commit.tooLong': '{n}자를 넘는 부분은 저장되지 않습니다',
@@ -1470,8 +1491,11 @@ window.__ModuleLoader__.load({
1470
1491
  'settings.commit.title': 'KI-Commit-Nachricht',
1471
1492
  'settings.commit.model': 'Modell',
1472
1493
  'settings.commit.modelHint': 'Leer = dieser Sitzung folgen (provider/model)',
1494
+ 'settings.commit.modelFollow': 'Dieser Sitzung folgen',
1473
1495
  'settings.commit.reasoning': 'Denkaufwand',
1474
- 'settings.commit.reasoningHint': 'Leer = Standard des Modells',
1496
+ 'settings.commit.reasoningHint': 'Leer = kein Denken',
1497
+ 'settings.commit.reasoningDefault': 'Kein Denken (Standard)',
1498
+ 'settings.commit.reasoningFollowHint': 'Beim Folgen der Sitzung wird nie gedacht; wähle ein festes Modell, um eine Stufe zu wählen',
1475
1499
  'settings.commit.prompt': 'Zusätzliche Anweisungen',
1476
1500
  'settings.commit.promptHint': 'z. B. Conventional Commits, Betreff auf Deutsch, höchstens 72 Zeichen',
1477
1501
  'settings.commit.tooLong': 'Nur die ersten {n} Zeichen werden gespeichert',
@@ -1655,8 +1679,11 @@ window.__ModuleLoader__.load({
1655
1679
  'settings.commit.title': 'Message de commit par IA',
1656
1680
  'settings.commit.model': 'Modèle',
1657
1681
  'settings.commit.modelHint': 'Vide = suivre cette session (provider/model)',
1682
+ 'settings.commit.modelFollow': 'Suivre cette session',
1658
1683
  'settings.commit.reasoning': 'Effort de raisonnement',
1659
- 'settings.commit.reasoningHint': 'Vide = valeur par défaut du modèle',
1684
+ 'settings.commit.reasoningHint': 'Vide = sans raisonnement',
1685
+ 'settings.commit.reasoningDefault': 'Sans raisonnement (par défaut)',
1686
+ 'settings.commit.reasoningFollowHint': 'En suivant la session, jamais de raisonnement ; choisissez un modèle fixe pour en choisir l\'intensité',
1660
1687
  'settings.commit.prompt': 'Instructions supplémentaires',
1661
1688
  'settings.commit.promptHint': 'ex. Conventional Commits, sujet en français, 72 caractères maximum',
1662
1689
  'settings.commit.tooLong': 'Seuls les {n} premiers caractères sont enregistrés',
@@ -1840,8 +1867,11 @@ window.__ModuleLoader__.load({
1840
1867
  'settings.commit.title': 'Сообщение коммита от ИИ',
1841
1868
  'settings.commit.model': 'Модель',
1842
1869
  'settings.commit.modelHint': 'Пусто = как в этой сессии (provider/model)',
1870
+ 'settings.commit.modelFollow': 'Как в этой сессии',
1843
1871
  'settings.commit.reasoning': 'Усилие рассуждения',
1844
- 'settings.commit.reasoningHint': 'Пусто = по умолчанию у модели',
1872
+ 'settings.commit.reasoningHint': 'Пусто = без размышлений',
1873
+ 'settings.commit.reasoningDefault': 'Без размышлений (по умолчанию)',
1874
+ 'settings.commit.reasoningFollowHint': 'При следовании за сессией размышления всегда выключены; выберите фиксированную модель, чтобы выбрать уровень',
1845
1875
  'settings.commit.prompt': 'Дополнительные указания',
1846
1876
  'settings.commit.promptHint': 'напр. Conventional Commits, тема по-русски, до 72 символов',
1847
1877
  'settings.commit.tooLong': 'Сохраняются только первые {n} символов',
@@ -2025,8 +2055,11 @@ window.__ModuleLoader__.load({
2025
2055
  'settings.commit.title': 'Mensagem de commit por IA',
2026
2056
  'settings.commit.model': 'Modelo',
2027
2057
  'settings.commit.modelHint': 'Vazio = seguir esta sessão (provider/model)',
2058
+ 'settings.commit.modelFollow': 'Seguir esta sessão',
2028
2059
  'settings.commit.reasoning': 'Esforço de raciocínio',
2029
- 'settings.commit.reasoningHint': 'Vazio = padrão do modelo',
2060
+ 'settings.commit.reasoningHint': 'Vazio = sem raciocínio',
2061
+ 'settings.commit.reasoningDefault': 'Sem raciocínio (padrão)',
2062
+ 'settings.commit.reasoningFollowHint': 'Ao seguir a sessão, o raciocínio fica sempre desativado; escolha um modelo fixo para escolher o nível',
2030
2063
  'settings.commit.prompt': 'Instruções extras',
2031
2064
  'settings.commit.promptHint': 'ex.: Conventional Commits, assunto em português, até 72 caracteres',
2032
2065
  'settings.commit.tooLong': 'Só os primeiros {n} caracteres são salvos',
@@ -2210,8 +2243,11 @@ window.__ModuleLoader__.load({
2210
2243
  'settings.commit.title': 'Messaggio di commit con IA',
2211
2244
  'settings.commit.model': 'Modello',
2212
2245
  'settings.commit.modelHint': 'Vuoto = segui questa sessione (provider/model)',
2246
+ 'settings.commit.modelFollow': 'Segui questa sessione',
2213
2247
  'settings.commit.reasoning': 'Sforzo di ragionamento',
2214
- 'settings.commit.reasoningHint': 'Vuoto = predefinito del modello',
2248
+ 'settings.commit.reasoningHint': 'Vuoto = senza ragionamento',
2249
+ 'settings.commit.reasoningDefault': 'Senza ragionamento (predefinito)',
2250
+ 'settings.commit.reasoningFollowHint': 'Seguendo la sessione il ragionamento resta sempre spento; scegli un modello fisso per sceglierne il livello',
2215
2251
  'settings.commit.prompt': 'Istruzioni aggiuntive',
2216
2252
  'settings.commit.promptHint': 'es. Conventional Commits, oggetto in italiano, max 72 caratteri',
2217
2253
  'settings.commit.tooLong': 'Solo i primi {n} caratteri vengono salvati',
@@ -2395,8 +2431,11 @@ window.__ModuleLoader__.load({
2395
2431
  'settings.commit.title': 'AI-commitbericht',
2396
2432
  'settings.commit.model': 'Model',
2397
2433
  'settings.commit.modelHint': 'Leeg = deze sessie volgen (provider/model)',
2434
+ 'settings.commit.modelFollow': 'Deze sessie volgen',
2398
2435
  'settings.commit.reasoning': 'Redeneerinspanning',
2399
- 'settings.commit.reasoningHint': 'Leeg = standaard van het model',
2436
+ 'settings.commit.reasoningHint': 'Leeg = geen redeneren',
2437
+ 'settings.commit.reasoningDefault': 'Geen redeneren (standaard)',
2438
+ 'settings.commit.reasoningFollowHint': 'Bij het volgen van de sessie wordt nooit geredeneerd; kies een vast model om een niveau te kiezen',
2400
2439
  'settings.commit.prompt': 'Extra instructies',
2401
2440
  'settings.commit.promptHint': 'bv. Conventional Commits, onderwerp in het Nederlands, max. 72 tekens',
2402
2441
  'settings.commit.tooLong': 'Alleen de eerste {n} tekens worden opgeslagen',
@@ -2580,8 +2619,11 @@ window.__ModuleLoader__.load({
2580
2619
  'settings.commit.title': 'Wiadomość commita od AI',
2581
2620
  'settings.commit.model': 'Model',
2582
2621
  'settings.commit.modelHint': 'Puste = zgodnie z tą sesją (provider/model)',
2622
+ 'settings.commit.modelFollow': 'Zgodnie z tą sesją',
2583
2623
  'settings.commit.reasoning': 'Poziom rozumowania',
2584
- 'settings.commit.reasoningHint': 'Puste = domyślne modelu',
2624
+ 'settings.commit.reasoningHint': 'Puste = bez rozumowania',
2625
+ 'settings.commit.reasoningDefault': 'Bez rozumowania (domyślnie)',
2626
+ 'settings.commit.reasoningFollowHint': 'Podążanie za sesją zawsze pomija rozumowanie; wybierz stały model, aby wybrać poziom',
2585
2627
  'settings.commit.prompt': 'Dodatkowe instrukcje',
2586
2628
  'settings.commit.promptHint': 'np. Conventional Commits, temat po polsku, maks. 72 znaki',
2587
2629
  'settings.commit.tooLong': 'Zapisywane jest tylko pierwszych {n} znaków',
@@ -2765,8 +2807,11 @@ window.__ModuleLoader__.load({
2765
2807
  'settings.commit.title': 'AI-commitmeddelande',
2766
2808
  'settings.commit.model': 'Modell',
2767
2809
  'settings.commit.modelHint': 'Tomt = följ denna session (provider/model)',
2810
+ 'settings.commit.modelFollow': 'Följ denna session',
2768
2811
  'settings.commit.reasoning': 'Resonemangsinsats',
2769
- 'settings.commit.reasoningHint': 'Tomt = modellens standard',
2812
+ 'settings.commit.reasoningHint': 'Tomt = inget resonemang',
2813
+ 'settings.commit.reasoningDefault': 'Inget resonemang (standard)',
2814
+ 'settings.commit.reasoningFollowHint': 'När sessionen följs används aldrig resonemang; välj en fast modell för att välja en nivå',
2770
2815
  'settings.commit.prompt': 'Extra instruktioner',
2771
2816
  'settings.commit.promptHint': 't.ex. Conventional Commits, ämne på svenska, högst 72 tecken',
2772
2817
  'settings.commit.tooLong': 'Endast de första {n} tecknen sparas',
@@ -2950,8 +2995,11 @@ window.__ModuleLoader__.load({
2950
2995
  'settings.commit.title': 'AI commit mesajı',
2951
2996
  'settings.commit.model': 'Model',
2952
2997
  'settings.commit.modelHint': 'Boş = bu oturumu izle (provider/model)',
2998
+ 'settings.commit.modelFollow': 'Bu oturumu izle',
2953
2999
  'settings.commit.reasoning': 'Düşünme yoğunluğu',
2954
- 'settings.commit.reasoningHint': 'Boş = modelin varsayılanı',
3000
+ 'settings.commit.reasoningHint': 'Boş = akıl yürütme yok',
3001
+ 'settings.commit.reasoningDefault': 'Akıl yürütme yok (varsayılan)',
3002
+ 'settings.commit.reasoningFollowHint': 'Oturumu izlerken akıl yürütme her zaman kapalıdır; bir düzey seçmek için sabit bir model seçin',
2955
3003
  'settings.commit.prompt': 'Ek yönergeler',
2956
3004
  'settings.commit.promptHint': 'örn. Conventional Commits, konu Türkçe, en fazla 72 karakter',
2957
3005
  'settings.commit.tooLong': 'Yalnızca ilk {n} karakter kaydedilir',
@@ -3135,8 +3183,11 @@ window.__ModuleLoader__.load({
3135
3183
  'settings.commit.title': 'Pesan commit AI',
3136
3184
  'settings.commit.model': 'Model',
3137
3185
  'settings.commit.modelHint': 'Kosong = ikuti sesi ini (provider/model)',
3186
+ 'settings.commit.modelFollow': 'Ikuti sesi ini',
3138
3187
  'settings.commit.reasoning': 'Intensitas penalaran',
3139
- 'settings.commit.reasoningHint': 'Kosong = bawaan model',
3188
+ 'settings.commit.reasoningHint': 'Kosong = tanpa penalaran',
3189
+ 'settings.commit.reasoningDefault': 'Tanpa penalaran (bawaan)',
3190
+ 'settings.commit.reasoningFollowHint': 'Saat mengikuti sesi, penalaran selalu nonaktif; pilih model tetap untuk memilih tingkat',
3140
3191
  'settings.commit.prompt': 'Instruksi tambahan',
3141
3192
  'settings.commit.promptHint': 'mis. Conventional Commits, subjek bahasa Indonesia, maks 72 karakter',
3142
3193
  'settings.commit.tooLong': 'Hanya {n} karakter pertama yang disimpan',
@@ -3320,8 +3371,11 @@ window.__ModuleLoader__.load({
3320
3371
  'settings.commit.title': 'Thông điệp commit do AI viết',
3321
3372
  'settings.commit.model': 'Mô hình',
3322
3373
  'settings.commit.modelHint': 'Trống = theo phiên này (provider/model)',
3374
+ 'settings.commit.modelFollow': 'Theo phiên này',
3323
3375
  'settings.commit.reasoning': 'Mức suy luận',
3324
- 'settings.commit.reasoningHint': 'Trống = mặc định của mô hình',
3376
+ 'settings.commit.reasoningHint': 'Để trống = không suy luận',
3377
+ 'settings.commit.reasoningDefault': 'Không suy luận (mặc định)',
3378
+ 'settings.commit.reasoningFollowHint': 'Khi theo phiên, suy luận luôn tắt; chọn một mô hình cố định để chọn mức',
3325
3379
  'settings.commit.prompt': 'Chỉ dẫn bổ sung',
3326
3380
  'settings.commit.promptHint': 'ví dụ Conventional Commits, tiêu đề tiếng Việt, tối đa 72 ký tự',
3327
3381
  'settings.commit.tooLong': 'Chỉ {n} ký tự đầu được lưu',
@@ -3505,8 +3559,11 @@ window.__ModuleLoader__.load({
3505
3559
  'settings.commit.title': 'رسالة إيداع بالذكاء الاصطناعي',
3506
3560
  'settings.commit.model': 'النموذج',
3507
3561
  'settings.commit.modelHint': 'فارغ = اتبع هذه الجلسة (provider/model)',
3562
+ 'settings.commit.modelFollow': 'اتبع هذه الجلسة',
3508
3563
  'settings.commit.reasoning': 'مستوى التفكير',
3509
- 'settings.commit.reasoningHint': 'فارغ = الافتراضي للنموذج',
3564
+ 'settings.commit.reasoningHint': 'فارغ = بدون استدلال',
3565
+ 'settings.commit.reasoningDefault': 'بدون استدلال (افتراضي)',
3566
+ 'settings.commit.reasoningFollowHint': 'عند اتباع الجلسة يكون الاستدلال معطلًا دائمًا؛ اختر نموذجًا ثابتًا لاختيار المستوى',
3510
3567
  'settings.commit.prompt': 'تعليمات إضافية',
3511
3568
  'settings.commit.promptHint': 'مثال: Conventional Commits، والعنوان بالعربية، وبحد أقصى 72 حرفًا',
3512
3569
  'settings.commit.tooLong': 'يُحفظ أول {n} حرفًا فقط',
@@ -3690,8 +3747,11 @@ window.__ModuleLoader__.load({
3690
3747
  'settings.commit.title': 'AI कमिट संदेश',
3691
3748
  'settings.commit.model': 'मॉडल',
3692
3749
  'settings.commit.modelHint': 'खाली = इस सत्र का अनुसरण करें (provider/model)',
3750
+ 'settings.commit.modelFollow': 'इस सत्र का पालन करें',
3693
3751
  'settings.commit.reasoning': 'तर्क की तीव्रता',
3694
- 'settings.commit.reasoningHint': 'खाली = मॉडल का डिफ़ॉल्ट',
3752
+ 'settings.commit.reasoningHint': 'खाली = कोई तर्क नहीं',
3753
+ 'settings.commit.reasoningDefault': 'कोई तर्क नहीं (डिफ़ॉल्ट)',
3754
+ 'settings.commit.reasoningFollowHint': 'सत्र का पालन करते समय तर्क हमेशा बंद रहता है; स्तर चुनने के लिए एक निश्चित मॉडल चुनें',
3695
3755
  'settings.commit.prompt': 'अतिरिक्त निर्देश',
3696
3756
  'settings.commit.promptHint': 'जैसे Conventional Commits, विषय हिंदी में, अधिकतम 72 अक्षर',
3697
3757
  'settings.commit.tooLong': 'केवल पहले {n} अक्षर सहेजे जाते हैं',
@@ -3875,8 +3935,11 @@ window.__ModuleLoader__.load({
3875
3935
  'settings.commit.title': 'ข้อความคอมมิตโดย AI',
3876
3936
  'settings.commit.model': 'โมเดล',
3877
3937
  'settings.commit.modelHint': 'ว่าง = ตามเซสชันนี้ (provider/model)',
3938
+ 'settings.commit.modelFollow': 'ตามเซสชันนี้',
3878
3939
  'settings.commit.reasoning': 'ระดับการใช้เหตุผล',
3879
- 'settings.commit.reasoningHint': 'ว่าง = ค่าเริ่มต้นของโมเดล',
3940
+ 'settings.commit.reasoningHint': 'เว้นว่าง = ไม่ใช้เหตุผล',
3941
+ 'settings.commit.reasoningDefault': 'ไม่ใช้เหตุผล (ค่าเริ่มต้น)',
3942
+ 'settings.commit.reasoningFollowHint': 'เมื่อตามเซสชันจะไม่ใช้เหตุผลเสมอ เลือกโมเดลคงที่เพื่อเลือกระดับ',
3880
3943
  'settings.commit.prompt': 'คำสั่งเพิ่มเติม',
3881
3944
  'settings.commit.promptHint': 'เช่น Conventional Commits หัวข้อเป็นภาษาไทย ไม่เกิน 72 ตัวอักษร',
3882
3945
  'settings.commit.tooLong': 'บันทึกเพียง {n} ตัวอักษรแรก',
@@ -4045,6 +4108,48 @@ window.__ModuleLoader__.load({
4045
4108
 
4046
4109
  /* ============================== storage ============================== */
4047
4110
 
4111
+ /* ---- ai commit pickers (issue #7): the model catalog and per-model
4112
+ reasoning efforts, served by the host's commit-models / commit-efforts
4113
+ routes. Cached for a short window so the settings card can mount twice
4114
+ without asking twice; a failed or empty lookup degrades the row to its
4115
+ plain text field (the hand-typed value keeps working: routing accepts
4116
+ unlisted ids), never to an error. ---- */
4117
+ const PICKER_TTL_MS = 30_000
4118
+ let commitCatalogCache = null
4119
+ let commitCatalogFlight = null
4120
+ const commitEffortsCache = new Map()
4121
+
4122
+ function pickerProvidersOf(catalog) {
4123
+ if (catalog === null || typeof catalog !== 'object' || !Array.isArray(catalog.providers)) return null
4124
+ return catalog.providers
4125
+ }
4126
+
4127
+ async function fetchCommitCatalog() {
4128
+ if (commitCatalogCache !== null && Date.now() - commitCatalogCache.at < PICKER_TTL_MS) {
4129
+ return pickerProvidersOf(commitCatalogCache.value)
4130
+ }
4131
+ if (commitCatalogFlight !== null) return commitCatalogFlight
4132
+ commitCatalogFlight = request('commit-models', {})
4133
+ .then(
4134
+ (value) => {
4135
+ commitCatalogCache = { at: Date.now(), value: value }
4136
+ return pickerProvidersOf(value)
4137
+ },
4138
+ () => null,
4139
+ )
4140
+ commitCatalogFlight.then(() => { commitCatalogFlight = null }, () => { commitCatalogFlight = null })
4141
+ return commitCatalogFlight
4142
+ return commitCatalogFlight
4143
+ }
4144
+
4145
+ async function fetchCommitEfforts(model) {
4146
+ const hit = commitEffortsCache.get(model)
4147
+ if (hit !== undefined && Date.now() - hit.at < PICKER_TTL_MS) return hit.value
4148
+ const value = await request('commit-efforts', { model: model })
4149
+ commitEffortsCache.set(model, { at: Date.now(), value: value })
4150
+ return value
4151
+ }
4152
+
4048
4153
  function readRememberedRepo(sessionId) {
4049
4154
  try {
4050
4155
  const raw = window.localStorage.getItem(REPO_KEY)
@@ -4986,25 +5091,31 @@ window.__ModuleLoader__.load({
4986
5091
  list is drag-sortable and arrow-sortable (the arrows also work with a
4987
5092
  keyboard); visibility is a per-action toggle, not a delete, so a hidden
4988
5093
  action keeps its position. */
4989
- /* The plugin's settings card. It is the SAME surface on the plugin's detail
4990
- page (slot `plugins.bundle.config`, keyed by the PACKAGE name) and in
4991
- Settings → Plugins (slot `settings.plugin.item`, keyed by the settings
4992
- namespace = the loader row id), and it is the only editor when the host
4993
- serves the row Config — the in-panel editor is not reachable there.
5094
+ /* The plugin's settings card. It lives on the plugin's detail page (slot
5095
+ `plugins.bundle.config`, keyed by the PACKAGE name), and it is the only
5096
+ editor when the host serves the row Config — the in-panel editor is not
5097
+ reachable there. The legacy Settings → Plugins seat went away with the
5098
+ raised host floor.
4994
5099
 
4995
5100
  A host without that service still renders the card (the seat exists), so it
4996
5101
  says so instead of pretending to have settings. */
4997
5102
  function RailSettingsCard(props) {
4998
5103
  const t = props.t
4999
5104
  const [tick, setTick] = useState(0)
5000
- /* Collapse state of the card shell (the li card on the Settings → Plugins
5001
- seat). The plugin-page seat (props.view === 'page') renders flat. */
5105
+ /* Collapse state of the card shell (the li card on the list seat). The
5106
+ plugin-page seat (props.view === 'page') renders flat. */
5002
5107
  const [openCard, setOpenCard] = useState(false)
5003
5108
  const [pending, setPending] = useState({})
5004
5109
  const [note, setNote] = useState('')
5005
5110
  /* The three AI-commit settings are free text, so they are drafted locally
5006
5111
  and written on blur / Enter — one write per edit, never one per keystroke. */
5007
5112
  const [textDraft, setTextDraft] = useState({})
5113
+ /* Picker data (issue #7): the model catalog once per mount, and the
5114
+ reasoning efforts of whichever model the settings currently name.
5115
+ null = not answered yet; an empty array = answered with nothing. Both
5116
+ degrade the row to its plain text field, never to an error — a
5117
+ hand-typed value keeps working because routing accepts unlisted ids. */
5118
+ const [pickers, setPickers] = useState({ catalog: null, efforts: null })
5008
5119
  /* The user's latest intent per field, written SYNCHRONOUSLY. React state is
5009
5120
  not enough: two clicks inside one frame would both read the pre-click
5010
5121
  snapshot, so the second click recomputed the same target and the toggle
@@ -5073,6 +5184,32 @@ window.__ModuleLoader__.load({
5073
5184
  setTextDraft((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5074
5185
  })
5075
5186
  }
5187
+ /* A picker row writes on CHANGE (a select has no blur-edit cycle): the
5188
+ optimistic value lands in the same textDraft the text rows use, so the
5189
+ control does not snap back to the accepted document for the round trip
5190
+ the host needs to echo the write. */
5191
+ const commitSelectField = (field, value) => {
5192
+ if (readOnly === true) { setNote(t('settings.rail.readonly')); return }
5193
+ setTextDraft((now) => Object.assign({}, now, { [field]: value }))
5194
+ const outcome = writeRailFields([{ op: 'set', path: [field], value: value }], scope)
5195
+ if (outcome.sent !== true) {
5196
+ setTextDraft((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5197
+ setNote(t(outcome.reason === 'read-only' ? 'settings.rail.readonly' : 'settings.rail.writeFailed'))
5198
+ return
5199
+ }
5200
+ if (outcome.settled === undefined) return
5201
+ void outcome.settled.then((ok) => {
5202
+ if (ok !== true) {
5203
+ setTextDraft((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5204
+ setNote(t('settings.rail.writeFailed'))
5205
+ return
5206
+ }
5207
+ /* The accepted document now carries it: drop the draft so the control
5208
+ shows the Host's own value again — the same contract a text row's
5209
+ blur commit keeps. */
5210
+ setTextDraft((now) => { const copy = Object.assign({}, now); delete copy[field]; return copy })
5211
+ })
5212
+ }
5076
5213
  const textRow = (field, labelKey, hintKey, max, multiline) => {
5077
5214
  const accepted = values[field] === undefined || values[field] === null ? '' : String(values[field])
5078
5215
  const shown = Object.prototype.hasOwnProperty.call(textDraft, field) ? textDraft[field] : accepted
@@ -5090,6 +5227,21 @@ window.__ModuleLoader__.load({
5090
5227
  multiline === true ? E('textarea', Object.assign({ rows: 3 }, common)) : E('input', Object.assign({ type: 'text' }, common)),
5091
5228
  shown.length >= max ? E('div', { className: 'dig-settings-error' }, fill(t('settings.commit.tooLong'), { n: String(max) })) : null)
5092
5229
  }
5230
+ /* A picker row: same shell as a text row, but a <select>. `children` are
5231
+ the options (built by the caller); a stored value the list does not
5232
+ carry is injected as its own option, because a controlled select whose
5233
+ value matches no option renders blank and the setting would LOOK lost. */
5234
+ const selectRow = (field, labelKey, value, children, disabled, hint) => {
5235
+ const id = 'dig-' + field
5236
+ return E('div', { className: 'dig-settings-text', key: field },
5237
+ E('label', { id: id + '-label', className: 'dig-settings-text-label', htmlFor: id }, t(labelKey)),
5238
+ E('select', {
5239
+ id: id, className: 'dig-input dig-settings-input', value: value, disabled: disabled === true,
5240
+ 'aria-labelledby': id + '-label',
5241
+ onChange: (event) => commitSelectField(field, event.target.value),
5242
+ }, children),
5243
+ hint === null ? null : E('div', { className: 'dig-settings-subnote' }, hint))
5244
+ }
5093
5245
  const flush = () => {
5094
5246
  pendingFlush.current = null
5095
5247
  /* The pending TARGET per field, not "whatever the render-time intent
@@ -5192,6 +5344,39 @@ window.__ModuleLoader__.load({
5192
5344
  return changed ? next : current
5193
5345
  })
5194
5346
  })
5347
+ /* The picker catalog loads once per mount (cached module-wide); a rejected
5348
+ lookup answers null and the rows simply stay text fields. */
5349
+ useEffect(() => {
5350
+ let alive = true
5351
+ fetchCommitCatalog().then(
5352
+ (providers) => { if (alive === true) setPickers((now) => Object.assign({}, now, { catalog: providers })) },
5353
+ () => { if (alive === true) setPickers((now) => Object.assign({}, now, { catalog: null })) },
5354
+ )
5355
+ return () => { alive = false }
5356
+ }, [])
5357
+ /* The efforts list follows whichever model the settings name — the draft
5358
+ counts too, so the picker reacts to a fresh selection before the host
5359
+ has echoed the write. Following the session has no fixed model, so
5360
+ there is nothing to list (and nothing to ask). */
5361
+ const pickerModel = Object.prototype.hasOwnProperty.call(textDraft, 'commitModel') && typeof textDraft.commitModel === 'string'
5362
+ ? textDraft.commitModel
5363
+ : (values === null ? '' : (values.commitModel === undefined || values.commitModel === null ? '' : String(values.commitModel)))
5364
+ useEffect(() => {
5365
+ let alive = true
5366
+ if (pickerModel === '') {
5367
+ setPickers((now) => (now.efforts === null ? now : Object.assign({}, now, { efforts: null })))
5368
+ return () => { alive = false }
5369
+ }
5370
+ fetchCommitEfforts(pickerModel).then(
5371
+ (value) => {
5372
+ if (alive !== true) return
5373
+ const rows = value !== null && typeof value === 'object' && Array.isArray(value.efforts) ? value.efforts : []
5374
+ setPickers((now) => Object.assign({}, now, { efforts: rows }))
5375
+ },
5376
+ () => { if (alive === true) setPickers((now) => Object.assign({}, now, { efforts: null })) },
5377
+ )
5378
+ return () => { alive = false }
5379
+ }, [pickerModel])
5195
5380
  // Switches exist only where they can actually write: a host that does not
5196
5381
  // serve this entry's settings gets a sentence instead of dead controls.
5197
5382
  if (status !== 'ready' || values === null) {
@@ -5263,8 +5448,51 @@ window.__ModuleLoader__.load({
5263
5448
  E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t(spec.key)))
5264
5449
  })),
5265
5450
  E('div', { className: 'dig-settings-sub' }, t('settings.commit.title')),
5266
- textRow('commitModel', 'settings.commit.model', 'settings.commit.modelHint', 120, false),
5267
- textRow('commitReasoning', 'settings.commit.reasoning', 'settings.commit.reasoningHint', 60, false),
5451
+ (() => {
5452
+ /* The model row is a picker over the host's provider catalog; while
5453
+ the catalog has not answered (or cannot), the row stays the plain
5454
+ text field it always was. */
5455
+ const accepted = values.commitModel === undefined || values.commitModel === null ? '' : String(values.commitModel)
5456
+ const shown = Object.prototype.hasOwnProperty.call(textDraft, 'commitModel') && typeof textDraft.commitModel === 'string' ? textDraft.commitModel : accepted
5457
+ if (Array.isArray(pickers.catalog)) {
5458
+ const listed = pickers.catalog.some((provider) => Array.isArray(provider.models)
5459
+ && provider.models.some((model) => provider.id + '/' + model.id === shown))
5460
+ return selectRow('commitModel', 'settings.commit.model', shown, [
5461
+ E('option', { key: 'follow', value: '' }, t('settings.commit.modelFollow')),
5462
+ shown === '' || listed === true ? null : E('option', { key: 'kept', value: shown }, shown),
5463
+ ].concat(pickers.catalog.map((provider) => E('optgroup', {
5464
+ key: provider.id,
5465
+ label: provider.name === provider.id ? provider.id : provider.name + ' (' + provider.id + ')',
5466
+ }, (Array.isArray(provider.models) ? provider.models : []).map((model) => E('option', {
5467
+ key: model.id, value: provider.id + '/' + model.id,
5468
+ }, model.name === model.id ? model.id : model.name + ' — ' + model.id))))), false, null)
5469
+ }
5470
+ return textRow('commitModel', 'settings.commit.model', 'settings.commit.modelHint', 120, false)
5471
+ })(),
5472
+ (() => {
5473
+ /* The reasoning row follows the model row: following the session has
5474
+ no fixed model, so it is the fixed no-reasoning default (the host
5475
+ asks for "off" for us); a fixed model lists exactly the efforts
5476
+ that route accepts — hand-typing an effort id was issue #7's
5477
+ error-prone path. No list (yet/unavailable) keeps the text field. */
5478
+ if (pickerModel === '') {
5479
+ return selectRow('commitReasoning', 'settings.commit.reasoning', '', [
5480
+ E('option', { key: 'default', value: '' }, t('settings.commit.reasoningDefault')),
5481
+ ], true, t('settings.commit.reasoningFollowHint'))
5482
+ }
5483
+ if (pickers.efforts !== null && pickers.efforts.length > 0) {
5484
+ const accepted = values.commitReasoning === undefined || values.commitReasoning === null ? '' : String(values.commitReasoning)
5485
+ const shown = Object.prototype.hasOwnProperty.call(textDraft, 'commitReasoning') && typeof textDraft.commitReasoning === 'string' ? textDraft.commitReasoning : accepted
5486
+ const listed = pickers.efforts.some((row) => row.id === shown)
5487
+ return selectRow('commitReasoning', 'settings.commit.reasoning', shown, [
5488
+ E('option', { key: 'default', value: '' }, t('settings.commit.reasoningDefault')),
5489
+ shown === '' || listed === true ? null : E('option', { key: 'kept', value: shown }, shown),
5490
+ ].concat(pickers.efforts.map((row) => E('option', {
5491
+ key: row.id, value: row.id,
5492
+ }, row.name === row.id ? row.id : row.name + ' (' + row.id + ')'))), false, null)
5493
+ }
5494
+ return textRow('commitReasoning', 'settings.commit.reasoning', 'settings.commit.reasoningHint', 60, false)
5495
+ })(),
5268
5496
  textRow('commitPrompt', 'settings.commit.prompt', 'settings.commit.promptHint', 2000, true)))
5269
5497
  }
5270
5498
 
@@ -7307,6 +7535,7 @@ window.__ModuleLoader__.load({
7307
7535
  '.dig-settings-note{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:400}',
7308
7536
  '.dig-settings-note-hidden{display:none}',
7309
7537
  '.dig-settings-error{color:var(--dsw-alias-state-error-primary);font-weight:400}',
7538
+ '.dig-settings-subnote{color:var(--dsw-alias-label-tertiary);font-weight:400;font-size:12px}',
7310
7539
  '.dig-settings-sub{margin-top:6px;font-weight:600;color:var(--dsw-alias-label-primary)}',
7311
7540
  '.dig-settings-text{display:flex;flex-direction:column;gap:3px;min-width:0}',
7312
7541
  '.dig-settings-text-label{color:var(--dsw-alias-label-secondary);font-weight:400}',
@@ -7727,10 +7956,10 @@ window.__ModuleLoader__.load({
7727
7956
  }, 'dsh-ide-git: row config')
7728
7957
  })
7729
7958
 
7730
- /* The settings card, on both official seats: the plugin's detail page
7731
- (keyed by the PACKAGE name) and Settings → Plugins (keyed by the settings
7732
- namespace = the loader row id). Both seats are optional — a host without
7733
- the slots keeps whatever surface it has. */
7959
+ /* The settings card, on the plugin detail page (plugins.bundle.config,
7960
+ keyed by the PACKAGE name; dsh >= 0.1.6-alpha.2). The legacy settings-list
7961
+ seat served pre-alpha.2 hosts only and is gone with the raised floor. The
7962
+ seat is optional — a host without the slots keeps whatever surface it has. */
7734
7963
  ctx.inject(['slots'], (slotCtx) => {
7735
7964
  const slots = slotCtx.get('slots')
7736
7965
  if (slots === undefined || slots === null || typeof slots.inject !== 'function') return
@@ -7740,13 +7969,8 @@ window.__ModuleLoader__.load({
7740
7969
  { name: 'plugins.bundle.config', key: SETTINGS_BUNDLE, locale: LOCALE_NS },
7741
7970
  card,
7742
7971
  ))
7743
- const offItem = slots.inject('settings.plugin.item', () => slots.register(
7744
- { name: 'settings.plugin.item', key: RAIL_NS, locale: LOCALE_NS },
7745
- card,
7746
- ))
7747
7972
  return () => {
7748
7973
  try { offBundle() } catch (error) { void error }
7749
- try { offItem() } catch (error) { void error }
7750
7974
  }
7751
7975
  }, 'dsh-ide-git: settings card')
7752
7976
  })
package/src/index.js CHANGED
@@ -1315,12 +1315,14 @@ const COMMIT_BINARY = /\.(png|jpe?g|gif|webp|ico|pdf|zip|gz|tgz|bz2|xz|7z|jar|wa
1315
1315
  async function collectCommitStream(stream) {
1316
1316
  let text = ''
1317
1317
  let finish = null
1318
+ let sawReasoning = false
1318
1319
  for await (const chunk of stream) {
1319
1320
  if (chunk === null || chunk === undefined) continue
1320
1321
  if (chunk.type === 'text-delta' && typeof chunk.text === 'string') text += chunk.text
1322
+ else if (chunk.type === 'reasoning-delta') sawReasoning = true
1321
1323
  else if (chunk.type === 'finish') finish = chunk.reason
1322
1324
  }
1323
- return { text: text, finish: finish }
1325
+ return { text: text, finish: finish, sawReasoning: sawReasoning }
1324
1326
  }
1325
1327
 
1326
1328
  /** The change set, trimmed to a budget the model can actually be asked about. */
@@ -1433,6 +1435,89 @@ async function commitRouteOf(payload) {
1433
1435
  return { provider: provider, model: model, source: 'setting' }
1434
1436
  }
1435
1437
 
1438
+ /** The unset reasoning default: 'off' when this route offers it, '' otherwise.
1439
+ Failures stay silent on purpose — this is a preference, not a contract: an
1440
+ unreachable capability lookup must not turn a working model call into an
1441
+ error; it just leaves the effort to the adapter's own default. */
1442
+ async function quietOffEffortOf(route) {
1443
+ try {
1444
+ const llm = commitServices.llm
1445
+ if (typeof llm.resolveModelInfo !== 'function') return ''
1446
+ const info = await llm.resolveModelInfo(route.provider, route.model)
1447
+ const reasoning = info !== null && typeof info === 'object' && info.reasoning !== null && typeof info.reasoning === 'object' ? info.reasoning : null
1448
+ const efforts = reasoning !== null && Array.isArray(reasoning.efforts) ? reasoning.efforts : []
1449
+ return efforts.some((row) => row !== null && typeof row === 'object' && row.id === 'off') ? 'off' : ''
1450
+ } catch (error) {
1451
+ void error
1452
+ return ''
1453
+ }
1454
+ }
1455
+
1456
+ /** The settings card's model picker: every provider route this host serves and
1457
+ the models each one lists. The catalog is advisory (routing accepts
1458
+ unlisted ids, see commitRouteOf), so a stored hand-typed value stays valid
1459
+ even when it is not in this list. */
1460
+ async function commitModels() {
1461
+ if (commitServices === null) {
1462
+ throw new PanelError('no-host-service', 'this host does not serve llm + sessions, so there is no model catalog here', 501)
1463
+ }
1464
+ const providers = []
1465
+ for (const row of commitServices.llm.listProviders()) {
1466
+ /* One provider's discovery failing must not hide the others: its entry
1467
+ ships with an empty model list and the picker still works. */
1468
+ let models = []
1469
+ try { models = await commitServices.llm.listModels(row.id) } catch (error) { models = []; void error }
1470
+ providers.push({
1471
+ id: row.id,
1472
+ name: typeof row.name === 'string' && row.name !== '' ? row.name : row.id,
1473
+ models: models
1474
+ .filter((model) => model !== null && typeof model === 'object' && typeof model.id === 'string' && model.id !== '')
1475
+ .map((model) => ({ id: model.id, name: typeof model.name === 'string' && model.name !== '' ? model.name : model.id })),
1476
+ })
1477
+ }
1478
+ return { providers: providers }
1479
+ }
1480
+
1481
+ /** The settings card's reasoning picker: the efforts one exact provider/model
1482
+ route accepts. An unreachable capability lookup answers with an EMPTY list
1483
+ (the browser falls back to its text field) instead of an error — the picker
1484
+ is a convenience, not a gate. */
1485
+ async function commitEfforts(payload) {
1486
+ if (commitServices === null) {
1487
+ throw new PanelError('no-host-service', 'this host does not serve llm + sessions, so there are no reasoning efforts here', 501)
1488
+ }
1489
+ const wanted = typeof payload.model === 'string' ? payload.model.trim() : ''
1490
+ const slash = wanted.indexOf('/')
1491
+ if (slash <= 0 || slash === wanted.length - 1 || wanted.indexOf('/', slash + 1) >= 0) {
1492
+ throw badRequest('model must look like "provider/model"')
1493
+ }
1494
+ const provider = wanted.slice(0, slash)
1495
+ const model = wanted.slice(slash + 1)
1496
+ const known = commitServices.llm.listProviders().map((row) => row.id)
1497
+ if (known.indexOf(provider) < 0) {
1498
+ throw new PanelError('unknown-provider', 'unknown provider "' + provider + '"; this host serves: ' + known.join(', '))
1499
+ }
1500
+ if (typeof commitServices.llm.resolveModelInfo !== 'function') {
1501
+ return { provider: provider, model: model, efforts: [], defaultEffort: null }
1502
+ }
1503
+ try {
1504
+ const info = await commitServices.llm.resolveModelInfo(provider, model)
1505
+ const reasoning = info !== null && typeof info === 'object' && info.reasoning !== null && typeof info.reasoning === 'object' ? info.reasoning : null
1506
+ const efforts = reasoning !== null && Array.isArray(reasoning.efforts) ? reasoning.efforts : []
1507
+ return {
1508
+ provider: provider,
1509
+ model: model,
1510
+ efforts: efforts
1511
+ .filter((row) => row !== null && typeof row === 'object' && typeof row.id === 'string' && row.id !== '')
1512
+ .map((row) => ({ id: row.id, name: typeof row.name === 'string' && row.name !== '' ? row.name : row.id })),
1513
+ defaultEffort: reasoning !== null && typeof reasoning.defaultEffort === 'string' ? reasoning.defaultEffort : null,
1514
+ }
1515
+ } catch (error) {
1516
+ void error
1517
+ return { provider: provider, model: model, efforts: [], defaultEffort: null }
1518
+ }
1519
+ }
1520
+
1436
1521
  async function commitMessage(payload) {
1437
1522
  if (commitServices === null) {
1438
1523
  throw new PanelError('no-host-service', 'this host does not serve llm + sessions, so the message cannot be written here', 501)
@@ -1464,10 +1549,23 @@ async function commitMessage(payload) {
1464
1549
  model: route.model,
1465
1550
  system: spoken.system,
1466
1551
  messages: [{ role: 'user', content: [{ type: 'text', text: spoken.user }] }],
1467
- maxTokens: 512,
1468
1552
  temperature: 0.2,
1469
1553
  }
1470
- if (reasoning !== '') options.reasoningEffort = reasoning
1554
+ /* No maxTokens on purpose: omitted, the runtime materializes the selected
1555
+ model's own configured output cap (resolveModelInfo().defaultMaxTokens) —
1556
+ exactly what a session call gets. The hardcoded 512 here was half of
1557
+ issue #7: with thinking enabled, max_tokens is the budget for reasoning
1558
+ AND message together, so 512 died inside the reasoning phase before one
1559
+ word of the message existed. */
1560
+ /* The other half of issue #7: an UNSET effort is not a neutral "model
1561
+ default" — the DeepSeek adapter resolves "unspecified" to reasoning ON at
1562
+ "high". A commit message is a formatting task, so the unset default now
1563
+ asks for "off" whenever the route offers it. The id is read from the
1564
+ route's own efforts table (resolveModelInfo), never assumed: the runtime
1565
+ rejects an effort the route does not list, so a blind 'off' would trade
1566
+ one loud failure for another. The user's explicit choice always wins. */
1567
+ const effectiveReasoning = reasoning !== '' ? reasoning : await quietOffEffortOf(route)
1568
+ if (effectiveReasoning !== '') options.reasoningEffort = effectiveReasoning
1471
1569
  if (typeof payload.sessionId === 'string' && payload.sessionId !== '') options.sessionId = payload.sessionId
1472
1570
  const started = Date.now()
1473
1571
  const collected = await collectCommitStream(commitServices.llm.stream(options))
@@ -1480,6 +1578,12 @@ async function commitMessage(payload) {
1480
1578
  if (finish.kind === 'error') throw gatewayError('llm-error', failureTextOf(finish.failure))
1481
1579
  if (finish.kind === 'aborted') throw gatewayError('llm-aborted', 'the call was aborted: ' + failureTextOf(finish.failure))
1482
1580
  if (finish.kind !== 'stop') {
1581
+ /* The max-tokens + no-text + reasoning-delta shape is the exact signature
1582
+ of issue #7: the budget (reasoning included) ran out mid-thought. Say
1583
+ what happened and what to do, instead of a bare finish kind. */
1584
+ if (finish.kind === 'max-tokens' && collected.text.trim() === '' && collected.sawReasoning === true) {
1585
+ throw gatewayError('llm-unfinished', 'the model stopped with "max-tokens" before writing the message: its reasoning spent the whole output budget; lower the reasoning effort in the plugin settings or pick a lighter model')
1586
+ }
1483
1587
  throw gatewayError('llm-unfinished', 'the model stopped with "' + String(finish.kind) + '"; the message may be incomplete')
1484
1588
  }
1485
1589
  const message = cleanupCommitMessage(collected.text)
@@ -1489,7 +1593,7 @@ async function commitMessage(payload) {
1489
1593
  provider: route.provider,
1490
1594
  model: route.model,
1491
1595
  routeSource: route.source,
1492
- reasoningEffort: reasoning === '' ? null : reasoning,
1596
+ reasoningEffort: effectiveReasoning === '' ? null : effectiveReasoning,
1493
1597
  elapsedMs: Date.now() - started,
1494
1598
  input: { files: input.files.length, bytes: input.bytes, dropped: input.dropped, truncated: input.truncated },
1495
1599
  promptTruncated: asked.length > COMMIT_MAX_PROMPT_CHARS,
@@ -1502,6 +1606,8 @@ async function commitMessage(payload) {
1502
1606
  const METHODS = {
1503
1607
  version,
1504
1608
  'commit-message': commitMessage,
1609
+ 'commit-models': commitModels,
1610
+ 'commit-efforts': commitEfforts,
1505
1611
  repos,
1506
1612
  summary,
1507
1613
  branches,