charness 3.2.0 → 3.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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 +5 -4
  313. package/stt/gigaam_sidecar.py +800 -0
@@ -1,35 +1,28 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.limitsUsageRefreshBudgetMs = void 0;
4
3
  exports.createFallbackChain = createFallbackChain;
5
4
  /**
6
- * `/limits` — everything about a usage limit on one screen: the instance-wide priority chain of backend +
7
- * account / provider + model a topic continues on (reorder, include / exclude, remove, add through a two-step
8
- * sub-picker, rebuild a draft, clear — from EVERY topic, one instance-wide list), the two settings that decide what a
9
- * limit does (auto switch to a fallback model, wait for the reset and continue — instance defaults in General,
5
+ * `/limits_autoswitch` — the setup of automatic switching on a usage limit, on one screen: the instance-wide priority
6
+ * chain of backend + account / provider + model a topic continues on (reorder, include / exclude, remove, add through
7
+ * a two-step sub-picker, rebuild a draft, clear — from EVERY topic, one instance-wide list), the two settings that decide
8
+ * what a limit does (auto switch to a fallback model, wait for the reset and continue — instance defaults in General,
10
9
  * on / off / same-as-General overrides in a topic, chosen in a per-setting sub-menu) — and the «🔁 Switch to …»
11
- * buttons a usage-limit notice carries. The message text is deliberately short: the Claude accounts' usage block
12
- * on top (the `/accounts` renderer), then the topic's position and where its settings come from — the list and
13
- * the settings' values are on the buttons. `/fallback` and `/auto_continue_limits` are hidden aliases of this
14
- * screen. See README.md in this folder and `utils/fallbackChain.ts`.
10
+ * buttons a usage-limit notice carries. The message text is deliberately short: a title, the topic's position and
11
+ * where its settings come from — the list and the settings' values are on the buttons. `/fallback` and
12
+ * `/auto_continue_limits` are hidden aliases of this screen. `/limits` itself is the switch board
13
+ * (`limitsBoard.ts`). See README.md in this folder and `utils/fallbackChain.ts`.
15
14
  */
16
15
  const telegraf_1 = require("telegraf");
17
16
  const i18n_1 = require("../../../i18n");
18
- const paginateList_1 = require("../../../utils/paginateList");
17
+ const modelPickerPlan_1 = require("../../../utils/modelPickerPlan");
19
18
  const claudeUsage_1 = require("../../../utils/claudeUsage");
20
- const claudeProfileUsageLines_1 = require("../../../utils/claudeProfileUsageLines");
21
- const claudeUsageRefresher_1 = require("../../../utils/claudeUsageRefresher");
22
19
  const autoContinueOnLimit_1 = require("../../../utils/autoContinueOnLimit");
20
+ const usageRefreshBudget_1 = require("../../../utils/usageRefreshBudget");
23
21
  const fallbackChain_1 = require("../../../utils/fallbackChain");
22
+ const limitSettings_1 = require("../../../utils/limitSettings");
24
23
  const callbackMessage_1 = require("./callbackMessage");
