charness 3.2.0 → 3.3.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.
Files changed (313) hide show
  1. package/.env.example +3 -6
  2. package/DEVELOPMENT.md +15 -0
  3. package/README.md +111 -38
  4. package/SETUP.md +121 -16
  5. package/dist/adapters/adapterEventRouting.d.ts +2 -1
  6. package/dist/adapters/adapterEventRouting.d.ts.map +1 -1
  7. package/dist/adapters/adapterEventRouting.js +3 -0
  8. package/dist/adapters/adapterEventRouting.js.map +1 -1
  9. package/dist/adapters/adapterNames.d.ts +13 -0
  10. package/dist/adapters/adapterNames.d.ts.map +1 -1
  11. package/dist/adapters/adapterNames.js +16 -0
  12. package/dist/adapters/adapterNames.js.map +1 -1
  13. package/dist/adapters/backendUnavailableError.d.ts +11 -0
  14. package/dist/adapters/backendUnavailableError.d.ts.map +1 -0
  15. package/dist/adapters/backendUnavailableError.js +19 -0
  16. package/dist/adapters/backendUnavailableError.js.map +1 -0
  17. package/dist/adapters/claudeCliAdapter.d.ts +29 -6
  18. package/dist/adapters/claudeCliAdapter.d.ts.map +1 -1
  19. package/dist/adapters/claudeCliAdapter.js +70 -16
  20. package/dist/adapters/claudeCliAdapter.js.map +1 -1
  21. package/dist/adapters/claudeJsonStreamAdapter.d.ts +36 -3
  22. package/dist/adapters/claudeJsonStreamAdapter.d.ts.map +1 -1
  23. package/dist/adapters/claudeJsonStreamAdapter.js +93 -30
  24. package/dist/adapters/claudeJsonStreamAdapter.js.map +1 -1
  25. package/dist/adapters/createAdapter.d.ts +3 -14
  26. package/dist/adapters/createAdapter.d.ts.map +1 -1
  27. package/dist/adapters/createAdapter.js +9 -19
  28. package/dist/adapters/createAdapter.js.map +1 -1
  29. package/dist/adapters/openCodeAdapter.d.ts +140 -12
  30. package/dist/adapters/openCodeAdapter.d.ts.map +1 -1
  31. package/dist/adapters/openCodeAdapter.js +385 -119
  32. package/dist/adapters/openCodeAdapter.js.map +1 -1
  33. package/dist/apiErrorRetry.d.ts +70 -6
  34. package/dist/apiErrorRetry.d.ts.map +1 -1
  35. package/dist/apiErrorRetry.js +312 -34
  36. package/dist/apiErrorRetry.js.map +1 -1
  37. package/dist/apiRetryKick.d.ts +26 -1
  38. package/dist/apiRetryKick.d.ts.map +1 -1
  39. package/dist/apiRetryKick.js +41 -2
  40. package/dist/apiRetryKick.js.map +1 -1
  41. package/dist/bot.d.ts +2 -1
  42. package/dist/bot.d.ts.map +1 -1
  43. package/dist/bot.js +736 -184
  44. package/dist/bot.js.map +1 -1
  45. package/dist/cli/claudeProfiles.d.ts +1 -1
  46. package/dist/cli/claudeProfiles.js +1 -1
  47. package/dist/cli/hot.d.ts +7 -1
  48. package/dist/cli/hot.d.ts.map +1 -1
  49. package/dist/cli/hot.js +29 -3
  50. package/dist/cli/hot.js.map +1 -1
  51. package/dist/configDirFile.d.ts +73 -0
  52. package/dist/configDirFile.d.ts.map +1 -0
  53. package/dist/configDirFile.js +216 -0
  54. package/dist/configDirFile.js.map +1 -0
  55. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts +2 -2
  56. package/dist/connectors/telegram/commands/autoContinueLimits.d.ts.map +1 -1
  57. package/dist/connectors/telegram/commands/autoContinueLimits.js +4 -4
  58. package/dist/connectors/telegram/commands/autoContinueLimits.js.map +1 -1
  59. package/dist/connectors/telegram/commands/backendSwitch.d.ts +25 -0
  60. package/dist/connectors/telegram/commands/backendSwitch.d.ts.map +1 -0
  61. package/dist/connectors/telegram/commands/backendSwitch.js +104 -0
  62. package/dist/connectors/telegram/commands/backendSwitch.js.map +1 -0
  63. package/dist/connectors/telegram/commands/callbackMessage.d.ts +12 -0
  64. package/dist/connectors/telegram/commands/callbackMessage.d.ts.map +1 -1
  65. package/dist/connectors/telegram/commands/callbackMessage.js +24 -0
  66. package/dist/connectors/telegram/commands/callbackMessage.js.map +1 -1
  67. package/dist/connectors/telegram/commands/callbackToast.d.ts +36 -0
  68. package/dist/connectors/telegram/commands/callbackToast.d.ts.map +1 -0
  69. package/dist/connectors/telegram/commands/callbackToast.js +83 -0
  70. package/dist/connectors/telegram/commands/callbackToast.js.map +1 -0
  71. package/dist/connectors/telegram/commands/claudeAccounts.d.ts +18 -0
  72. package/dist/connectors/telegram/commands/claudeAccounts.d.ts.map +1 -1
  73. package/dist/connectors/telegram/commands/claudeAccounts.js +220 -66
  74. package/dist/connectors/telegram/commands/claudeAccounts.js.map +1 -1
  75. package/dist/connectors/telegram/commands/displayModes.d.ts.map +1 -1
  76. package/dist/connectors/telegram/commands/displayModes.js +4 -2
  77. package/dist/connectors/telegram/commands/displayModes.js.map +1 -1
  78. package/dist/connectors/telegram/commands/fallbackChain.d.ts +5 -8
  79. package/dist/connectors/telegram/commands/fallbackChain.d.ts.map +1 -1
  80. package/dist/connectors/telegram/commands/fallbackChain.js +183 -193
  81. package/dist/connectors/telegram/commands/fallbackChain.js.map +1 -1
  82. package/dist/connectors/telegram/commands/limitsBoard.d.ts +40 -0
  83. package/dist/connectors/telegram/commands/limitsBoard.d.ts.map +1 -0
  84. package/dist/connectors/telegram/commands/limitsBoard.js +281 -0
  85. package/dist/connectors/telegram/commands/limitsBoard.js.map +1 -0
  86. package/dist/connectors/telegram/commands/modelPage.d.ts +130 -0
  87. package/dist/connectors/telegram/commands/modelPage.d.ts.map +1 -0
  88. package/dist/connectors/telegram/commands/modelPage.js +120 -0
  89. package/dist/connectors/telegram/commands/modelPage.js.map +1 -0
  90. package/dist/connectors/telegram/commands/modelProviders.d.ts +9 -1
  91. package/dist/connectors/telegram/commands/modelProviders.d.ts.map +1 -1
  92. package/dist/connectors/telegram/commands/modelProviders.js +203 -111
  93. package/dist/connectors/telegram/commands/modelProviders.js.map +1 -1
  94. package/dist/connectors/telegram/commands/stt.d.ts +32 -0
  95. package/dist/connectors/telegram/commands/stt.d.ts.map +1 -0
  96. package/dist/connectors/telegram/commands/stt.js +460 -0
  97. package/dist/connectors/telegram/commands/stt.js.map +1 -0
  98. package/dist/effortLevels.d.ts +57 -7
  99. package/dist/effortLevels.d.ts.map +1 -1
  100. package/dist/effortLevels.js +65 -9
  101. package/dist/effortLevels.js.map +1 -1
  102. package/dist/i18n/de.d.ts.map +1 -1
  103. package/dist/i18n/de.js +160 -17
  104. package/dist/i18n/de.js.map +1 -1
  105. package/dist/i18n/en.d.ts.map +1 -1
  106. package/dist/i18n/en.js +160 -17
  107. package/dist/i18n/en.js.map +1 -1
  108. package/dist/i18n/es.d.ts.map +1 -1
  109. package/dist/i18n/es.js +160 -17
  110. package/dist/i18n/es.js.map +1 -1
  111. package/dist/i18n/fr.d.ts.map +1 -1
  112. package/dist/i18n/fr.js +160 -17
  113. package/dist/i18n/fr.js.map +1 -1
  114. package/dist/i18n/hi.d.ts.map +1 -1
  115. package/dist/i18n/hi.js +160 -17
  116. package/dist/i18n/hi.js.map +1 -1
  117. package/dist/i18n/ja.d.ts.map +1 -1
  118. package/dist/i18n/ja.js +160 -17
  119. package/dist/i18n/ja.js.map +1 -1
  120. package/dist/i18n/ka.d.ts.map +1 -1
  121. package/dist/i18n/ka.js +160 -17
  122. package/dist/i18n/ka.js.map +1 -1
  123. package/dist/i18n/pt.d.ts.map +1 -1
  124. package/dist/i18n/pt.js +160 -17
  125. package/dist/i18n/pt.js.map +1 -1
  126. package/dist/i18n/ru.d.ts.map +1 -1
  127. package/dist/i18n/ru.js +160 -17
  128. package/dist/i18n/ru.js.map +1 -1
  129. package/dist/i18n/uk.d.ts.map +1 -1
  130. package/dist/i18n/uk.js +160 -17
  131. package/dist/i18n/uk.js.map +1 -1
  132. package/dist/i18n/uz.d.ts.map +1 -1
  133. package/dist/i18n/uz.js +160 -17
  134. package/dist/i18n/uz.js.map +1 -1
  135. package/dist/i18n/zh.d.ts.map +1 -1
  136. package/dist/i18n/zh.js +160 -17
  137. package/dist/i18n/zh.js.map +1 -1
  138. package/dist/installManager.d.ts +20 -4
  139. package/dist/installManager.d.ts.map +1 -1
  140. package/dist/installManager.js +82 -14
  141. package/dist/installManager.js.map +1 -1
  142. package/dist/instanceSettings.d.ts +52 -0
  143. package/dist/instanceSettings.d.ts.map +1 -0
  144. package/dist/instanceSettings.js +136 -0
  145. package/dist/instanceSettings.js.map +1 -0
  146. package/dist/pinnedStatus.d.ts +7 -1
  147. package/dist/pinnedStatus.d.ts.map +1 -1
  148. package/dist/pinnedStatus.js +1 -1
  149. package/dist/pinnedStatus.js.map +1 -1
  150. package/dist/requests/requestLedger.d.ts +8 -0
  151. package/dist/requests/requestLedger.d.ts.map +1 -1
  152. package/dist/requests/requestLedger.js +19 -1
  153. package/dist/requests/requestLedger.js.map +1 -1
  154. package/dist/scheduler/mcpSurface.js +4 -4
  155. package/dist/scheduler/mcpSurface.js.map +1 -1
  156. package/dist/shutdown.d.ts +14 -0
  157. package/dist/shutdown.d.ts.map +1 -1
  158. package/dist/shutdown.js +16 -0
  159. package/dist/shutdown.js.map +1 -1
  160. package/dist/state.d.ts +54 -2
  161. package/dist/state.d.ts.map +1 -1
  162. package/dist/state.js +62 -1
  163. package/dist/state.js.map +1 -1
  164. package/dist/stt/sttAutoInstall.d.ts +39 -0
  165. package/dist/stt/sttAutoInstall.d.ts.map +1 -0
  166. package/dist/stt/sttAutoInstall.js +158 -0
  167. package/dist/stt/sttAutoInstall.js.map +1 -0
  168. package/dist/stt/sttChildEnv.d.ts +11 -0
  169. package/dist/stt/sttChildEnv.d.ts.map +1 -0
  170. package/dist/stt/sttChildEnv.js +30 -0
  171. package/dist/stt/sttChildEnv.js.map +1 -0
  172. package/dist/stt/sttConfigEdit.d.ts +71 -0
  173. package/dist/stt/sttConfigEdit.d.ts.map +1 -0
  174. package/dist/stt/sttConfigEdit.js +144 -0
  175. package/dist/stt/sttConfigEdit.js.map +1 -0
  176. package/dist/stt/sttInstall.d.ts +147 -0
  177. package/dist/stt/sttInstall.d.ts.map +1 -0
  178. package/dist/stt/sttInstall.js +491 -0
  179. package/dist/stt/sttInstall.js.map +1 -0
  180. package/dist/stt/sttInstallEntry.d.ts +2 -0
  181. package/dist/stt/sttInstallEntry.d.ts.map +1 -0
  182. package/dist/stt/sttInstallEntry.js +31 -0
  183. package/dist/stt/sttInstallEntry.js.map +1 -0
  184. package/dist/stt/sttInstallReporter.d.ts +45 -0
  185. package/dist/stt/sttInstallReporter.d.ts.map +1 -0
  186. package/dist/stt/sttInstallReporter.js +57 -0
  187. package/dist/stt/sttInstallReporter.js.map +1 -0
  188. package/dist/stt/sttRuntime.d.ts +35 -0
  189. package/dist/stt/sttRuntime.d.ts.map +1 -0
  190. package/dist/stt/sttRuntime.js +73 -0
  191. package/dist/stt/sttRuntime.js.map +1 -0
  192. package/dist/stt/sttScreenPlan.d.ts +193 -0
  193. package/dist/stt/sttScreenPlan.d.ts.map +1 -0
  194. package/dist/stt/sttScreenPlan.js +541 -0
  195. package/dist/stt/sttScreenPlan.js.map +1 -0
  196. package/dist/stt/sttSettings.d.ts +201 -0
  197. package/dist/stt/sttSettings.d.ts.map +1 -0
  198. package/dist/stt/sttSettings.js +387 -0
  199. package/dist/stt/sttSettings.js.map +1 -0
  200. package/dist/stt/sttSidecarOwner.d.ts +115 -0
  201. package/dist/stt/sttSidecarOwner.d.ts.map +1 -0
  202. package/dist/stt/sttSidecarOwner.js +475 -0
  203. package/dist/stt/sttSidecarOwner.js.map +1 -0
  204. package/dist/stt/transcribeChain.d.ts +36 -0
  205. package/dist/stt/transcribeChain.d.ts.map +1 -0
  206. package/dist/stt/transcribeChain.js +42 -0
  207. package/dist/stt/transcribeChain.js.map +1 -0
  208. package/dist/types.d.ts +77 -4
  209. package/dist/types.d.ts.map +1 -1
  210. package/dist/utils/adapterSwitchSessionIds.d.ts +32 -0
  211. package/dist/utils/adapterSwitchSessionIds.d.ts.map +1 -0
  212. package/dist/utils/adapterSwitchSessionIds.js +44 -0
  213. package/dist/utils/adapterSwitchSessionIds.js.map +1 -0
  214. package/dist/utils/backendSwitchPlan.d.ts +63 -0
  215. package/dist/utils/backendSwitchPlan.d.ts.map +1 -0
  216. package/dist/utils/backendSwitchPlan.js +73 -0
  217. package/dist/utils/backendSwitchPlan.js.map +1 -0
  218. package/dist/utils/claudeAccountPickerPlan.d.ts +12 -1
  219. package/dist/utils/claudeAccountPickerPlan.d.ts.map +1 -1
  220. package/dist/utils/claudeAccountPickerPlan.js +20 -1
  221. package/dist/utils/claudeAccountPickerPlan.js.map +1 -1
  222. package/dist/utils/claudeTranscriptContent.d.ts +24 -0
  223. package/dist/utils/claudeTranscriptContent.d.ts.map +1 -0
  224. package/dist/utils/claudeTranscriptContent.js +107 -0
  225. package/dist/utils/claudeTranscriptContent.js.map +1 -0
  226. package/dist/utils/claudeUsage.d.ts +47 -8
  227. package/dist/utils/claudeUsage.d.ts.map +1 -1
  228. package/dist/utils/claudeUsage.js +76 -14
  229. package/dist/utils/claudeUsage.js.map +1 -1
  230. package/dist/utils/claudeUsageRefresher.d.ts +8 -1
  231. package/dist/utils/claudeUsageRefresher.d.ts.map +1 -1
  232. package/dist/utils/claudeUsageRefresher.js +9 -4
  233. package/dist/utils/claudeUsageRefresher.js.map +1 -1
  234. package/dist/utils/effortPickerPlan.d.ts +54 -0
  235. package/dist/utils/effortPickerPlan.d.ts.map +1 -0
  236. package/dist/utils/effortPickerPlan.js +56 -0
  237. package/dist/utils/effortPickerPlan.js.map +1 -0
  238. package/dist/utils/ensureSessionPlan.d.ts +20 -4
  239. package/dist/utils/ensureSessionPlan.d.ts.map +1 -1
  240. package/dist/utils/ensureSessionPlan.js +17 -4
  241. package/dist/utils/ensureSessionPlan.js.map +1 -1
  242. package/dist/utils/fallbackBridge.d.ts +6 -2
  243. package/dist/utils/fallbackBridge.d.ts.map +1 -1
  244. package/dist/utils/fallbackBridge.js +10 -3
  245. package/dist/utils/fallbackBridge.js.map +1 -1
  246. package/dist/utils/fallbackChain.d.ts +36 -0
  247. package/dist/utils/fallbackChain.d.ts.map +1 -1
  248. package/dist/utils/fallbackChain.js +65 -1
  249. package/dist/utils/fallbackChain.js.map +1 -1
  250. package/dist/utils/fallbackSwitchNotice.d.ts +41 -0
  251. package/dist/utils/fallbackSwitchNotice.d.ts.map +1 -0
  252. package/dist/utils/fallbackSwitchNotice.js +75 -0
  253. package/dist/utils/fallbackSwitchNotice.js.map +1 -0
  254. package/dist/utils/fileSendService.d.ts +33 -10
  255. package/dist/utils/fileSendService.d.ts.map +1 -1
  256. package/dist/utils/fileSendService.js +78 -27
  257. package/dist/utils/fileSendService.js.map +1 -1
  258. package/dist/utils/jsonStreamHost.d.ts +39 -0
  259. package/dist/utils/jsonStreamHost.d.ts.map +1 -1
  260. package/dist/utils/jsonStreamHost.js +60 -1
  261. package/dist/utils/jsonStreamHost.js.map +1 -1
  262. package/dist/utils/limitEpisodeRecovery.d.ts +16 -2
  263. package/dist/utils/limitEpisodeRecovery.d.ts.map +1 -1
  264. package/dist/utils/limitEpisodeRecovery.js +44 -3
  265. package/dist/utils/limitEpisodeRecovery.js.map +1 -1
  266. package/dist/utils/limitSettings.d.ts +25 -0
  267. package/dist/utils/limitSettings.d.ts.map +1 -0
  268. package/dist/utils/limitSettings.js +31 -0
  269. package/dist/utils/limitSettings.js.map +1 -0
  270. package/dist/utils/limitsBoardPlan.d.ts +52 -0
  271. package/dist/utils/limitsBoardPlan.d.ts.map +1 -0
  272. package/dist/utils/limitsBoardPlan.js +93 -0
  273. package/dist/utils/limitsBoardPlan.js.map +1 -0
  274. package/dist/utils/openCodeUsage.d.ts +93 -0
  275. package/dist/utils/openCodeUsage.d.ts.map +1 -0
  276. package/dist/utils/openCodeUsage.js +248 -0
  277. package/dist/utils/openCodeUsage.js.map +1 -0
  278. package/dist/utils/operatorSwitchPlan.d.ts +61 -0
  279. package/dist/utils/operatorSwitchPlan.d.ts.map +1 -0
  280. package/dist/utils/operatorSwitchPlan.js +32 -0
  281. package/dist/utils/operatorSwitchPlan.js.map +1 -0
  282. package/dist/utils/promptDelivery.d.ts +40 -0
  283. package/dist/utils/promptDelivery.d.ts.map +1 -1
  284. package/dist/utils/promptDelivery.js +31 -0
  285. package/dist/utils/promptDelivery.js.map +1 -1
  286. package/dist/utils/recvPreviewRedaction.d.ts +14 -4
  287. package/dist/utils/recvPreviewRedaction.d.ts.map +1 -1
  288. package/dist/utils/recvPreviewRedaction.js +19 -7
  289. package/dist/utils/recvPreviewRedaction.js.map +1 -1
  290. package/dist/utils/startupReadiness.d.ts +3 -3
  291. package/dist/utils/startupReadiness.d.ts.map +1 -1
  292. package/dist/utils/startupReadiness.js +2 -2
  293. package/dist/utils/startupReadiness.js.map +1 -1
  294. package/dist/utils/threadStatusReport.d.ts +3 -1
  295. package/dist/utils/threadStatusReport.d.ts.map +1 -1
  296. package/dist/utils/threadStatusReport.js +4 -3
  297. package/dist/utils/threadStatusReport.js.map +1 -1
  298. package/dist/utils/timezone.d.ts +23 -0
  299. package/dist/utils/timezone.d.ts.map +1 -1
  300. package/dist/utils/timezone.js +38 -0
  301. package/dist/utils/timezone.js.map +1 -1
  302. package/dist/utils/transcribeAudio.d.ts +25 -12
  303. package/dist/utils/transcribeAudio.d.ts.map +1 -1
  304. package/dist/utils/transcribeAudio.js +39 -21
  305. package/dist/utils/transcribeAudio.js.map +1 -1
  306. package/dist/utils/usageRefreshBudget.d.ts +9 -0
  307. package/dist/utils/usageRefreshBudget.d.ts.map +1 -0
  308. package/dist/utils/usageRefreshBudget.js +27 -0
  309. package/dist/utils/usageRefreshBudget.js.map +1 -0
  310. package/examples/.env.example +7 -3
  311. package/examples/docker-compose.yml +9 -7
  312. package/package.json +2 -1
  313. package/stt/gigaam_sidecar.py +800 -0
