@grinev/opencode-telegram-bot 0.21.1 → 0.22.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 (193) hide show
  1. package/.env.example +10 -15
  2. package/README.md +33 -13
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +20 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +177 -13
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +27 -12
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +49 -7
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +113 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/callbacks/settings-callback-handler.js +105 -0
  62. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  63. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  64. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  65. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  66. package/dist/bot/commands/command-catalog-command.js +42 -0
  67. package/dist/bot/commands/definitions.js +1 -1
  68. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  69. package/dist/bot/commands/ls-command.js +48 -0
  70. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  71. package/dist/bot/commands/messages-command.js +59 -0
  72. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  73. package/dist/bot/commands/open-command.js +45 -0
  74. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  75. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  76. package/dist/bot/commands/projects-command.js +32 -0
  77. package/dist/bot/commands/rename-command.js +33 -0
  78. package/dist/bot/commands/sessions-command.js +42 -0
  79. package/dist/bot/commands/settings-command.js +10 -0
  80. package/dist/bot/commands/skills-catalog-command.js +42 -0
  81. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  82. package/dist/bot/commands/{status.js → status-command.js} +15 -10
  83. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  84. package/dist/bot/commands/tasklist-command.js +40 -0
  85. package/dist/bot/commands/worktree-command.js +47 -0
  86. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  87. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  88. package/dist/bot/handlers/photo-handler.js +44 -0
  89. package/dist/bot/handlers/prompt.js +17 -17
  90. package/dist/bot/handlers/text-message-handler.js +61 -0
  91. package/dist/bot/handlers/tts-response-handler.js +29 -0
  92. package/dist/bot/handlers/{voice.js → voice-handler.js} +6 -3
  93. package/dist/bot/index.js +27 -1179
  94. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  95. package/dist/bot/keyboards/keyboard-types.js +1 -0
  96. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  97. package/dist/bot/menus/agent-selection-menu.js +54 -0
  98. package/dist/bot/menus/command-catalog-menu.js +101 -0
  99. package/dist/bot/menus/context-control-menu.js +33 -0
  100. package/dist/bot/menus/file-browser-menu.js +265 -0
  101. package/dist/bot/{handlers → menus}/inline-menu.js +7 -34
  102. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  103. package/dist/bot/menus/message-history-menu.js +107 -0
  104. package/dist/bot/menus/model-selection-menu.js +66 -0
  105. package/dist/bot/menus/permission-menu.js +116 -0
  106. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  107. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  108. package/dist/bot/menus/rename-menu.js +6 -0
  109. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  110. package/dist/bot/menus/session-selection-menu.js +115 -0
  111. package/dist/bot/menus/settings-menu.js +52 -0
  112. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  113. package/dist/bot/menus/variant-selection-menu.js +67 -0
  114. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  115. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  116. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  117. package/dist/bot/messages/external-user-input-notification.js +22 -0
  118. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  119. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  120. package/dist/bot/messages/summary-message-formatter.js +130 -0
  121. package/dist/bot/{utils → messages}/thinking-message.js +1 -4
  122. package/dist/bot/messages/thinking-rendering.js +66 -0
  123. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  124. package/dist/bot/middleware/interaction-guard.js +2 -2
  125. package/dist/bot/middleware/unknown-command.js +1 -1
  126. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  127. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  128. package/dist/bot/pinned/pinned-message-types.js +1 -0
  129. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  130. package/dist/bot/render/types.js +1 -0
  131. package/dist/bot/routers/command-router.js +73 -0
  132. package/dist/bot/routers/message-router.js +150 -0
  133. package/dist/bot/services/attach-presentation.js +40 -0
  134. package/dist/bot/services/event-subscription-service.js +1116 -0
  135. package/dist/bot/services/project-switch-presentation.js +26 -0
  136. package/dist/bot/streaming/compact-progress-streamer.js +162 -0
  137. package/dist/bot/{utils → streaming}/finalize-assistant-response.js +9 -3
  138. package/dist/bot/streaming/response-streamer.js +16 -1
  139. package/dist/cli.js +4 -4
  140. package/dist/config.js +9 -18
  141. package/dist/i18n/ar.js +31 -6
  142. package/dist/i18n/de.js +32 -7
  143. package/dist/i18n/en.js +32 -7
  144. package/dist/i18n/es.js +32 -7
  145. package/dist/i18n/fr.js +32 -7
  146. package/dist/i18n/ru.js +32 -7
  147. package/dist/i18n/zh.js +32 -7
  148. package/dist/index.js +1 -1
  149. package/dist/opencode/ready-refresh.js +2 -2
  150. package/dist/{service → runtime/service}/manager.js +2 -2
  151. package/dist/runtime/service/types.js +1 -0
  152. package/package.json +1 -1
  153. package/dist/bot/commands/ls.js +0 -445
  154. package/dist/bot/commands/open.js +0 -319
  155. package/dist/bot/commands/tts.js +0 -13
  156. package/dist/bot/commands/worktree.js +0 -201
  157. package/dist/bot/handlers/permission.js +0 -235
  158. package/dist/bot/utils/browser-roots.js +0 -140
  159. package/dist/bot/utils/file-tree.js +0 -92
  160. package/dist/bot/utils/send-tts-response.js +0 -37
  161. package/dist/bot/utils/switch-project.js +0 -45
  162. package/dist/bot/utils/telegram-file-url.js +0 -11
  163. package/dist/interaction/busy.js +0 -8
  164. package/dist/interaction/cleanup.js +0 -32
  165. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  166. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  167. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  168. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  169. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  170. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  171. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  172. /package/dist/{model/types.js → app/types/model.js} +0 -0
  173. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  174. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  175. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  176. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  177. /package/dist/{question/types.js → app/types/session.js} +0 -0
  178. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  179. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  180. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  181. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  182. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  183. /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
  184. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  185. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  186. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  187. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  188. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  189. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  190. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  191. /package/dist/{telegram → bot}/render/validator.js +0 -0
  192. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  193. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