25
- /** Models per page in the «add an OpenCode model» step (the `/model` picker's density). */
26
- const addModelPageSize = 8;
27
- /**
28
- * How long opening the screen waits for the usage refresh (one poll is ~0.5 s). A slower one — a hung endpoint
29
- * runs into the fetch's own 15 s timeout — keeps going in the background and the next render shows its result.
30
- */
31
- exports.limitsUsageRefreshBudgetMs = 2_000;
32
- const settingIds = ['switch', 'wait'];
24
+ const callbackToast_1 = require("./callbackToast");
25
+ const modelPage_1 = require("./modelPage");
33
26
  const settingCallbacks = {
34
27
  switch: { open: fallbackChain_1.fallbackAutoSwitchOpenCallback, on: fallbackChain_1.fallbackAutoSwitchOnCallback, off: fallbackChain_1.fallbackAutoSwitchOffCallback, same: fallbackChain_1.fallbackAutoSwitchSameCallback },
35
28
  wait: { open: fallbackChain_1.fallbackWaitOpenCallback, on: fallbackChain_1.fallbackWaitOnCallback, off: fallbackChain_1.fallbackWaitOffCallback, same: fallbackChain_1.fallbackWaitSameCallback },
@@ -39,10 +32,14 @@ const settingTextKeys = {
39
32
  switch: { button: 'limits.switchButton', menuTitle: 'limits.switchMenuTitle' },
40
33
  wait: { button: 'limits.waitButton', menuTitle: 'limits.waitMenuTitle' },
41
34
  };
42
- /** @description Build the `/limits` feature over its ports: the command (+ the hidden aliases), its buttons, the limit-notice switch button. */
35
+ /** @description Build the `/limits_autoswitch` feature over its ports: the command (+ the hidden aliases), its buttons, the limit-notice switch button. */
43
36
  function createFallbackChain(ports) {
44
37
  const { bot, command, getState, replyToThread, editThreadMessage, authoriseContext, withThreadLocale, checkIsGeneral, getInstanceTimezone, refreshClaudeUsage, detectFallbackOptions, getTopicPosition, takeSwitchOffer, applyAutoContinue, getArmedLimitRetryFireAt, } = ports;
38
+ const answerWithWholeText = (0, callbackToast_1.createWholeTextAnswerer)(replyToThread);
39
+ // Both per MESSAGE, keyed by chat + message id (`buildMessageSnapshotKey`): a message id is unique only within one chat.
45
40
  const addSnapshots = new Map();
41
+ /** The rung each «add it?» question waits to add; the revision of the list it was asked about rides in its button. */
42
+ const pendingAdds = new Map();
46
43
  function getLabelWords() {
47
44
  return { claude: (0, i18n_1.t)('fallback.backendClaude'), openCode: (0, i18n_1.t)('fallback.backendOpenCode'), topicModel: (0, i18n_1.t)('fallback.topicModel') };
48
45
  }
@@ -51,15 +48,17 @@ function createFallbackChain(ports) {
51
48
  return (0, fallbackChain_1.formatFallbackEntryLabel)(entry, getLabelWords());
52
49
  }
53
50
  /**
54
- * A rung on its label button: `3. Claude · work ⛔ 14:00` / `1. openai / gpt-6.1 🚫` — the FULL name (Telegram
55
- * sizes and truncates the button as it sees fit). The rung's state rides as a suffix (⛔ limited until, 🔒 not
56
- * signed in, ❓ not offered any more, 🚫 left out of switching, ✓ where the topic runs now). The one place the
57
- * label is built: a two-row layout (label on its own row) only changes how `buildPicker` assembles the row.
51
+ * A rung on its label button: `✓ 1. Claude · work · opus` / `3. OpenCode · openai · gpt-6.1 🚫` — the program, then
52
+ * the account or provider and the model when the rung names one (Telegram sizes and truncates the button as it sees
53
+ * fit). The rung's state rides as a suffix (⛔ limited until, 🔒 not signed in, ❓ not offered any more, 🚫 left out
54
+ * of switching) and ✓ at the start marks where the topic runs now. The one place the label is built: a two-row
55
+ * layout (label on its own row) only changes how `buildPicker` assembles the row.
58
56
  */
59
57
  function formatRowButtonLabel(index, entry, detection, current, nowMs) {
58
+ const words = getLabelWords();
60
59
  const base = entry.backend === 'claude'
61
- ? formatEntry(entry)
62
- : `${entry.providerId} / ${entry.modelId}`;
60
+ ? `${words.claude} · ${entry.profile}${entry.model === null ? '' : ` · ${entry.model}`}`
61
+ : `${words.openCode} · ${entry.providerId} · ${entry.modelId}`;
63
62
  const availability = (0, fallbackChain_1.getFallbackEntryAvailability)(entry, detection, nowMs);
64
63
  let suffix = '';
65
64
  if (availability.status === 'limited')
@@ -70,9 +69,8 @@ function createFallbackChain(ports) {
70
69
  suffix += ' ❓';
71
70
  if ((0, fallbackChain_1.checkIsFallbackEntryDisabled)(entry))
72
71
  suffix += ' 🚫';
73
- if (current && (0, fallbackChain_1.checkIsSameFallbackAccount)(entry, current))
74
- suffix += ' ✓';
75
- return `${index + 1}. ${base}${suffix}`;
72
+ const here = current && (0, fallbackChain_1.checkIsSameFallbackAccount)(entry, current) ? '✓ ' : '';
73
+ return `${here}${index + 1}. ${base}${suffix}`;
76
74
  }
77
75
  /**
78
76
  * A rung's detail view, in the message the list sits in: its name and state, then ⬆️ Up / ⬇️ Down (hidden at
@@ -121,28 +119,7 @@ function createFallbackChain(ports) {
121
119
  ];
122
120
  await editThreadMessage(key, messageId, lines.join('\n'), telegraf_1.Markup.inlineKeyboard(rows));
123
121
  }
124
- /** A setting's value for the topic (General: the instance default, where there is no override). */
125
- function readSetting(id, key, isGeneral) {
126
- if (id === 'switch') {
127
- return {
128
- effective: isGeneral ? getState().getClaudeAutoSwitchOnLimitGlobalDefault() : getState().checkIsClaudeAutoSwitchOnLimitEnabled(key),
129
- override: isGeneral ? undefined : getState().getClaudeAutoSwitchOnLimitOverride(key),
130
- generalValue: getState().getClaudeAutoSwitchOnLimitGlobalDefault(),
131
- };
132
- }
133
- return {
134
- effective: isGeneral ? getState().getAutoContinueOnLimitGlobalDefault() : getState().checkIsAutoContinueOnLimitEnabled(key),
135
- override: isGeneral ? undefined : getState().getAutoContinueOnLimitOverride(key),
136
- generalValue: getState().getAutoContinueOnLimitGlobalDefault(),
137
- };
138
- }
139
- function formatState(enabled) {
140
- return enabled ? (0, i18n_1.t)('autoContinueLimits.on') : (0, i18n_1.t)('autoContinueLimits.off');
141
- }
142
- /** Where a topic's setting value comes from: General, or the topic's own override. */
143
- function formatSettingSource(setting) {
144
- return setting.override === undefined ? (0, i18n_1.t)('limits.sourceGeneral') : (0, i18n_1.t)('limits.sourceTopic');
145
- }
122
+ const readSetting = (id, key, isGeneral) => (0, limitSettings_1.readLimitSetting)(getState(), id, key, isGeneral);
146
123
  /**
147
124
  * One setting's button on the main screen: one full-width button with the setting's name and its effective value
148
125
  * (`🔁 Auto switch: ON`). Where a topic's value comes from is in the message text above and in the sub-menu
@@ -153,7 +130,7 @@ function createFallbackChain(ports) {
153
130
  function buildSettingRow(id, setting, isGeneral) {
154
131
  const callbacks = settingCallbacks[id];
155
132
  const tapData = isGeneral ? (setting.effective ? callbacks.off : callbacks.on) : callbacks.open;
156
- return [telegraf_1.Markup.button.callback((0, i18n_1.t)(settingTextKeys[id].button, { state: formatState(setting.effective) }), tapData)];
133
+ return [telegraf_1.Markup.button.callback((0, i18n_1.t)(settingTextKeys[id].button, { state: (0, limitSettings_1.formatLimitSettingState)(setting.effective) }), tapData)];
157
134
  }
158
135
  /**
159
136
  * A topic's setting sub-menu, in the message the screen sits in: the setting's name, then ON / OFF / As in
@@ -168,41 +145,17 @@ function createFallbackChain(ports) {
168
145
  const callbacks = settingCallbacks[id];
169
146
  const mark = (isCurrent) => (isCurrent ? ' ✓' : '');
170
147
  const rows = [
171
- [telegraf_1.Markup.button.callback(`${formatState(true)}${mark(setting.override === true)}`, callbacks.on)],
172
- [telegraf_1.Markup.button.callback(`${formatState(false)}${mark(setting.override === false)}`, callbacks.off)],
173
- [telegraf_1.Markup.button.callback(`${(0, i18n_1.t)('limits.settingSameButton', { state: formatState(setting.generalValue) })}${mark(setting.override === undefined)}`, callbacks.same)],
148
+ [telegraf_1.Markup.button.callback(`${(0, limitSettings_1.formatLimitSettingState)(true)}${mark(setting.override === true)}`, callbacks.on)],
149
+ [telegraf_1.Markup.button.callback(`${(0, limitSettings_1.formatLimitSettingState)(false)}${mark(setting.override === false)}`, callbacks.off)],
150
+ [telegraf_1.Markup.button.callback(`${(0, i18n_1.t)('limits.settingSameButton', { state: (0, limitSettings_1.formatLimitSettingState)(setting.generalValue) })}${mark(setting.override === undefined)}`, callbacks.same)],
174
151
  buildBackRow(),
175
152
  ];
176
153
  await editThreadMessage(key, messageId, (0, i18n_1.t)(settingTextKeys[id].menuTitle), telegraf_1.Markup.inlineKeyboard(rows));
177
154
  }
178
155
  /**
179
- * The usage block at the top of the screen: a heading that says the percentages are USED, then each Claude
180
- * profile's windows as `/accounts` shows them under its row (`formatClaudeProfileUsageLines`), one row per
181
- * profile — the name, then its windows (a limit mark on the next row). A signed-out profile is named as such;
182
- * one never polled has nothing to show and is left out (no rows → no heading either). OpenCode's limit marks
183
- * are not repeated here: they are on the rung buttons (`⛔ until`).
184
- */
185
- function formatUsageBlock(detection, nowMs) {
186
- const timezone = getInstanceTimezone();
187
- const rows = [];
188
- for (const profile of detection.claudeProfiles) {
189
- if (profile.loggedIn === false) {
190
- rows.push(`• ${profile.name} — ${(0, i18n_1.t)('account.row.loggedOut')}`);
191
- continue;
192
- }
193
- // The first line hangs under a row in `/accounts` (the locale indents it); here it sits after the name.
194
- const [first, ...rest] = (0, claudeProfileUsageLines_1.formatClaudeProfileUsageLines)(profile, timezone, nowMs);
195
- if (first === undefined)
196
- continue;
197
- rows.push([`• ${profile.name} — ${first.trimStart()}`, ...rest].join('\n'));
198
- }
199
- return rows.length === 0 ? '' : [(0, i18n_1.t)('limits.usageHeading'), ...rows].join('\n');
200
- }
201
- /**
202
- * The `/limits` screen. Text: the Claude accounts' usage, then what the topic runs on and where its two
203
- * settings come from (General: just that these are defaults) and the list's heading — the list itself, its
204
- * marks and the two settings' values are on the buttons. A topic also gets «skip once» while a limit wait is
205
- * armed for it.
156
+ * The `/limits_autoswitch` screen. Text: its title, what the topic runs on and where its two settings come from
157
+ * (General: just that these are defaults), then the list's heading — the list itself, its marks and the two
158
+ * settings' values are on the buttons. A topic also gets «skip once» while a limit wait is armed for it.
206
159
  */
207
160
  async function buildPicker(key, isGeneral) {
208
161
  const chain = getState().getFallbackChain();
@@ -212,18 +165,13 @@ function createFallbackChain(ports) {
212
165
  const switchSetting = readSetting('switch', key, isGeneral);
213
166
  const waitSetting = readSetting('wait', key, isGeneral);
214
167
  const parts = [];
215
- const usageBlock = formatUsageBlock(detection, nowMs);
216
- if (usageBlock !== '')
217
- parts.push(usageBlock);
218
- if (isGeneral) {
219
- parts.push((0, i18n_1.t)('limits.settingsGeneral'));
220
- }
221
- else {
222
- parts.push([
168
+ parts.push(isGeneral
169
+ ? [(0, i18n_1.t)('limits.autoswitchTitle'), (0, i18n_1.t)('limits.settingsGeneral')].join('\n')
170
+ : [
171
+ (0, i18n_1.t)('limits.autoswitchTitle'),
223
172
  (0, i18n_1.t)('limits.topicRuns', { current: current ? formatEntry(current) : (0, i18n_1.t)('fallback.noAgent') }),
224
- (0, i18n_1.t)('limits.sourceLine', { switch: formatSettingSource(switchSetting), wait: formatSettingSource(waitSetting) }),
173
+ (0, i18n_1.t)('limits.sourceLine', { switch: (0, limitSettings_1.formatLimitSettingSource)(switchSetting), wait: (0, limitSettings_1.formatLimitSettingSource)(waitSetting) }),
225
174
  ].join('\n'));
226
- }
227
175
  parts.push(chain.length === 0 ? (0, i18n_1.t)('limits.listNoteEmpty') : (0, i18n_1.t)('limits.listNote'));
228
176
  const rows = [];
229
177
  const revision = (0, fallbackChain_1.getFallbackChainRevision)(chain);
@@ -249,7 +197,9 @@ function createFallbackChain(ports) {
249
197
  async function rerenderPicker(key, messageId, isGeneral) {
250
198
  if (messageId === null)
251
199
  return;
252
- addSnapshots.delete(messageId);
200
+ const snapshotKey = (0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId);
201
+ addSnapshots.delete(snapshotKey);
202
+ pendingAdds.delete(snapshotKey);
253
203
  const picker = await buildPicker(key, isGeneral);
254
204
  await editThreadMessage(key, messageId, picker.text, picker.keyboard);
255
205
  }
@@ -274,7 +224,7 @@ function createFallbackChain(ports) {
274
224
  await replyToThread(key, (0, i18n_1.t)('fallback.nothingDetected'));
275
225
  return;
276
226
  }
277
- addSnapshots.set(messageId, { providers, providerId: null, models: [] });
227
+ addSnapshots.set((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId), { providers, providerId: null, models: [] });
278
228
  rows.push(buildBackRow());
279
229
  await editThreadMessage(key, messageId, (0, i18n_1.t)('fallback.pickAccount'), telegraf_1.Markup.inlineKeyboard(rows));
280
230
  }
@@ -285,22 +235,25 @@ function createFallbackChain(ports) {
285
235
  rows.push(buildBackRow());
286
236
  await editThreadMessage(key, messageId, (0, i18n_1.t)('fallback.pickModel', { account: `${getLabelWords().claude} · ${profile}` }), telegraf_1.Markup.inlineKeyboard(rows));
287
237
  }
288
- /** Step 2 for an OpenCode provider: one page of its models (absolute indexes into the snapshot). */
289
- async function renderAddModelPage(key, messageId, snapshot, page) {
290
- const { slice, currentPage, totalPages } = (0, paginateList_1.paginateList)(snapshot.models, page, addModelPageSize);
291
- const offset = currentPage * addModelPageSize;
292
- const rows = slice.map((modelId, index) => [telegraf_1.Markup.button.callback(modelId, (0, fallbackChain_1.buildFallbackAddModelCallback)(offset + index))]);
293
- if (totalPages > 1) {
294
- const nav = [];
295
- if (currentPage > 0)
296
- nav.push(telegraf_1.Markup.button.callback('◀️', (0, fallbackChain_1.buildFallbackAddModelPageCallback)(currentPage - 1)));
297
- nav.push(telegraf_1.Markup.button.callback(`${currentPage + 1}/${totalPages}`, (0, fallbackChain_1.buildFallbackAddModelPageCallback)(currentPage)));
298
- if (currentPage < totalPages - 1)
299
- nav.push(telegraf_1.Markup.button.callback('▶️', (0, fallbackChain_1.buildFallbackAddModelPageCallback)(currentPage + 1)));
300
- rows.push(nav);
301
- }
302
- rows.push(buildBackRow());
303
- await editThreadMessage(key, messageId, (0, i18n_1.t)('fallback.pickModel', { account: `${getLabelWords().openCode} · ${snapshot.providerId}` }), telegraf_1.Markup.inlineKeyboard(rows));
238
+ /** Step 2 for an OpenCode provider: one page of its models (absolute indexes into the snapshot), limit marks on the limited ones. */
239
+ async function showAddModelPage(key, messageId, snapshot, page) {
240
+ const providerId = snapshot.providerId ?? '';
241
+ const { openCodeLimits } = await detectFallbackOptions();
242
+ const nowMs = Date.now();
243
+ const { rows } = (0, modelPage_1.buildModelPageButtons)({
244
+ provider: providerId,
245
+ models: snapshot.models,
246
+ page,
247
+ current: null,
248
+ callbacks: {
249
+ buildPage: fallbackChain_1.buildFallbackAddModelPageCallback,
250
+ buildPick: fallbackChain_1.buildFallbackAddModelCallback,
251
+ noop: modelPickerPlan_1.modelPickerNoopCallback,
252
+ back: { label: (0, i18n_1.t)('fallback.backButton'), data: fallbackChain_1.fallbackBackCallback },
253
+ },
254
+ getSuffix: (modelId) => (0, fallbackChain_1.formatOpenCodeModelLimitSuffix)(`${providerId}/${modelId}`, openCodeLimits, getInstanceTimezone(), nowMs),
255
+ });
256
+ await editThreadMessage(key, messageId, (0, i18n_1.t)('fallback.pickModel', { account: `${getLabelWords().openCode} · ${providerId}` }), telegraf_1.Markup.inlineKeyboard(rows));
304
257
  }
305
258
  /** Append an entry to the chain and RETURN the confirmation (the one write path behind both add steps). */
306
259
  async function applyAdd(entry) {
@@ -311,6 +264,33 @@ function createFallbackChain(ports) {
311
264
  await getState().setFallbackChain(result.chain);
312
265
  return (0, i18n_1.t)('fallback.added', { label: formatEntry(entry) });
313
266
  }
267
+ /**
268
+ * The question before a rung of a NEW backend joins automatic switching, in the message the tap came from: the
269
+ * rung, the shared line about the hand-over, ✅ Continue (the revision of the list is baked in) and ✖️ Cancel
270
+ * (`cancelData`: back to the list, or to the rung's detail view). A tap that comes late finds the list changed.
271
+ */
272
+ async function askAboutNewBackend(key, messageId, questionKey, entry, continueData, cancelData) {
273
+ const text = `${(0, i18n_1.t)(questionKey, { label: formatEntry(entry) })}\n${(0, i18n_1.t)('handover.note')}`;
274
+ await editThreadMessage(key, messageId, text, telegraf_1.Markup.inlineKeyboard([[
275
+ telegraf_1.Markup.button.callback((0, i18n_1.t)('handover.continueButton'), continueData),
276
+ telegraf_1.Markup.button.callback((0, i18n_1.t)('handover.cancelButton'), cancelData),
277
+ ]]));
278
+ }
279
+ /**
280
+ * An «add» that finished its steps. A rung of a backend the list does not use yet asks first (the question
281
+ * replaces the picker in the same message); any other add goes ahead and the screen is re-rendered.
282
+ */
283
+ async function takeAdd(ctx, key, isGeneral, messageId, entry) {
284
+ const chain = getState().getFallbackChain();
285
+ if (messageId !== null && (0, fallbackChain_1.addFallbackEntry)(chain, entry).ok && (0, fallbackChain_1.checkIsNewBackendForChain)(chain, entry)) {
286
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
287
+ pendingAdds.set((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId), entry);
288
+ await askAboutNewBackend(key, messageId, 'limits.addConfirm', entry, (0, fallbackChain_1.buildFallbackAddConfirmCallback)((0, fallbackChain_1.getFallbackChainRevision)(chain)), fallbackChain_1.fallbackBackCallback);
289
+ return;
290
+ }
291
+ await answerWithWholeText(ctx, key, await applyAdd(entry));
292
+ await rerenderPicker(key, messageId, isGeneral);
293
+ }
314
294
  /** Replace the chain with the draft and RETURN the confirmation. */
315
295
  async function applyPropose() {
316
296
  const chain = (0, fallbackChain_1.proposeFallbackChain)(await detectFallbackOptions(), getState().getFallbackChain());
@@ -332,55 +312,42 @@ function createFallbackChain(ports) {
332
312
  const what = (0, i18n_1.t)('limits.whatSwitch');
333
313
  if (isGeneral) {
334
314
  await getState().setClaudeAutoSwitchOnLimitGlobalDefault(enabled ?? false);
335
- return (0, i18n_1.t)('limits.setGlobal', { what, state: formatState(enabled ?? false) });
315
+ return (0, i18n_1.t)('limits.setGlobal', { what, state: (0, limitSettings_1.formatLimitSettingState)(enabled ?? false) });
336
316
  }
337
317
  await getState().setClaudeAutoSwitchOnLimitOverride(key, enabled);
338
318
  return enabled === null
339
- ? (0, i18n_1.t)('limits.setSame', { what, state: formatState(getState().getClaudeAutoSwitchOnLimitGlobalDefault()) })
340
- : (0, i18n_1.t)('limits.setThisTopic', { what, state: formatState(enabled) });
319
+ ? (0, i18n_1.t)('limits.setSame', { what, state: (0, limitSettings_1.formatLimitSettingState)(getState().getClaudeAutoSwitchOnLimitGlobalDefault()) })
320
+ : (0, i18n_1.t)('limits.setThisTopic', { what, state: (0, limitSettings_1.formatLimitSettingState)(enabled) });
341
321
  }
342
322
  /** The «wait for the reset» twin of {@link applyAutoSwitch}; the write (and the armed-wait cleanup) is `autoContinueLimits.ts`'s. */
343
323
  async function applyWait(key, isGeneral, enabled) {
344
324
  const what = (0, i18n_1.t)('limits.whatWait');
345
325
  await applyAutoContinue(key, isGeneral, isGeneral ? (enabled ?? true) : enabled);
346
326
  if (isGeneral)
347
- return (0, i18n_1.t)('limits.setGlobal', { what, state: formatState(enabled ?? true) });
327
+ return (0, i18n_1.t)('limits.setGlobal', { what, state: (0, limitSettings_1.formatLimitSettingState)(enabled ?? true) });
348
328
  return enabled === null
349
- ? (0, i18n_1.t)('limits.setSame', { what, state: formatState(getState().getAutoContinueOnLimitGlobalDefault()) })
350
- : (0, i18n_1.t)('limits.setThisTopic', { what, state: formatState(enabled) });
329
+ ? (0, i18n_1.t)('limits.setSame', { what, state: (0, limitSettings_1.formatLimitSettingState)(getState().getAutoContinueOnLimitGlobalDefault()) })
330
+ : (0, i18n_1.t)('limits.setThisTopic', { what, state: (0, limitSettings_1.formatLimitSettingState)(enabled) });
351
331
  }
352
332
  // ── the command ──
353
333
  function registerFallbackChainCommands() {
354
- // `/limits` — bare: the screen (the chain is edited from every topic). `propose` / `rebuild`
334
+ // `/limits_autoswitch` — bare: the screen (the chain is edited from every topic). `propose` / `rebuild`
355
335
  // drafts the chain, `clear` empties it, `switch|autoswitch on|off|same` and `wait on|off|same` flip the two
356
- // settings. `/fallback` is the same command under its old name (hidden alias). Not gated on a session:
357
- // the chain must be settable before any limit hits.
358
- command(['limits', 'fallback'], async (_ctx, key, parsed) => {
336
+ // settings; `/limits` takes the same verbs (`limitsBoard.ts` hands them to {@link runAutoSwitchCommand}).
337
+ // `/fallback` is the same command under its old name (hidden alias). Not gated on a session: the chain must
338
+ // be settable before any limit hits.
339
+ command(['limits_autoswitch', 'fallback'], async (_ctx, key, parsed) => {
359
340
  const [verb = '', value = ''] = parsed.args;
360
- await runLimitsCommand(key, verb, value);
341
+ await runAutoSwitchCommand(key, verb, value);
361
342
  });
362
343
  }
363
- /**
364
- * Refresh the usage snapshots (a snapshot younger than a minute is kept — the `/accounts` freshness), waiting at
365
- * most {@link limitsUsageRefreshBudgetMs}: the screen is never held by a slow usage endpoint.
366
- */
367
- async function refreshUsageWithinBudget() {
368
- let timer;
369
- const budget = new Promise((resolve) => { timer = setTimeout(resolve, exports.limitsUsageRefreshBudgetMs); });
370
- try {
371
- await Promise.race([refreshClaudeUsage({ maxAgeMs: claudeUsageRefresher_1.claudeUsageOnDemandMaxAgeMs }), budget]);
372
- }
373
- finally {
374
- clearTimeout(timer);
375
- }
376
- }
377
344
  /** Open the screen as a fresh message (the `/auto_continue_limits` alias lands here); the usage is refreshed first. */
378
- async function openLimitsView(key) {
379
- await refreshUsageWithinBudget();
345
+ async function openAutoSwitchView(key) {
346
+ await (0, usageRefreshBudget_1.refreshUsageWithinBudget)(refreshClaudeUsage);
380
347
  const picker = await buildPicker(key, checkIsGeneral(key));
381
348
  await replyToThread(key, picker.text, picker.keyboard);
382
349
  }
383
- async function runLimitsCommand(key, verb, value) {
350
+ async function runAutoSwitchCommand(key, verb, value, usageKey = 'limits.autoswitchUsage') {
384
351
  const isGeneral = checkIsGeneral(key);
385
352
  const word = value.toLowerCase();
386
353
  const parseState = () => {
@@ -394,12 +361,12 @@ function createFallbackChain(ports) {
394
361
  };
395
362
  switch (verb.toLowerCase()) {
396
363
  case '':
397
- await openLimitsView(key);
364
+ await openAutoSwitchView(key);
398
365
  return;
399
366
  case 'propose':
400
367
  case 'rebuild': {
401
368
  await replyToThread(key, await applyPropose());
402
- await openLimitsView(key);
369
+ await openAutoSwitchView(key);
403
370
  return;
404
371
  }
405
372
  case 'clear': {
@@ -410,7 +377,7 @@ function createFallbackChain(ports) {
410
377
  case 'autoswitch': {
411
378
  const enabled = parseState();
412
379
  if (enabled === undefined) {
413
- await replyToThread(key, (0, i18n_1.t)('limits.usage'));
380
+ await replyToThread(key, (0, i18n_1.t)(usageKey));
414
381
  return;
415
382
  }
416
383
  await replyToThread(key, await applyAutoSwitch(key, isGeneral, enabled));
@@ -419,14 +386,14 @@ function createFallbackChain(ports) {
419
386
  case 'wait': {
420
387
  const enabled = parseState();
421
388
  if (enabled === undefined) {
422
- await replyToThread(key, (0, i18n_1.t)('limits.usage'));
389
+ await replyToThread(key, (0, i18n_1.t)(usageKey));
423
390
  return;
424
391
  }
425
392
  await replyToThread(key, await applyWait(key, isGeneral, enabled));
426
393
  return;
427
394
  }
428
395
  default:
429
- await replyToThread(key, (0, i18n_1.t)('limits.usage'));
396
+ await replyToThread(key, (0, i18n_1.t)(usageKey));
430
397
  }
431
398
  }
432
399
  /** The shared tap prologue: authorise, resolve the topic, run under its locale. */
@@ -438,39 +405,39 @@ function createFallbackChain(ports) {
438
405
  }
439
406
  await withThreadLocale(key, () => run(key, checkIsGeneral(key), (0, callbackMessage_1.getCallbackMessageId)(ctx)));
440
407
  }
441
- /** `answerCbQuery` is capped at 200 characters: a long confirmation goes to the topic instead. */
442
- async function answerTap(ctx, key, text) {
443
- if (text.length <= 180) {
444
- await ctx.answerCbQuery(text);
445
- return;
446
- }
447
- await ctx.answerCbQuery();
448
- await replyToThread(key, text);
449
- }
450
408
  /** An edit tap: a stale revision only re-renders. */
451
409
  function registerEdit(re, edit, confirm,
452
410
  /** Where the rung sits after the edit (the detail view follows it); `null` = back to the list. */
453
- nextIndex) {
411
+ nextIndex,
412
+ /** An edit that must ask first answers with the data of the question's two buttons; `null` = go ahead. */
413
+ gate) {
454
414
  bot.action(re, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
455
415
  const chain = getState().getFallbackChain();
456
- const parsed = (0, fallbackChain_1.parseFallbackEditCallback)(ctx.match[0], re, (0, fallbackChain_1.getFallbackChainRevision)(chain));
416
+ const revision = (0, fallbackChain_1.getFallbackChainRevision)(chain);
417
+ const parsed = (0, fallbackChain_1.parseFallbackEditCallback)(ctx.match[0], re, revision);
457
418
  if (parsed === null) {
458
- await ctx.answerCbQuery();
419
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
459
420
  return;
460
421
  }
461
422
  if (!parsed.isCurrent) {
462
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
423
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
463
424
  await rerenderPicker(key, messageId, isGeneral);
464
425
  return;
465
426
  }
427
+ const question = gate?.(chain, parsed.index, revision);
428
+ if (question && messageId !== null && chain[parsed.index]) {
429
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
430
+ await askAboutNewBackend(key, messageId, 'limits.enableConfirm', chain[parsed.index], question.continueData, question.cancelData);
431
+ return;
432
+ }
466
433
  const result = edit(chain, parsed.index);
467
434
  if (!result.ok) {
468
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
435
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
469
436
  await rerenderPicker(key, messageId, isGeneral);
470
437
  return;
471
438
  }
472
439
  await getState().setFallbackChain(result.chain);
473
- await answerTap(ctx, key, confirm(chain, parsed.index));
440
+ await answerWithWholeText(ctx, key, confirm(chain, parsed.index));
474
441
  const next = nextIndex(parsed.index);
475
442
  if (next === null)
476
443
  await rerenderPicker(key, messageId, isGeneral);
@@ -481,106 +448,129 @@ function createFallbackChain(ports) {
481
448
  function registerFallbackChainCallbacks() {
482
449
  registerEdit(fallbackChain_1.fallbackMoveUpCallbackRe, (chain, index) => (0, fallbackChain_1.moveFallbackEntry)(chain, index, -1), () => (0, i18n_1.t)('fallback.moved'), (index) => Math.max(0, index - 1));
483
450
  registerEdit(fallbackChain_1.fallbackMoveDownCallbackRe, (chain, index) => (0, fallbackChain_1.moveFallbackEntry)(chain, index, 1), () => (0, i18n_1.t)('fallback.moved'), (index) => index + 1);
484
- registerEdit(fallbackChain_1.fallbackToggleCallbackRe, fallbackChain_1.toggleFallbackEntryDisabled, (chain, index) => (0, i18n_1.t)((0, fallbackChain_1.checkIsFallbackEntryDisabled)(chain[index]) ? 'limits.entryEnabled' : 'limits.entryDisabled', { label: formatEntry(chain[index]) }), (index) => index);
451
+ const confirmToggle = (chain, index) => (0, i18n_1.t)((0, fallbackChain_1.checkIsFallbackEntryDisabled)(chain[index]) ? 'limits.entryEnabled' : 'limits.entryDisabled', { label: formatEntry(chain[index]) });
452
+ // Using a rung that is left out is a question when its backend is not in automatic switching yet.
453
+ registerEdit(fallbackChain_1.fallbackToggleCallbackRe, fallbackChain_1.toggleFallbackEntryDisabled, confirmToggle, (index) => index, (chain, index, revision) => {
454
+ const entry = chain[index];
455
+ if (!entry || !(0, fallbackChain_1.checkIsFallbackEntryDisabled)(entry) || !(0, fallbackChain_1.checkIsNewBackendForChain)(chain.filter((_, i) => i !== index), entry))
456
+ return null;
457
+ return { continueData: (0, fallbackChain_1.buildFallbackToggleConfirmCallback)(index, revision), cancelData: (0, fallbackChain_1.buildFallbackOpenCallback)(index, revision) };
458
+ });
459
+ registerEdit(fallbackChain_1.fallbackToggleConfirmCallbackRe, fallbackChain_1.toggleFallbackEntryDisabled, confirmToggle, (index) => index);
485
460
  registerEdit(fallbackChain_1.fallbackRemoveCallbackRe, fallbackChain_1.removeFallbackEntry, (chain, index) => (0, i18n_1.t)('fallback.removed', { label: formatEntry(chain[index]) }), () => null);
486
461
  // A rung's button on the list: open its detail view (a stale revision only re-renders the list).
487
462
  bot.action(fallbackChain_1.fallbackOpenCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
488
463
  const chain = getState().getFallbackChain();
489
464
  const parsed = (0, fallbackChain_1.parseFallbackEditCallback)(ctx.match[0], fallbackChain_1.fallbackOpenCallbackRe, (0, fallbackChain_1.getFallbackChainRevision)(chain));
490
465
  if (parsed === null) {
491
- await ctx.answerCbQuery();
466
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
492
467
  return;
493
468
  }
494
469
  if (!parsed.isCurrent || parsed.index >= chain.length) {
495
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
470
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
496
471
  await rerenderPicker(key, messageId, isGeneral);
497
472
  return;
498
473
  }
499
- await ctx.answerCbQuery();
474
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
500
475
  await renderRungDetail(key, messageId, parsed.index, isGeneral);
501
476
  }));
502
477
  bot.action(fallbackChain_1.fallbackAddCallback, (ctx) => handleTap(ctx, async (key, _isGeneral, messageId) => {
503
- await ctx.answerCbQuery();
478
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
504
479
  if (messageId !== null)
505
480
  await renderAddAccountStep(key, messageId);
506
481
  }));
507
482
  bot.action(fallbackChain_1.fallbackAddClaudeCallbackRe, (ctx) => handleTap(ctx, async (key, _isGeneral, messageId) => {
508
- await ctx.answerCbQuery();
483
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
509
484
  if (messageId !== null)
510
485
  await renderAddClaudeModelStep(key, messageId, ctx.match[1]);
511
486
  }));
512
487
  bot.action(fallbackChain_1.fallbackAddClaudeModelCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
513
488
  const model = ctx.match[2] === '-' ? null : ctx.match[2];
514
- await answerTap(ctx, key, await applyAdd({ backend: 'claude', profile: ctx.match[1], model }));
515
- await rerenderPicker(key, messageId, isGeneral);
489
+ await takeAdd(ctx, key, isGeneral, messageId, { backend: 'claude', profile: ctx.match[1], model });
516
490
  }));
517
491
  bot.action(fallbackChain_1.fallbackAddProviderCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
518
- const snapshot = messageId === null ? undefined : addSnapshots.get(messageId);
492
+ const snapshot = messageId === null ? undefined : addSnapshots.get((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId));
519
493
  const providerId = snapshot?.providers[Number(ctx.match[1])];
520
494
  if (messageId === null || !snapshot || providerId === undefined) {
521
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
495
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
522
496
  await rerenderPicker(key, messageId, isGeneral);
523
497
  return;
524
498
  }
525
- await ctx.answerCbQuery();
499
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
526
500
  const detection = await detectFallbackOptions();
527
501
  const models = (0, fallbackChain_1.listOpenCodeModelsOfProvider)(detection.openCodeModels, providerId);
528
502
  const next = { ...snapshot, providerId, models };
529
- addSnapshots.set(messageId, next);
530
- await renderAddModelPage(key, messageId, next, 0);
503
+ addSnapshots.set((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId), next);
504
+ await showAddModelPage(key, messageId, next, 0);
531
505
  }));
532
506
  bot.action(fallbackChain_1.fallbackAddModelPageCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
533
- const snapshot = messageId === null ? undefined : addSnapshots.get(messageId);
507
+ const snapshot = messageId === null ? undefined : addSnapshots.get((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId));
534
508
  if (messageId === null || !snapshot || snapshot.providerId === null) {
535
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
509
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
536
510
  await rerenderPicker(key, messageId, isGeneral);
537
511
  return;
538
512
  }
539
- await ctx.answerCbQuery();
540
- await renderAddModelPage(key, messageId, snapshot, Number(ctx.match[1]));
513
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
514
+ await showAddModelPage(key, messageId, snapshot, Number(ctx.match[1]));
541
515
  }));
542
516
  bot.action(fallbackChain_1.fallbackAddModelCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
543
- const snapshot = messageId === null ? undefined : addSnapshots.get(messageId);
517
+ const snapshot = messageId === null ? undefined : addSnapshots.get((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId));
544
518
  const modelId = snapshot?.models[Number(ctx.match[1])];
545
519
  if (!snapshot || snapshot.providerId === null || modelId === undefined) {
546
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.stale'));
520
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
521
+ await rerenderPicker(key, messageId, isGeneral);
522
+ return;
523
+ }
524
+ await takeAdd(ctx, key, isGeneral, messageId, { backend: 'opencode', providerId: snapshot.providerId, modelId });
525
+ }));
526
+ // «✅ Continue» of the question before a rung of a new backend is added: only while the list is the one the
527
+ // question was asked about; a stale tap re-renders the list and adds nothing.
528
+ bot.action(fallbackChain_1.fallbackAddConfirmCallbackRe, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
529
+ const entry = messageId === null ? undefined : pendingAdds.get((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId));
530
+ if (!entry || ctx.match[1] !== (0, fallbackChain_1.getFallbackChainRevision)(getState().getFallbackChain())) {
531
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.stale'));
547
532
  await rerenderPicker(key, messageId, isGeneral);
548
533
  return;
549
534
  }
550
- await answerTap(ctx, key, await applyAdd({ backend: 'opencode', providerId: snapshot.providerId, modelId }));
535
+ if (messageId !== null)
536
+ pendingAdds.delete((0, callbackMessage_1.buildMessageSnapshotKey)(key, messageId));
537
+ await answerWithWholeText(ctx, key, await applyAdd(entry));
551
538
  await rerenderPicker(key, messageId, isGeneral);
552
539
  }));
553
540
  bot.action(fallbackChain_1.fallbackProposeCallback, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
554
541
  if (getState().getFallbackChain().length === 0) {
555
- await answerTap(ctx, key, await applyPropose());
542
+ await answerWithWholeText(ctx, key, await applyPropose());
556
543
  await rerenderPicker(key, messageId, isGeneral);
557
544
  return;
558
545
  }
559
- await ctx.answerCbQuery();
546
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
560
547
  if (messageId !== null) {
561
- await editThreadMessage(key, messageId, (0, i18n_1.t)('limits.rebuildConfirm'), telegraf_1.Markup.inlineKeyboard([
548
+ // A draft that holds both backends hands conversations over when a limit moves a topic along it: say so.
549
+ const draft = (0, fallbackChain_1.proposeFallbackChain)(await detectFallbackOptions(), getState().getFallbackChain());
550
+ const text = (0, fallbackChain_1.checkIsMixedFallbackChain)(draft) ? `${(0, i18n_1.t)('limits.rebuildConfirm')}\n${(0, i18n_1.t)('handover.note')}` : (0, i18n_1.t)('limits.rebuildConfirm');
551
+ await editThreadMessage(key, messageId, text, telegraf_1.Markup.inlineKeyboard([
562
552
  [telegraf_1.Markup.button.callback((0, i18n_1.t)('fallback.proposeConfirmButton'), fallbackChain_1.fallbackProposeConfirmCallback)],
563
553
  buildBackRow(),
564
554
  ]));
565
555
  }
566
556
  }));
567
557
  bot.action(fallbackChain_1.fallbackProposeConfirmCallback, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
568
- await answerTap(ctx, key, await applyPropose());
558
+ await answerWithWholeText(ctx, key, await applyPropose());
569
559
  await rerenderPicker(key, messageId, isGeneral);
570
560
  }));
571
561
  bot.action(fallbackChain_1.fallbackClearCallback, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
572
- await answerTap(ctx, key, await applyClear());
562
+ await answerWithWholeText(ctx, key, await applyClear());
573
563
  await rerenderPicker(key, messageId, isGeneral);
574
564
  }));
575
565
  bot.action(fallbackChain_1.fallbackBackCallback, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
576
- await ctx.answerCbQuery();
566
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
577
567
  await rerenderPicker(key, messageId, isGeneral);
578
568
  }));
579
569
  // A topic's setting button: open its sub-menu. General has no sub-menu (its button flips the value), so a tap
580
570
  // that still arrives there only re-renders the screen.
581
- for (const id of settingIds) {
571
+ for (const id of limitSettings_1.limitSettingIds) {
582
572
  bot.action(settingCallbacks[id].open, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
583
- await ctx.answerCbQuery();
573
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
584
574
  if (isGeneral)
585
575
  await rerenderPicker(key, messageId, isGeneral);
586
576
  else
@@ -594,10 +584,10 @@ function createFallbackChain(ports) {
594
584
  for (const [callback, apply, enabled] of settingTaps) {
595
585
  bot.action(callback, (ctx) => handleTap(ctx, async (key, isGeneral, messageId) => {
596
586
  if (enabled === null && isGeneral) {
597
- await ctx.answerCbQuery();
587
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
598
588
  return;
599
589
  }
600
- await answerTap(ctx, key, await apply(key, isGeneral, enabled));
590
+ await answerWithWholeText(ctx, key, await apply(key, isGeneral, enabled));
601
591
  await rerenderPicker(key, messageId, isGeneral);
602
592
  }));
603
593
  }
@@ -606,19 +596,19 @@ function createFallbackChain(ports) {
606
596
  bot.action(fallbackChain_1.fallbackSwitchCallbackRe, (ctx) => handleTap(ctx, async (key, _isGeneral, messageId) => {
607
597
  const parsed = (0, fallbackChain_1.parseFallbackSwitchCallbackData)(ctx.match[0]);
608
598
  if (parsed === null) {
609
- await ctx.answerCbQuery();
599
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
610
600
  return;
611
601
  }
612
602
  const confirmation = await takeSwitchOffer(key, parsed.episodeId, parsed.index);
613
603
  if (confirmation === null) {
614
- await ctx.answerCbQuery((0, i18n_1.t)('fallback.switchExpired'));
604
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx, (0, i18n_1.t)('fallback.switchExpired'));
615
605
  return;
616
606
  }
617
- await ctx.answerCbQuery();
607
+ await (0, callbackToast_1.answerCallbackQuerySafely)(ctx);
618
608
  if (messageId !== null)
619
609
  await editThreadMessage(key, messageId, confirmation);
620
610
  }));
621
611
  }
622
- return { registerFallbackChainCommands, registerFallbackChainCallbacks, openLimitsView, applyAutoSwitch, formatFallbackEntry: formatEntry };
612
+ return { registerFallbackChainCommands, registerFallbackChainCallbacks, openAutoSwitchView, runAutoSwitchCommand, applyAutoSwitch, formatFallbackEntry: formatEntry };
623
613
  }
624
614
  //# sourceMappingURL=fallbackChain.js.map