@@ -13,19 +13,21 @@ const telegraf_1 = require("telegraf");
13
13
  const createAdapter_1 = require("../../../adapters/createAdapter");
14
14
  const adapterNames_1 = require("../../../adapters/adapterNames");
15
15
  const sessionKey_1 = require("../../../sessionKey");
16
- const sessionKeyCodec_1 = require("../sessionKeyCodec");
17
16
  const openCodeAdapter_1 = require("../../../adapters/openCodeAdapter");
18
- const rateLimiter_1 = require("../../../rateLimiter");
19
17
  const i18n_1 = require("../../../i18n");
20
- const paginateList_1 = require("../../../utils/paginateList");
21
18
  const modelPickerPlan_1 = require("../../../utils/modelPickerPlan");
22
19
  const modelRecentPlan_1 = require("../../../utils/modelRecentPlan");
23
20
  const providerDisconnectPlan_1 = require("../../../utils/providerDisconnectPlan");
24
21
  const sendErrorClassifier_1 = require("../../../sendErrorClassifier");
25
22
  const modelSetReplyDecision_1 = require("../../../utils/modelSetReplyDecision");
26
23
  const effortLevels_1 = require("../../../effortLevels");
24
+ const configDirFile_1 = require("../../../configDirFile");
25
+ const instanceSettings_1 = require("../../../instanceSettings");
26
+ const effortPickerPlan_1 = require("../../../utils/effortPickerPlan");
27
27
  const openCodeAuthLogin_1 = require("../../../utils/openCodeAuthLogin");