@@ -1,15 +1,15 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
- import { getGitWorktreeContext } from "../../git/worktree.js";
3
- import { getCurrentSession } from "../../session/manager.js";
4
- import { getCurrentProject, isTtsEnabled } from "../../settings/manager.js";
5
- import { fetchCurrentAgent } from "../../agent/manager.js";
6
- import { getAgentDisplayName } from "../../agent/types.js";
7
- import { fetchCurrentModel } from "../../model/manager.js";
8
- import { keyboardManager } from "../../keyboard/manager.js";
9
- import { pinnedMessageManager } from "../../pinned/manager.js";
2
+ import { getGitWorktreeContext } from "../../app/services/worktree-service.js";
3
+ import { getCurrentSession } from "../../app/services/session-service.js";
4
+ import { getCurrentProject, getTtsMode } from "../../app/stores/settings-store.js";
5
+ import { fetchCurrentAgent } from "../../app/services/agent-selection-service.js";
6
+ import { fetchCurrentModel } from "../../app/services/model-selection-service.js";
7
+ import { getAgentDisplayName } from "../../app/types/agent.js";
8
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
9
+ import { pinnedMessageManager } from "../pinned/pinned-message-manager.js";
10
10
  import { logger } from "../../utils/logger.js";
11
11
  import { t } from "../../i18n/index.js";
12
- import { sendBotText } from "../utils/telegram-text.js";
12
+ import { sendBotText } from "../messages/telegram-text.js";
13
13
  export async function statusCommand(ctx) {
14
14
  try {
15
15
  const { data, error } = await opencodeClient.global.health();
@@ -22,8 +22,13 @@ export async function statusCommand(ctx) {
22
22
  if (data.version) {
23
23
  message += `${t("status.line.version", { version: data.version })}\n`;
24
24
  }
25
+ const ttsMode = getTtsMode();
25
26
  message += `${t("status.line.tts", {
26
- tts: isTtsEnabled() ? t("status.tts.on") : t("status.tts.off"),
27
+ tts: ttsMode === "off"
28
+ ? t("status.tts.off")
29
+ : ttsMode === "all"
30
+ ? t("status.tts.all")
31
+ : t("status.tts.auto"),
27
32
  })}\n`;
28
33
  // Add agent information
29
34
  const currentAgent = await fetchCurrentAgent();
@@ -1,35 +1,17 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { InlineKeyboard } from "grammy";
3
2
  import { config } from "../../config.js";
4
3
  import { getDateLocale, t } from "../../i18n/index.js";
5
- import { interactionManager } from "../../interaction/manager.js";
6
- import { getStoredModel } from "../../model/manager.js";
7
- import { getCurrentProject } from "../../settings/manager.js";
8
- import { taskCreationManager } from "../../scheduled-task/creation-manager.js";
9
- import { parseTaskSchedule } from "../../scheduled-task/schedule-parser.js";
10
- import { addScheduledTask, listScheduledTasks } from "../../scheduled-task/store.js";
11
- import { scheduledTaskRuntime } from "../../scheduled-task/runtime.js";
12
- import { createScheduledTaskModel, } from "../../scheduled-task/types.js";
4
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
5
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
6
+ import { getCurrentProject } from "../../app/stores/settings-store.js";
7
+ import { taskCreationManager } from "../../app/managers/scheduled-task-creation-manager.js";
8
+ import { parseTaskSchedule } from "../../app/services/scheduled-task-schedule-parser-service.js";
9
+ import { addScheduledTask, listScheduledTasks } from "../../app/stores/scheduled-task-store.js";
10
+ import { scheduledTaskRuntime } from "../../app/services/scheduled-task-runtime-service.js";
11
+ import { buildCancelKeyboard, buildRetryScheduleKeyboard } from "../menus/scheduled-task-menu.js";
12
+ import { createScheduledTaskModel, } from "../../app/types/scheduled-task.js";
13
13
  import { logger } from "../../utils/logger.js";
14
- const TASK_RETRY_SCHEDULE_CALLBACK = "task:retry-schedule";
15
- const TASK_CANCEL_CALLBACK = "task:cancel";
16
14
  const TASK_PROMPT_PREVIEW_LENGTH = 100;
17
- function buildRetryScheduleKeyboard() {
18
- return new InlineKeyboard()
19
- .text(t("task.button.retry_schedule"), TASK_RETRY_SCHEDULE_CALLBACK)
20
- .text(t("task.button.cancel"), TASK_CANCEL_CALLBACK);
21
- }
22
- function buildCancelKeyboard() {
23
- return new InlineKeyboard().text(t("task.button.cancel"), TASK_CANCEL_CALLBACK);
24
- }
25
- function getCallbackMessageId(ctx) {
26
- const message = ctx.callbackQuery?.message;
27
- if (!message || !("message_id" in message)) {
28
- return null;
29
- }
30
- const messageId = message.message_id;
31
- return typeof messageId === "number" ? messageId : null;
32
- }
33
15
  function clearTaskInteraction(reason) {
34
16
  const state = interactionManager.getSnapshot();
35
17
  if (state?.kind === "task") {
@@ -179,13 +161,6 @@ function buildTaskInteractionMetadata(stage, projectId, projectWorktree, preview
179
161
  function isTaskInteraction(state) {
180
162
  return state?.kind === "task";
181
163
  }
182
- function isTaskCallbackActive(flowState, messageId) {
183
- return [
184
- flowState.scheduleRequestMessageId,
185
- flowState.previewMessageId,
186
- flowState.promptRequestMessageId,
187
- ].includes(messageId);
188
- }
189
164
  async function deleteMessageIfPresent(ctx, messageId) {
190
165
  if (!ctx.chat || typeof messageId !== "number") {
191
166
  return;
@@ -244,53 +219,6 @@ export async function taskCommand(ctx) {
244
219
  });
245
220
  taskCreationManager.setScheduleRequestMessageId(message.message_id);
246
221
  }
247
- export async function handleTaskCallback(ctx) {
248
- const data = ctx.callbackQuery?.data;
249
- if (data !== TASK_RETRY_SCHEDULE_CALLBACK && data !== TASK_CANCEL_CALLBACK) {
250
- return false;
251
- }
252
- const flowState = taskCreationManager.getState();
253
- const interactionState = interactionManager.getSnapshot();
254
- const callbackMessageId = getCallbackMessageId(ctx);
255
- if (!flowState ||
256
- !isTaskInteraction(interactionState) ||
257
- callbackMessageId === null ||
258
- !isTaskCallbackActive(flowState, callbackMessageId)) {
259
- if (!flowState && isTaskInteraction(interactionState)) {
260
- clearTaskInteraction("task_retry_inactive_state");
261
- }
262
- await ctx.answerCallbackQuery({ text: t("task.inactive_callback"), show_alert: true });
263
- return true;
264
- }
265
- if (data === TASK_CANCEL_CALLBACK) {
266
- await ctx.answerCallbackQuery({ text: t("task.cancel_callback") });
267
- await deleteMessageIfPresent(ctx, flowState.scheduleRequestMessageId);
268
- await deleteMessageIfPresent(ctx, flowState.previewMessageId);
269
- await deleteMessageIfPresent(ctx, flowState.promptRequestMessageId);
270
- clearTaskFlow("task_cancelled");
271
- await ctx.reply(t("task.cancelled"));
272
- return true;
273
- }
274
- if (!taskCreationManager.isWaitingForPrompt() ||
275
- callbackMessageId !== flowState.previewMessageId) {
276
- await ctx.answerCallbackQuery({ text: t("task.inactive_callback"), show_alert: true });
277
- return true;
278
- }
279
- taskCreationManager.resetSchedule();
280
- interactionManager.transition({
281
- kind: "task",
282
- expectedInput: "text",
283
- metadata: buildTaskInteractionMetadata("awaiting_schedule", flowState.projectId, flowState.projectWorktree),
284
- });
285
- await ctx.answerCallbackQuery({ text: t("task.retry_schedule_callback") });
286
- await deleteMessageIfPresent(ctx, flowState.promptRequestMessageId);
287
- await deleteMessageIfPresent(ctx, flowState.previewMessageId);
288
- const message = await ctx.reply(t("task.prompt.schedule"), {
289
- reply_markup: buildCancelKeyboard(),
290
- });
291
- taskCreationManager.setScheduleRequestMessageId(message.message_id);
292
- return true;
293
- }
294
222
  export async function handleTaskTextInput(ctx) {
295
223
  const text = ctx.message?.text;
296
224
  if (!text || text.startsWith("/")) {
@@ -0,0 +1,40 @@
1
+ import { t } from "../../i18n/index.js";
2
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
3
+ import { listScheduledTasks } from "../../app/stores/scheduled-task-store.js";
4
+ import { buildTaskListKeyboard } from "../menus/scheduled-task-menu.js";
5
+ import { logger } from "../../utils/logger.js";
6
+ function sortTasks(tasks) {
7
+ return [...tasks].sort((left, right) => {
8
+ const leftNextRun = left.nextRunAt ? Date.parse(left.nextRunAt) : Number.POSITIVE_INFINITY;
9
+ const rightNextRun = right.nextRunAt ? Date.parse(right.nextRunAt) : Number.POSITIVE_INFINITY;
10
+ if (leftNextRun !== rightNextRun) {
11
+ return leftNextRun - rightNextRun;
12
+ }
13
+ return left.createdAt.localeCompare(right.createdAt);
14
+ });
15
+ }
16
+ export async function taskListCommand(ctx) {
17
+ try {
18
+ const tasks = sortTasks(listScheduledTasks());
19
+ if (tasks.length === 0) {
20
+ await ctx.reply(t("tasklist.empty"));
21
+ return;
22
+ }
23
+ const message = await ctx.reply(t("tasklist.select"), {
24
+ reply_markup: buildTaskListKeyboard(tasks),
25
+ });
26
+ interactionManager.start({
27
+ kind: "custom",
28
+ expectedInput: "callback",
29
+ metadata: {
30
+ flow: "tasklist",
31
+ stage: "list",
32
+ messageId: message.message_id,
33
+ },
34
+ });
35
+ }
36
+ catch (error) {
37
+ logger.error("[TaskList] Failed to open task list", error);
38
+ await ctx.reply(t("tasklist.load_error"));
39
+ }
40
+ }
@@ -0,0 +1,47 @@
1
+ import { getGitWorktreeContext } from "../../app/services/worktree-service.js";
2
+ import { isForegroundBusy } from "../../app/services/run-control-service.js";
3
+ import { getCurrentProject } from "../../app/stores/settings-store.js";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { t } from "../../i18n/index.js";
6
+ import { replyWithInlineMenu } from "../menus/inline-menu.js";
7
+ import { buildWorktreeMenuView } from "../menus/worktree-selection-menu.js";
8
+ import { replyBusyBlocked } from "../messages/busy-blocked-renderer.js";
9
+ async function loadCurrentWorktreeContext() {
10
+ const currentProject = getCurrentProject();
11
+ if (!currentProject) {
12
+ return { currentProject: null, context: null };
13
+ }
14
+ const context = await getGitWorktreeContext(currentProject.worktree);
15
+ return { currentProject, context };
16
+ }
17
+ export async function worktreeCommand(ctx) {
18
+ try {
19
+ if (isForegroundBusy()) {
20
+ await replyBusyBlocked(ctx);
21
+ return;
22
+ }
23
+ const { currentProject, context } = await loadCurrentWorktreeContext();
24
+ if (!currentProject) {
25
+ await ctx.reply(t("worktree.project_not_selected"));
26
+ return;
27
+ }
28
+ if (!context) {
29
+ await ctx.reply(t("worktree.not_git_repo"));
30
+ return;
31
+ }
32
+ if (context.worktrees.length === 0) {
33
+ await ctx.reply(t("worktree.empty"));
34
+ return;
35
+ }
36
+ const { text, keyboard } = buildWorktreeMenuView(context.worktrees, 0);
37
+ await replyWithInlineMenu(ctx, {
38
+ menuKind: "worktree",
39
+ text,
40
+ keyboard,
41
+ });
42
+ }
43
+ catch (error) {
44
+ logger.error("[Bot] Error loading worktrees:", error);
45
+ await ctx.reply(t("worktree.fetch_error"));
46
+ }
47
+ }
@@ -1,8 +1,8 @@
1
1
  import { config } from "../../config.js";
2
2
  import { processUserPrompt } from "./prompt.js";
3
- import { downloadTelegramFile, toDataUri, isTextMimeType, isFileSizeAllowed, } from "../utils/file-download.js";
4
- import { getModelCapabilities, supportsInput } from "../../model/capabilities.js";
5
- import { getStoredModel } from "../../model/manager.js";
3
+ import { downloadTelegramFile, toDataUri, isTextMimeType, isFileSizeAllowed, } from "../../app/services/file-download-service.js";
4
+ import { getModelCapabilities, supportsInput } from "../../app/services/model-capabilities-service.js";
5
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
6
6
  import { logger } from "../../utils/logger.js";
7
7
  import { t } from "../../i18n/index.js";
8
8
  export async function handleDocumentMessage(ctx, deps) {
@@ -1,9 +1,9 @@
1
1
  import { config } from "../../config.js";
2
2
  import { t } from "../../i18n/index.js";
3
- import { getModelCapabilities, supportsInput } from "../../model/capabilities.js";
4
- import { getStoredModel } from "../../model/manager.js";
3
+ import { getModelCapabilities, supportsInput } from "../../app/services/model-capabilities-service.js";
4
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
5
5
  import { logger } from "../../utils/logger.js";
6
- import { downloadTelegramFile, isFileSizeAllowed, isTextMimeType, toDataUri, } from "../utils/file-download.js";
6
+ import { downloadTelegramFile, isFileSizeAllowed, isTextMimeType, toDataUri, } from "../../app/services/file-download-service.js";
7
7
  import { processUserPrompt } from "./prompt.js";
8
8
  const DEFAULT_MEDIA_GROUP_DEBOUNCE_MS = 1_000;
9
9
  export class MediaGroupAttachmentHandler {
@@ -0,0 +1,44 @@
1
+ import { downloadTelegramFile, toDataUri } from "../../app/services/file-download-service.js";
2
+ import { getModelCapabilities, supportsInput } from "../../app/services/model-capabilities-service.js";
3
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { logger } from "../../utils/logger.js";
6
+ import { processUserPrompt } from "./prompt.js";
7
+ export async function handlePhotoMessage(ctx, deps) {
8
+ const photos = ctx.message?.photo;
9
+ if (!photos || photos.length === 0) {
10
+ return;
11
+ }
12
+ const caption = ctx.message.caption || "";
13
+ const largestPhoto = photos[photos.length - 1];
14
+ const downloadFile = deps.downloadFile ?? downloadTelegramFile;
15
+ const getCapabilities = deps.getModelCapabilities ?? getModelCapabilities;
16
+ const getStored = deps.getStoredModel ?? getStoredModel;
17
+ const processPrompt = deps.processPrompt ?? processUserPrompt;
18
+ try {
19
+ const storedModel = getStored();
20
+ const capabilities = await getCapabilities(storedModel.providerID, storedModel.modelID);
21
+ if (!supportsInput(capabilities, "image")) {
22
+ logger.warn(`[Bot] Model ${storedModel.providerID}/${storedModel.modelID} doesn't support image input`);
23
+ await ctx.reply(t("bot.photo_model_no_image"));
24
+ if (caption.trim().length > 0) {
25
+ await processPrompt(ctx, caption, deps);
26
+ }
27
+ return;
28
+ }
29
+ await ctx.reply(t("bot.photo_downloading"));
30
+ const downloadedFile = await downloadFile(ctx.api, largestPhoto.file_id);
31
+ const filePart = {
32
+ type: "file",
33
+ mime: "image/jpeg",
34
+ filename: "photo.jpg",
35
+ url: toDataUri(downloadedFile.buffer, "image/jpeg"),
36
+ };
37
+ logger.info(`[Bot] Sending photo (${downloadedFile.buffer.length} bytes) with prompt`);
38
+ await processPrompt(ctx, caption, deps, [filePart]);
39
+ }
40
+ catch (err) {
41
+ logger.error("[Bot] Error handling photo message:", err);
42
+ await ctx.reply(t("bot.photo_download_error"));
43
+ }
44
+ }
@@ -1,25 +1,25 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
- import { clearSession, getCurrentSession, setCurrentSession } from "../../session/manager.js";
3
- import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
4
- import { getCurrentProject, isTtsEnabled } from "../../settings/manager.js";
5
- import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
6
- import { getStoredModel } from "../../model/manager.js";
7
- import { formatVariantForButton } from "../../variant/manager.js";
8
- import { createMainKeyboard } from "../utils/keyboard.js";
9
- import { keyboardManager } from "../../keyboard/manager.js";
10
- import { pinnedMessageManager } from "../../pinned/manager.js";
11
- import { summaryAggregator } from "../../summary/aggregator.js";
2
+ import { clearSession, getCurrentSession, setCurrentSession, } from "../../app/services/session-service.js";
3
+ import { ingestSessionInfoForCache } from "../../app/services/session-cache-service.js";
4
+ import { getCurrentProject, getTtsMode } from "../../app/stores/settings-store.js";
5
+ import { getStoredAgent, resolveProjectAgent } from "../../app/services/agent-selection-service.js";
6
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
7
+ import { formatVariantForButton } from "../../app/services/variant-selection-service.js";
8
+ import { createMainKeyboard } from "../keyboards/main-reply-keyboard.js";
9
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
10
+ import { pinnedMessageManager } from "../pinned/pinned-message-manager.js";
11
+ import { summaryAggregator } from "../../app/managers/summary-aggregation-manager.js";
12
12
  import { stopEventListening } from "../../opencode/events.js";
13
- import { interactionManager } from "../../interaction/manager.js";
14
- import { clearAllInteractionState } from "../../interaction/cleanup.js";
13
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
14
+ import { clearAllInteractionState } from "../../app/managers/interaction-manager.js";
15
15
  import { safeBackgroundTask } from "../../utils/safe-background-task.js";
16
16
  import { formatErrorDetails } from "../../utils/error-format.js";
17
17
  import { logger } from "../../utils/logger.js";
18
18
  import { t } from "../../i18n/index.js";
19
- import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
20
- import { assistantRunState } from "../assistant-run-state.js";
21
- import { attachToSession, detachAttachedSession, markAttachedSessionBusy, markAttachedSessionIdle, } from "../../attach/service.js";
22
- import { externalUserInputSuppressionManager } from "../../external-input/suppression.js";
19
+ import { foregroundSessionState } from "../../app/managers/foreground-session-state-manager.js";
20
+ import { assistantRunState } from "../../app/managers/assistant-run-state-manager.js";
21
+ import { attachToSession, detachAttachedSession, markAttachedSessionBusy, markAttachedSessionIdle, } from "../../app/services/attach-service.js";
22
+ import { externalUserInputSuppressionManager } from "../../app/managers/external-input-suppression-manager.js";
23
23
  /** Module-level references for async callbacks that don't have ctx. */
24
24
  let botInstance = null;
25
25
  let chatIdInstance = null;
@@ -91,7 +91,7 @@ async function resetMismatchedSessionContext() {
91
91
  */
92
92
  export async function processUserPrompt(ctx, text, deps, fileParts = [], options = {}) {
93
93
  const { bot, ensureEventSubscription } = deps;
94
- const responseMode = options.responseMode ?? (isTtsEnabled() ? "text_and_tts" : "text_only");
94
+ const responseMode = options.responseMode ?? (getTtsMode() === "all" ? "text_and_tts" : "text_only");
95
95
  const currentProject = getCurrentProject();
96
96
  if (!currentProject) {
97
97
  await ctx.reply(t("bot.project_not_selected"));
@@ -0,0 +1,61 @@
1
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
2
+ import { t } from "../../i18n/index.js";
3
+ import { clearCommandsInteraction, executeCommand, parseCommandsMetadata, } from "../callbacks/command-catalog-callback-handler.js";
4
+ import { clearSkillsInteraction, executeSkill, parseSkillsMetadata, } from "../callbacks/skills-catalog-callback-handler.js";
5
+ export async function handleCommandTextArguments(ctx, deps) {
6
+ const text = ctx.message?.text;
7
+ if (!text || text.startsWith("/")) {
8
+ return false;
9
+ }
10
+ const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
11
+ if (!metadata || metadata.stage !== "confirm") {
12
+ return false;
13
+ }
14
+ const argumentsText = text.trim();
15
+ if (!argumentsText) {
16
+ await ctx.reply(t("commands.arguments_empty"));
17
+ return true;
18
+ }
19
+ clearCommandsInteraction("commands_arguments_submitted");
20
+ if (ctx.chat) {
21
+ await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
22
+ }
23
+ await executeCommand(ctx, deps, {
24
+ projectDirectory: metadata.projectDirectory,
25
+ commandName: metadata.commandName,
26
+ argumentsText,
27
+ });
28
+ return true;
29
+ }
30
+ export async function handleSkillTextArguments(ctx, deps) {
31
+ const text = ctx.message?.text;
32
+ if (!text || text.startsWith("/")) {
33
+ return false;
34
+ }
35
+ const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
36
+ if (!metadata || metadata.stage !== "confirm") {
37
+ return false;
38
+ }
39
+ const argumentsText = text.trim();
40
+ if (!argumentsText) {
41
+ await ctx.reply(t("skills.arguments_empty"));
42
+ return true;
43
+ }
44
+ clearSkillsInteraction("skills_arguments_submitted");
45
+ if (ctx.chat) {
46
+ await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
47
+ }
48
+ await executeSkill(ctx, deps, {
49
+ projectDirectory: metadata.projectDirectory,
50
+ skillName: metadata.skillName,
51
+ argumentsText,
52
+ });
53
+ return true;
54
+ }
55
+ export async function handleCatalogTextArguments(ctx, deps) {
56
+ const handledCommandArgs = await handleCommandTextArguments(ctx, deps);
57
+ if (handledCommandArgs) {
58
+ return true;
59
+ }
60
+ return handleSkillTextArguments(ctx, deps);
61
+ }
@@ -0,0 +1,29 @@
1
+ import { InputFile } from "grammy";
2
+ import { prepareTtsResponseForSession } from "../../app/services/tts-service.js";
3
+ import { t } from "../../i18n/index.js";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { consumePromptResponseMode } from "./prompt.js";
6
+ export async function sendTtsResponseForSession({ api, sessionId, chatId, text, consumeResponseMode: consumeResponseModeImpl = consumePromptResponseMode, isTtsConfigured, synthesizeSpeech, }) {
7
+ try {
8
+ const prepared = await prepareTtsResponseForSession({
9
+ sessionId,
10
+ text,
11
+ consumeResponseMode: consumeResponseModeImpl,
12
+ isTtsConfigured,
13
+ synthesizeSpeech,
14
+ });
15
+ if (!prepared.shouldSend) {
16
+ return false;
17
+ }
18
+ await api.sendAudio(chatId, new InputFile(prepared.speech.buffer, prepared.speech.filename));
19
+ logger.info(`[TTS] Sent audio reply for session ${sessionId}`);
20
+ return true;
21
+ }
22
+ catch (error) {
23
+ logger.warn(`[TTS] Failed to send audio reply for session ${sessionId}`, error);
24
+ await api.sendMessage(chatId, t("tts.failed")).catch((sendError) => {
25
+ logger.warn(`[TTS] Failed to send audio error message for session ${sessionId}`, sendError);
26
+ });
27
+ return false;
28
+ }
29
+ }
@@ -4,11 +4,12 @@ import { URL } from "node:url";
4
4
  import { HttpsProxyAgent } from "https-proxy-agent";
5
5
  import { SocksProxyAgent } from "socks-proxy-agent";
6
6
  import { config } from "../../config.js";
7
- import { isSttConfigured, transcribeAudio } from "../../stt/client.js";
7
+ import { getTtsMode } from "../../app/stores/settings-store.js";
8
+ import { isSttConfigured, transcribeAudio, } from "../../app/services/stt-service.js";
8
9
  import { processUserPrompt } from "./prompt.js";
9
10
  import { logger } from "../../utils/logger.js";
10
11
  import { t } from "../../i18n/index.js";
11
- import { buildTelegramFileUrl } from "../utils/telegram-file-url.js";
12
+ import { buildTelegramFileUrl } from "../../app/services/file-download-service.js";
12
13
  const TELEGRAM_DOWNLOAD_TIMEOUT_MS = 30_000;
13
14
  const TELEGRAM_DOWNLOAD_MAX_REDIRECTS = 3;
14
15
  let telegramDownloadAgent;
@@ -169,7 +170,9 @@ export async function handleVoiceMessage(ctx, deps) {
169
170
  textForLLM = `${llmNote}\n${recognizedText}`;
170
171
  }
171
172
  // Process the recognized text as a prompt
172
- await processPrompt(ctx, textForLLM, deps);
173
+ const currentTtsMode = getTtsMode();
174
+ const responseMode = currentTtsMode === "all" || currentTtsMode === "auto" ? "text_and_tts" : "text_only";
175
+ await processPrompt(ctx, textForLLM, deps, [], { responseMode });
173
176
  }
174
177
  catch (err) {
175
178
  const errorMessage = err instanceof Error ? err.message : "unknown error";