28
28
  const callbackMessage_1 = require("./callbackMessage");
29
+ const modelPage_1 = require("./modelPage");
30
+ const callbackToast_1 = require("./callbackToast");
29
31
  const defaultConnectProviderId = 'openai';
30
32
  function checkLooksLikeProviderApiKey(value) {
31
33
  return value.startsWith('sk-');
@@ -61,15 +63,6 @@ function getProviderAuthAdapter() {
61
63
  // ═══════════════════════════════════════════════════════════════════════════════
62
64
  // /model — two-level provider → models picker
63
65
  // ═══════════════════════════════════════════════════════════════════════════════
64
- /**
65
- * @description Models per page in the `/model` model level. Model names are
66
- * long, so each gets a full-width row; 10 rows keep the keyboard scroll-free
67
- * and the numbered text list ~10 short lines — far from Telegram's 4096-char
68
- * message cap, which the old render-everything list blew past (12 990 chars on
69
- * a 367-model `openrouter` catalog → every `/model` died with "message is too
70
- * long" and the topic went silent).
71
- */
72
- const MODEL_PAGE_SIZE = 10;
73
66
  /** Model label the thread currently runs, or the localized "default" stand-in. */
74
67
  function getCurrentModelLabel(adapter, key) {
75
68
  return adapter.getCurrentModel?.(key) || (0, i18n_1.t)('model.current_default');
@@ -182,29 +175,21 @@ function buildModelPageRender(catalog, providerIndex, page, current, options) {
182
175
  if (provider === undefined || catalog.hiddenProviders.includes(provider))
183
176
  return null;
184
177
  const providerModels = catalog.byProvider.get(provider) ?? [];
185
- const { slice, currentPage, totalPages } = (0, paginateList_1.paginateList)(providerModels, page, MODEL_PAGE_SIZE);
186
- const firstIndexOnPage = currentPage * MODEL_PAGE_SIZE;
187
- const rows = slice.map((modelId, offset) => {
188
- const shortLabel = (0, modelPickerPlan_1.getModelShortLabel)(modelId, provider);
189
- return [
190
- telegraf_1.Markup.button.callback(modelId === current ? `${shortLabel} ✓` : shortLabel, (0, modelPickerPlan_1.buildModelPickCallback)(providerIndex, firstIndexOnPage + offset)),
191
- ];
178
+ const { rows, pageModels, currentPage, totalPages } = (0, modelPage_1.buildModelPageButtons)({
179
+ provider,
180
+ models: providerModels,
181
+ page,
182
+ current,
183
+ callbacks: {
184
+ buildPage: (targetPage) => (0, modelPickerPlan_1.buildProviderPageCallback)(providerIndex, targetPage),
185
+ buildPick: (modelIndex) => (0, modelPickerPlan_1.buildModelPickCallback)(providerIndex, modelIndex),
186
+ noop: modelPickerPlan_1.modelPickerNoopCallback,
187
+ // No back row when the provider level was skipped — it would lead nowhere.
188
+ back: (0, modelPickerPlan_1.checkHasProviderLevel)(catalog.visibleProviders.length, catalog.hiddenProviders.length)
189
+ ? { label: (0, i18n_1.t)('model.back_button'), data: modelPickerPlan_1.modelPickerBackCallback }
190
+ : null,
191
+ },
192
192
  });
193
- if (totalPages > 1) {
194
- const nav = [];
195
- if (currentPage > 0) {
196
- nav.push(telegraf_1.Markup.button.callback((0, i18n_1.t)('model.prev_button'), (0, modelPickerPlan_1.buildProviderPageCallback)(providerIndex, currentPage - 1)));
197
- }
198
- nav.push(telegraf_1.Markup.button.callback((0, i18n_1.t)('model.page_button', { page: currentPage + 1, totalPages }), modelPickerPlan_1.modelPickerNoopCallback));
199
- if (currentPage < totalPages - 1) {
200
- nav.push(telegraf_1.Markup.button.callback((0, i18n_1.t)('model.next_button'), (0, modelPickerPlan_1.buildProviderPageCallback)(providerIndex, currentPage + 1)));
201
- }
202
- rows.push(nav);
203
- }
204
- // No back row when the provider level was skipped — it would lead nowhere.
205
- if ((0, modelPickerPlan_1.checkHasProviderLevel)(catalog.visibleProviders.length, catalog.hiddenProviders.length)) {
206
- rows.push([telegraf_1.Markup.button.callback((0, i18n_1.t)('model.back_button'), modelPickerPlan_1.modelPickerBackCallback)]);
207
- }
208
193
  const header = (0, i18n_1.t)('model.page_header', {
209
194
  provider,
210
195
  count: providerModels.length,
@@ -213,14 +198,14 @@ function buildModelPageRender(catalog, providerIndex, page, current, options) {
213
198
  current,
214
199
  });
215
200
  const numberedList = options.isWithNumberedList
216
- ? slice.map((modelId, offset) => `${offset + 1}. ${(0, modelPickerPlan_1.getModelShortLabel)(modelId, provider)}`).join('\n')
201
+ ? pageModels.map((modelId, offset) => `${offset + 1}. ${(0, modelPickerPlan_1.getModelShortLabel)(modelId, provider)}`).join('\n')
217
202
  : '';
218
203
  const hint = options.isWithNumberedList ? (0, i18n_1.t)('model.page_hint') : (0, i18n_1.t)('model.page_hint_buttons_only');
219
204
  const text = [header, numberedList, hint].filter((part) => part.length > 0).join('\n\n');
220
205
  return {
221
206
  text,
222
207
  keyboard: telegraf_1.Markup.inlineKeyboard(rows),
223
- pageModels: slice,
208
+ pageModels,
224
209
  isNumberedPickArmed: options.isWithNumberedList,
225
210
  };
226
211
  }
@@ -240,17 +225,9 @@ async function editModelPickerMessage(ctx, render) {
240
225
  }
241
226
  }
242
227
  }
243
- /**
244
- * @description Build the `/effort` level picker keyboard.
245
- *
246
- * One callback button per available reasoning-effort level (3 per row);
247
- * the level matching `current` carries a `✓` marker. Shared by the
248
- * `/effort` command (initial render) and the `effort_<level>` callback
249
- * (re-render after a press) so the marker can never drift between the two.
250
- */
251
- function buildEffortKeyboard(levels, current) {
252
- const buttons = levels.map((l) => telegraf_1.Markup.button.callback(l === current ? `${l} ✓` : l, `effort_${l}`));
253
- return telegraf_1.Markup.inlineKeyboard(buttons, { columns: 3 });
228
+ /** @description The inline keyboard of an `/effort` picker plan (a thin Markup wrapper over its rows). */
229
+ function buildEffortKeyboard(picker) {
230
+ return telegraf_1.Markup.inlineKeyboard(picker.rows.map((row) => row.map((button) => telegraf_1.Markup.button.callback(button.text, button.data))));
254
231
  }
255
232
  /**
256
233
  * @description Build the model, effort and provider-credential flows over their ports. It returns what the bot's
@@ -258,7 +235,8 @@ function buildEffortKeyboard(levels, current) {
258
235
  * bot makes at the positions its neighbouring commands and buttons are registered.
259
236
  */
260
237
  function createModelProviders(ports) {
261
- const { awaitingModelSelection, awaitingSessionSelection, awaitingFolderName, threadModelLists, pendingProviderConnects, connectMethodLists, disconnectProviderLists, startOpenCodeOAuthLogin, bot, command, getState, replyToThread, deleteThreadMessage, authoriseContext, updatePinnedStatus, wakeSleepingSession } = ports;
238
+ const { awaitingModelSelection, awaitingSessionSelection, awaitingFolderName, threadModelLists, pendingProviderConnects, connectMethodLists, disconnectProviderLists, startOpenCodeOAuthLogin, getInstanceSettingsPath, bot, command, getState, replyToThread, editThreadMessage, deleteThreadMessage, authoriseContext, updatePinnedStatus, wakeSleepingSession, checkIsGeneral } = ports;
239
+ const answerWithWholeText = (0, callbackToast_1.createWholeTextAnswerer)(replyToThread);
262
240
  /**
263
241
  * @description Single choke point for the four `/model`-set paths (the
264
242
  * `/model <num>` and `/model <name>` commands, the text-handler numeric pick,
@@ -280,8 +258,8 @@ function createModelProviders(ports) {
280
258
  // Read AFTER the switch: a model that does not offer the level in force
281
259
  // clears it (`effort.cleared_on_model_switch`), so a pre-switch read would
282
260
  // report a level the new model no longer runs. Same resolution rule as
283
- // `/status` — no `getEffort` means the backend has no effort concept at all.
284
- const effort = adapter.getEffort ? (adapter.getEffort(key) ?? effortLevels_1.defaultEffortLevel) : null;
261
+ // `/status` — no `getEffortState` means the backend has no effort concept at all.
262
+ const effort = (0, effortLevels_1.getThreadEffortView)(adapter, key)?.level ?? null;
285
263
  const decision = (0, modelSetReplyDecision_1.getModelSetReplyDecision)({
286
264
  hasSetModel: Boolean(adapter.setModel),
287
265
  setModelError,
@@ -607,6 +585,117 @@ function createModelProviders(ports) {
607
585
  await applyProviderDisconnect(key, providerId);
608
586
  });
609
587
  }
588
+ /**
589
+ * @description How a regular topic's effort reads against the levels its backend offers: the level its running
590
+ * session uses (else the one its next start will), the next start's level when that differs, and what General's
591
+ * value really is for it. OpenCode offers the model's variants only: a level it lacks runs as the nearest it has,
592
+ * so every level shown is that one.
593
+ */
594
+ function getTopicEffortAmongLevels(key, adapter, levels) {
595
+ const instanceDefault = (0, effortLevels_1.getEffortAmongLevels)((0, effortLevels_1.getInstanceDefaultEffort)(), levels);
596
+ const view = (0, effortLevels_1.getThreadEffortView)(adapter, key);
597
+ const inForce = view ? (0, effortLevels_1.getEffortAmongLevels)(view.level, levels) : instanceDefault;
598
+ const nextStartLevel = view?.nextStartLevel ? (0, effortLevels_1.getEffortAmongLevels)(view.nextStartLevel, levels) : null;
599
+ return { instanceDefault, inForce, nextStartLevel: nextStartLevel === inForce ? null : nextStartLevel };
600
+ }
601
+ /** @description The levels `adapter` offers the thread; `[]` when it offers none (or cannot say). */
602
+ async function listTopicEffortLevels(key, adapter) {
603
+ try {
604
+ return (await adapter.getAvailableEffortLevels?.(key)) ?? [];
605
+ }
606
+ catch (e) {
607
+ console.error('[Bot] getAvailableEffortLevels:', e);
608
+ return [];
609
+ }
610
+ }
611
+ /**
612
+ * @description The `/effort` picker of a regular topic: the level its session uses, where the next start's level
613
+ * comes from (its own pick or General's value), and a button per level the backend offers. `null` when the backend
614
+ * offers none.
615
+ */
616
+ async function buildTopicEffortPicker(key, adapter) {
617
+ const levels = await listTopicEffortLevels(key, adapter);
618
+ if (levels.length === 0)
619
+ return null;
620
+ return (0, effortPickerPlan_1.buildEffortPicker)({
621
+ isGeneral: false,
622
+ levels,
623
+ ...getTopicEffortAmongLevels(key, adapter, levels),
624
+ isOwnPick: (adapter.getOwnEffort?.(key) ?? null) !== null,
625
+ canClear: Boolean(adapter.clearEffort),
626
+ }, i18n_1.t);
627
+ }
628
+ /** @description The `/effort` picker as it stands now, for the screen of `key`; `null` when there is nothing to pick. */
629
+ async function buildEffortPickerFor(key) {
630
+ if (checkIsGeneral(key)) {
631
+ return (0, effortPickerPlan_1.buildEffortPicker)({ isGeneral: true, levels: effortLevels_1.claudeEffortLevels, instanceDefault: (0, effortLevels_1.getInstanceDefaultEffort)() }, i18n_1.t);
632
+ }
633
+ return buildTopicEffortPicker(key, (0, createAdapter_1.getThreadAdapter)(key));
634
+ }
635
+ /**
636
+ * @description Re-render the picker a tap came from, text and buttons: the ✓ AND the level named in the text
637
+ * (and its source) follow the new state instead of staying on the previous one. "Message is not modified" (the
638
+ * same tap twice) is swallowed by the shared edit helper.
639
+ */
640
+ async function rerenderEffortPicker(ctx, key) {
641
+ const messageId = (0, callbackMessage_1.getCallbackMessageId)(ctx);
642
+ if (messageId === null)
643
+ return;
644
+ const picker = await buildEffortPickerFor(key);
645
+ if (!picker)
646
+ return;
647
+ await editThreadMessage(key, messageId, picker.text, buildEffortKeyboard(picker));
648
+ }
649
+ /**
650
+ * @description Write the instance default effort (`settings.json`) on top of what the file already holds.
651
+ * Returns `null` on success, else the errno code of the failed write — logged here with the file's path; the
652
+ * caller words it for the user.
653
+ */
654
+ function saveInstanceDefaultEffort(level) {
655
+ const filePath = getInstanceSettingsPath();
656
+ try {
657
+ (0, instanceSettings_1.saveInstanceSettings)(filePath, { ...(0, instanceSettings_1.loadInstanceSettingsForUpdate)(filePath), defaultEffort: level });
658
+ return null;
659
+ }
660
+ catch (e) {
661
+ const code = (0, configDirFile_1.getCaughtErrnoCode)(e);
662
+ console.warn(`[Settings] could not write ${filePath} (${code})`);
663
+ return code;
664
+ }
665
+ }
666
+ /** @description `/effort` in General: the picker of the instance default, or `/effort <level>` to set it. */
667
+ async function handleGeneralEffortCommand(key, args) {
668
+ if (!args) {
669
+ const picker = await buildEffortPickerFor(key);
670
+ if (picker)
671
+ await replyToThread(key, picker.text, buildEffortKeyboard(picker));
672
+ return;
673
+ }
674
+ if (!(0, effortLevels_1.checkIsClaudeEffortLevel)(args)) {
675
+ await replyToThread(key, (0, i18n_1.t)('effort.invalid_level', { level: args, valid: effortLevels_1.claudeEffortLevels.join(', ') }));
676
+ return;
677
+ }
678
+ const writeCode = saveInstanceDefaultEffort(args);
679
+ await replyToThread(key, writeCode === null
680
+ ? (0, i18n_1.t)('effort.general.setConfirmation', { level: args })
681
+ : (0, i18n_1.t)('effort.general.writeFailedNotice', { path: getInstanceSettingsPath(), code: writeCode }));
682
+ }
683
+ /** @description A level button in General: save it as the instance default, confirm, and re-render the picker. */
684
+ async function handleGeneralEffortTap(ctx, key, level) {
685
+ if (!(0, effortLevels_1.checkIsClaudeEffortLevel)(level)) {
686
+ await ctx.answerCbQuery((0, i18n_1.t)('cb.effort_error', { error: level }));
687
+ return;
688
+ }
689
+ const writeCode = saveInstanceDefaultEffort(level);
690
+ if (writeCode !== null) {
691
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.effort_error', { error: writeCode }));
692
+ await replyToThread(key, (0, i18n_1.t)('effort.general.writeFailedNotice', { path: getInstanceSettingsPath(), code: writeCode }));
693
+ return;
694
+ }
695
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.effort_set', { level }));
696
+ await replyToThread(key, (0, i18n_1.t)('effort.general.setConfirmation', { level }));
697
+ await rerenderEffortPicker(ctx, key);
698
+ }
610
699
  function registerModelCommands() {
611
700
  command('model', async (_ctx, key, parsed) => {
612
701
  const adapter = (0, createAdapter_1.getThreadAdapter)(key);
@@ -627,8 +716,13 @@ function createModelProviders(ports) {
627
716
  await showModelPicker(key, adapter);
628
717
  });
629
718
  command('effort', async (_ctx, key, parsed) => {
630
- const adapter = (0, createAdapter_1.getThreadAdapter)(key);
631
719
  const args = parsed.argsText;
720
+ // General sets the instance default, which needs no backend at all.
721
+ if (checkIsGeneral(key)) {
722
+ await handleGeneralEffortCommand(key, args);
723
+ return;
724
+ }
725
+ const adapter = (0, createAdapter_1.getThreadAdapter)(key);
632
726
  // Backend must support the effort contract (both methods are optional).
633
727
  if (!adapter.setEffort || !adapter.getAvailableEffortLevels) {
634
728
  await replyToThread(key, (0, i18n_1.t)('effort.unsupported_backend', { label: adapter.label }));
@@ -645,32 +739,23 @@ function createModelProviders(ports) {
645
739
  const wakeNotice = await wakeSleepingSession(key); // L-D4: apply to the woken session
646
740
  if (wakeNotice)
647
741
  await replyToThread(key, wakeNotice);
648
- const err = await adapter.setEffort(key, args);
649
- if (err) {
650
- await replyToThread(key, err);
651
- }
652
- else {
653
- await replyToThread(key, (0, i18n_1.t)('effort.set_success', { level: args }));
742
+ const outcome = await adapter.setEffort(key, args);
743
+ await replyToThread(key, outcome.status === 'applied' ? (0, i18n_1.t)('effort.set_success', { level: args }) : outcome.status === 'stored' ? outcome.notice : outcome.reason);
744
+ // A pick the adapter stored but could not apply yet (no agent running) is a pick all the same: the banner
745
+ // follows it. Only a refusal leaves the banner as it was.
746
+ if (outcome.status !== 'refused')
654
747
  await updatePinnedStatus(key).catch(() => { });
655
- }
656
748
  return;
657
749
  }
658
- // No arg: show current effort + a button per available level.
659
- let levels = [];
660
- try {
661
- levels = await adapter.getAvailableEffortLevels(key);
662
- }
663
- catch (e) {
664
- console.error('[Bot] getAvailableEffortLevels:', e);
665
- }
666
- if (levels.length === 0) {
750
+ // No arg: the level in force, where it comes from, and a button per available level.
751
+ const picker = await buildTopicEffortPicker(key, adapter);
752
+ if (!picker) {
667
753
  // Empty means the current model declares no variants (OpenCode) or the
668
754
  // backend reports no levels — not an error, just nothing to pick.
669
755
  await replyToThread(key, (0, i18n_1.t)('effort.not_available'));
670
756
  return;
671
757
  }
672
- const cur = adapter.getEffort?.(key) ?? null;
673
- await replyToThread(key, (0, i18n_1.t)('effort.choose', { current: cur ?? (0, i18n_1.t)('effort.current_none') }), buildEffortKeyboard(levels, cur));
758
+ await replyToThread(key, picker.text, buildEffortKeyboard(picker));
674
759
  });
675
760
  }
676
761
  function registerModelCallbacks() {
@@ -724,14 +809,14 @@ function createModelProviders(ports) {
724
809
  }
725
810
  const { isOk, message, setModelError, displayLabel } = await applyModelSelection(adapter, key, modelId);
726
811
  if (!isOk) {
727
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_error', { error: (setModelError ?? message).slice(0, 50) }));
812
+ await answerWithWholeText(ctx, key, (0, i18n_1.t)('cb.model_error', { error: setModelError ?? message }));
728
813
  return;
729
814
  }
730
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
815
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
731
816
  // The pick is made, so the page's bare-digit affordance must go (an armed
732
817
  // thread swallows a later ordinary "3" prompt) — and with it the numbers in
733
818
  // the text. Re-render the SAME page so the ✓ moves to the model just picked.
734
- const refreshed = buildModelPageRender(catalog, providerIndex, Math.floor(modelIndex / MODEL_PAGE_SIZE), getCurrentModelLabel(adapter, key), { isWithNumberedList: false });
819
+ const refreshed = buildModelPageRender(catalog, providerIndex, Math.floor(modelIndex / modelPage_1.modelPageSize), getCurrentModelLabel(adapter, key), { isWithNumberedList: false });
735
820
  if (refreshed) {
736
821
  applyModelPagePickArming(key, refreshed);
737
822
  await editModelPickerMessage(ctx, refreshed);
@@ -775,10 +860,10 @@ function createModelProviders(ports) {
775
860
  }
776
861
  const { isOk, message, setModelError, displayLabel } = await applyModelSelection(adapter, key, modelId);
777
862
  if (!isOk) {
778
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_error', { error: (setModelError ?? message).slice(0, 50) }));
863
+ await answerWithWholeText(ctx, key, (0, i18n_1.t)('cb.model_error', { error: setModelError ?? message }));
779
864
  return;
780
865
  }
781
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
866
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
782
867
  // Re-render the SAME level: the pick just moved to the head and takes the ✓.
783
868
  await refreshRecentModelsLevel(ctx, key);
784
869
  await replyToThread(key, message);
@@ -799,7 +884,7 @@ function createModelProviders(ports) {
799
884
  return;
800
885
  }
801
886
  await getState().setModelProviderHidden(provider, true);
802
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_hidden', { provider }));
887
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.model_hidden', { provider }));
803
888
  await refreshModelProviderLevel(ctx, key);
804
889
  });
805
890
  bot.action(modelPickerPlan_1.providerShowCallbackRe, async (ctx) => {
@@ -815,7 +900,7 @@ function createModelProviders(ports) {
815
900
  return;
816
901
  }
817
902
  await getState().setModelProviderHidden(provider, false);
818
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_shown', { provider }));
903
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.model_shown', { provider }));
819
904
  await refreshModelProviderLevel(ctx, key);
820
905
  });
821
906
  // «⬅️ providers» — back to the provider level.
@@ -867,10 +952,10 @@ function createModelProviders(ports) {
867
952
  }
868
953
  const { isOk, message, setModelError, displayLabel } = await applyModelSelection(adapter, key, modelId);
869
954
  if (!isOk) {
870
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_error', { error: (setModelError ?? message).slice(0, 50) }));
955
+ await answerWithWholeText(ctx, key, (0, i18n_1.t)('cb.model_error', { error: setModelError ?? message }));
871
956
  return;
872
957
  }
873
- await ctx.answerCbQuery((0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
958
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.model_set', { model: displayLabel.split('/').pop() || displayLabel }));
874
959
  await replyToThread(key, message);
875
960
  });
876
961
  bot.action(/^connm_(\d+)$/, async (ctx) => {
@@ -896,17 +981,21 @@ function createModelProviders(ports) {
896
981
  armProviderConnect(key, entry.providerId);
897
982
  await replyToThread(key, (0, i18n_1.t)('connect.prompt_key', { provider: entry.providerId }));
898
983
  });
899
- bot.action(/^effort_(.+)$/, async (ctx) => {
984
+ bot.action(effortPickerPlan_1.effortLevelCallbackRe, async (ctx) => {
900
985
  const key = await authoriseContext(ctx);
901
986
  if (!key) {
902
987
  await ctx.answerCbQuery((0, i18n_1.t)('cb.access_denied'));
903
988
  return;
904
989
  }
905
990
  const level = ctx.match[1];
991
+ if (checkIsGeneral(key)) {
992
+ await handleGeneralEffortTap(ctx, key, level);
993
+ return;
994
+ }
906
995
  const adapter = (0, createAdapter_1.getThreadAdapter)(key);
907
996
  // No `checkIsActive` gate — effort is persisted pre-session (mirrors the
908
- // `/effort` command and `/model`). The adapter returns its own notice if the
909
- // pick can't apply live; we surface it via `cb.effort_error` below.
997
+ // `/effort` command and `/model`). The adapter returns its own notice when the
998
+ // pick can't apply live; it is shown whole (see below).
910
999
  if (!adapter.setEffort) {
911
1000
  await ctx.answerCbQuery((0, i18n_1.t)('cb.not_supported', { label: adapter.label }));
912
1001
  return;
@@ -914,40 +1003,42 @@ function createModelProviders(ports) {
914
1003
  const wakeNotice = await wakeSleepingSession(key); // L-D4: apply to the woken session
915
1004
  if (wakeNotice)
916
1005
  await replyToThread(key, wakeNotice);
917
- const err = await adapter.setEffort(key, level);
918
- if (err) {
919
- await ctx.answerCbQuery((0, i18n_1.t)('cb.effort_error', { error: err.slice(0, 50) }));
1006
+ const outcome = await adapter.setEffort(key, level);
1007
+ if (outcome.status === 'refused') {
1008
+ await answerWithWholeText(ctx, key, (0, i18n_1.t)('cb.effort_error', { error: outcome.reason }));
920
1009
  return;
921
1010
  }
922
- await ctx.answerCbQuery((0, i18n_1.t)('cb.effort_set', { level }));
923
- await replyToThread(key, (0, i18n_1.t)('effort.set_success', { level }));
1011
+ if (outcome.status === 'stored') {
1012
+ // Made, but no agent is running to apply it: what follows a pick follows this one too — only the toast carries
1013
+ // the notice (whole) instead of "set".
1014
+ await answerWithWholeText(ctx, key, outcome.notice);
1015
+ }
1016
+ else {
1017
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.effort_set', { level }));
1018
+ await replyToThread(key, (0, i18n_1.t)('effort.set_success', { level }));
1019
+ }
924
1020
  await updatePinnedStatus(key).catch(() => { });
925
- // Re-render the picker so the `✓` marker follows the new level instead of
926
- // staying stuck on the previously-selected one (B12). The current level is
927
- // the freshly-set one; fall back to `level` if the adapter can't report it.
928
- const cbMsg = ctx.callbackQuery?.message;
929
- if (cbMsg && adapter.getAvailableEffortLevels) {
930
- let levels = [];
931
- try {
932
- levels = await adapter.getAvailableEffortLevels(key);
933
- }
934
- catch (e) {
935
- console.error('[effort_cb] getAvailableEffortLevels:', e);
936
- }
937
- if (levels.length > 0) {
938
- const cur = adapter.getEffort?.(key) ?? level;
939
- const keyboard = buildEffortKeyboard(levels, cur);
940
- try {
941
- await (0, rateLimiter_1.enqueueSend)(key, () => bot.telegram.editMessageReplyMarkup((0, sessionKeyCodec_1.getTelegramChatId)(key), cbMsg.message_id, undefined, keyboard.reply_markup));
942
- }
943
- catch (e) {
944
- const desc = (0, sendErrorClassifier_1.checkIsApiError)(e) ? (0, sendErrorClassifier_1.getErrorDescription)(e) : '';
945
- if (!/message is not modified/i.test(desc)) {
946
- console.warn('[effort_cb] keyboard re-render failed:', desc || e);
947
- }
948
- }
949
- }
1021
+ await rerenderEffortPicker(ctx, key);
1022
+ });
1023
+ // «As in General»: drop the topic's own pick. Registered with the level buttons: `effortsame` has no
1024
+ // underscore, so `effortLevelCallbackRe` never takes it.
1025
+ bot.action(effortPickerPlan_1.effortSameCallback, async (ctx) => {
1026
+ const key = await authoriseContext(ctx);
1027
+ if (!key) {
1028
+ await ctx.answerCbQuery((0, i18n_1.t)('cb.access_denied'));
1029
+ return;
950
1030
  }
1031
+ const adapter = (0, createAdapter_1.getThreadAdapter)(key);
1032
+ if (!adapter.clearEffort) {
1033
+ await ctx.answerCbQuery((0, i18n_1.t)('cb.not_supported', { label: adapter.label }));
1034
+ return;
1035
+ }
1036
+ await adapter.clearEffort(key);
1037
+ const { inForce: level } = getTopicEffortAmongLevels(key, adapter, await listTopicEffortLevels(key, adapter));
1038
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('cb.effort_same', { level }));
1039
+ await replyToThread(key, (0, i18n_1.t)('effort.sameAsGeneralConfirmation', { level }));
1040
+ await updatePinnedStatus(key).catch(() => { });
1041
+ await rerenderEffortPicker(ctx, key);
951
1042
  });
952
1043
  }
953
1044
  return {
@@ -955,6 +1046,7 @@ function createModelProviders(ports) {
955
1046
  applyProviderDisconnect,
956
1047
  handleProviderConnectKey,
957
1048
  applyNumberedModelPick,
1049
+ applyModelSelection,
958
1050
  applyModelPagePickArming,
959
1051
  checkIsNumberedModelPickArmed,
960
1052
  getNumberedModelPick,