@grinev/opencode-telegram-bot 0.21.1 → 0.21.2

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 (179) hide show
  1. package/.env.example +11 -1
  2. package/README.md +18 -5
  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} +4 -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} +3 -3
  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} +5 -5
  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} +3 -3
  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 +110 -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/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  62. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  63. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  64. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  65. package/dist/bot/commands/command-catalog-command.js +42 -0
  66. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  67. package/dist/bot/commands/ls-command.js +48 -0
  68. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  69. package/dist/bot/commands/messages-command.js +59 -0
  70. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  71. package/dist/bot/commands/open-command.js +45 -0
  72. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  73. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  74. package/dist/bot/commands/projects-command.js +32 -0
  75. package/dist/bot/commands/rename-command.js +33 -0
  76. package/dist/bot/commands/sessions-command.js +42 -0
  77. package/dist/bot/commands/skills-catalog-command.js +42 -0
  78. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  79. package/dist/bot/commands/{status.js → status-command.js} +9 -9
  80. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  81. package/dist/bot/commands/tasklist-command.js +40 -0
  82. package/dist/bot/commands/{tts.js → tts-command.js} +2 -2
  83. package/dist/bot/commands/worktree-command.js +47 -0
  84. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  85. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  86. package/dist/bot/handlers/photo-handler.js +44 -0
  87. package/dist/bot/handlers/prompt.js +16 -16
  88. package/dist/bot/handlers/text-message-handler.js +61 -0
  89. package/dist/bot/handlers/tts-response-handler.js +29 -0
  90. package/dist/bot/handlers/{voice.js → voice-handler.js} +2 -2
  91. package/dist/bot/index.js +27 -1179
  92. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  93. package/dist/bot/keyboards/keyboard-types.js +1 -0
  94. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  95. package/dist/bot/menus/agent-selection-menu.js +54 -0
  96. package/dist/bot/menus/command-catalog-menu.js +101 -0
  97. package/dist/bot/menus/context-control-menu.js +33 -0
  98. package/dist/bot/menus/file-browser-menu.js +265 -0
  99. package/dist/bot/{handlers → menus}/inline-menu.js +4 -33
  100. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  101. package/dist/bot/menus/message-history-menu.js +107 -0
  102. package/dist/bot/menus/model-selection-menu.js +66 -0
  103. package/dist/bot/menus/permission-menu.js +116 -0
  104. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  105. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  106. package/dist/bot/menus/rename-menu.js +6 -0
  107. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  108. package/dist/bot/menus/session-selection-menu.js +115 -0
  109. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  110. package/dist/bot/menus/variant-selection-menu.js +67 -0
  111. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  112. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  113. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  114. package/dist/bot/messages/external-user-input-notification.js +22 -0
  115. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  116. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  117. package/dist/bot/messages/summary-message-formatter.js +130 -0
  118. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  119. package/dist/bot/middleware/interaction-guard.js +2 -2
  120. package/dist/bot/middleware/unknown-command.js +1 -1
  121. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  122. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  123. package/dist/bot/pinned/pinned-message-types.js +1 -0
  124. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  125. package/dist/bot/render/types.js +1 -0
  126. package/dist/bot/routers/command-router.js +73 -0
  127. package/dist/bot/routers/message-router.js +150 -0
  128. package/dist/bot/services/attach-presentation.js +40 -0
  129. package/dist/bot/services/event-subscription-service.js +842 -0
  130. package/dist/bot/services/project-switch-presentation.js +26 -0
  131. package/dist/cli.js +4 -4
  132. package/dist/config.js +9 -3
  133. package/dist/index.js +1 -1
  134. package/dist/opencode/ready-refresh.js +2 -2
  135. package/dist/{service → runtime/service}/manager.js +2 -2
  136. package/dist/runtime/service/types.js +1 -0
  137. package/package.json +1 -1
  138. package/dist/bot/commands/ls.js +0 -445
  139. package/dist/bot/commands/open.js +0 -319
  140. package/dist/bot/commands/worktree.js +0 -201
  141. package/dist/bot/handlers/permission.js +0 -235
  142. package/dist/bot/utils/browser-roots.js +0 -140
  143. package/dist/bot/utils/file-tree.js +0 -92
  144. package/dist/bot/utils/send-tts-response.js +0 -37
  145. package/dist/bot/utils/switch-project.js +0 -45
  146. package/dist/bot/utils/telegram-file-url.js +0 -11
  147. package/dist/interaction/busy.js +0 -8
  148. package/dist/interaction/cleanup.js +0 -32
  149. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  150. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  151. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  152. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  153. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  154. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  155. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  156. /package/dist/{model/types.js → app/types/model.js} +0 -0
  157. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  158. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  159. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  160. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  161. /package/dist/{question/types.js → app/types/session.js} +0 -0
  162. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  163. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  164. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  165. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  166. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  167. /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
  168. /package/dist/bot/{utils → messages}/thinking-message.js +0 -0
  169. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  170. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  171. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  172. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  173. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  174. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  175. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  176. /package/dist/{telegram → bot}/render/validator.js +0 -0
  177. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  178. /package/dist/bot/{utils → streaming}/finalize-assistant-response.js +0 -0
  179. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
package/.env.example CHANGED
@@ -80,6 +80,9 @@ OPENCODE_MODEL_ID=big-pickle
80
80
  # If exceeded, the bot stops waiting for the result and marks the run as failed.
81
81
  # SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES=120
82
82
 
83
+ # Send scheduled task result/error messages without Telegram push notifications (default: false)
84
+ # SCHEDULED_TASK_DISABLE_NOTIFICATION=false
85
+
83
86
  # Response streaming mode: "edit" (default) or "draft" (default: edit)
84
87
  # edit = uses editMessageText to update messages incrementally (may flicker)
85
88
  # draft = uses sendMessageDraft (Bot API 9.5+) for smooth animated draft previews,
@@ -141,7 +144,7 @@ OPENCODE_MODEL_ID=big-pickle
141
144
 
142
145
  # Text-to-Speech credentials (optional)
143
146
  # TTS reply behavior is controlled globally with /tts and persisted in settings.json.
144
- # Provider: "openai" (default) or "google".
147
+ # Provider: "openai" (default), "elevenlabs", or "google".
145
148
  #
146
149
  # --- OpenAI-compatible (default) ---
147
150
  # Set TTS_API_URL and TTS_API_KEY to any OpenAI-compatible TTS endpoint.
@@ -150,6 +153,13 @@ OPENCODE_MODEL_ID=big-pickle
150
153
  # TTS_MODEL=gpt-4o-mini-tts
151
154
  # TTS_VOICE=alloy
152
155
  #
156
+ # --- ElevenLabs ---
157
+ # TTS_PROVIDER=elevenlabs
158
+ # TTS_API_URL=https://api.elevenlabs.io/v1
159
+ # TTS_API_KEY=
160
+ # TTS_MODEL=eleven_flash_v2_5
161
+ # TTS_VOICE=21m00Tcm4TlvDq8ikWAM
162
+ #
153
163
  # --- Google Cloud TTS ---
154
164
  # 1. Create a project at https://console.cloud.google.com
155
165
  # 2. Enable "Cloud Text-to-Speech API"
package/README.md CHANGED
@@ -23,6 +23,8 @@ Languages: English (`en`), العربية (`ar`), Deutsch (`de`), Español (`es`
23
23
  <img src="assets/screencast.gif" width="45%" alt="OpenCode Telegram Bot screencast" />
24
24
  </p>
25
25
 
26
+ > I use [boardown](https://github.com/grinev/boardown), my open-source Markdown-based task board, to plan and track this project. It stores tasks in plain `.md` files and can be used as a VS Code extension or a desktop app.
27
+
26
28
  ## Features
27
29
 
28
30
  - **Remote coding** — send prompts to OpenCode from anywhere, receive complete results with code sent as files
@@ -225,6 +227,7 @@ When installed via npm, the configuration wizard handles the initial setup. The
225
227
  | `COMMANDS_LIST_LIMIT` | Items per page in `/commands` and `/skills` | No | `10` |
226
228
  | `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
227
229
  | `SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES` | Maximum time the bot waits for one scheduled task run before marking it failed | No | `120` |
230
+ | `SCHEDULED_TASK_DISABLE_NOTIFICATION` | Send scheduled task result/error messages without Telegram push notifications | No | `false` |
228
231
  | `BASH_TOOL_DISPLAY_MAX_LENGTH` | Maximum displayed length for `bash` tool commands in Telegram summaries; longer commands are truncated | No | `128` |
229
232
  | `SERVICE_MESSAGES_INTERVAL_SEC` | Service messages interval (thinking + tool calls); keep `>=2` to avoid Telegram rate limits, `0` = immediate | No | `5` |
230
233
  | `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
@@ -239,11 +242,11 @@ When installed via npm, the configuration wizard handles the initial setup. The
239
242
  | `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
240
243
  | `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
241
244
  | `STT_NOTE_PROMPT` | Optional note prepended to the LLM prompt as `[Note: ...]` for voice transcriptions; empty / `false` / `0` disable it | No | — |
242
- | `TTS_PROVIDER` | TTS provider: `openai` for OpenAI-compatible APIs or `google` for Google Cloud TTS | No | `openai` |
243
- | `TTS_API_URL` | OpenAI-compatible TTS API base URL | No | — |
244
- | `TTS_API_KEY` | OpenAI-compatible TTS API key | No | — |
245
- | `TTS_MODEL` | OpenAI-compatible TTS model name passed to `/audio/speech` | No | `gpt-4o-mini-tts` |
246
- | `TTS_VOICE` | TTS voice name. Defaults to `alloy` for OpenAI-compatible APIs and `en-US-Studio-O` for Google Cloud TTS | No | provider-specific |
245
+ | `TTS_PROVIDER` | TTS provider: `openai` for OpenAI-compatible APIs, `elevenlabs` for ElevenLabs, or `google` for Google Cloud TTS | No | `openai` |
246
+ | `TTS_API_URL` | TTS API base URL for OpenAI-compatible APIs or ElevenLabs | No | — |
247
+ | `TTS_API_KEY` | TTS API key for OpenAI-compatible APIs or ElevenLabs | No | — |
248
+ | `TTS_MODEL` | TTS model name. Passed as `model` for OpenAI-compatible APIs and `model_id` for ElevenLabs | No | `gpt-4o-mini-tts` |
249
+ | `TTS_VOICE` | TTS voice name or ElevenLabs voice ID. Defaults to `alloy`, `21m00Tcm4TlvDq8ikWAM`, or `en-US-Studio-O` by provider | No | provider-specific |
247
250
  | `GOOGLE_APPLICATION_CREDENTIALS` | Path to a Google Cloud service account JSON key file for `TTS_PROVIDER=google` | No | — |
248
251
  | `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
249
252
  | `LOG_RETENTION` | Number of log files to keep: launch files in `sources`, daily files in `installed` | No | `10` |
@@ -323,6 +326,16 @@ TTS_MODEL=gpt-4o-mini-tts
323
326
  TTS_VOICE=alloy
324
327
  ```
325
328
 
329
+ ElevenLabs TTS configuration example:
330
+
331
+ ```env
332
+ TTS_PROVIDER=elevenlabs
333
+ TTS_API_URL=https://api.elevenlabs.io/v1
334
+ TTS_API_KEY=your-elevenlabs-api-key
335
+ TTS_MODEL=eleven_flash_v2_5
336
+ TTS_VOICE=21m00Tcm4TlvDq8ikWAM
337
+ ```
338
+
326
339
  Google Cloud TTS configuration example:
327
340
 
328
341
  ```env
@@ -1,22 +1,23 @@
1
1
  import fs from "node:fs/promises";
2
2
  import { readFile } from "node:fs/promises";
3
- import { cleanupBotRuntime, createBot } from "../bot/index.js";
4
- import { config } from "../config.js";
5
- import { opencodeAutoRestartService } from "../opencode/auto-restart.js";
6
- import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../opencode/ready-refresh.js";
7
- import { loadSettings } from "../settings/manager.js";
8
- import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
9
- import { reconcileStoredModelSelection } from "../model/manager.js";
10
- import { getRuntimeMode } from "../runtime/mode.js";
11
- import { getRuntimePaths } from "../runtime/paths.js";
12
- import { clearServiceStateFile } from "../service/manager.js";
13
- import { getServiceStateFilePathFromEnv, isServiceChildProcess } from "../service/runtime.js";
14
- import { getLogFilePath, initializeLogger, logger } from "../utils/logger.js";
15
- import { safeBackgroundTask } from "../utils/safe-background-task.js";
3
+ import { cleanupBotRuntime, createBot } from "../../bot/index.js";
4
+ import { createScheduledTaskDeliverySender } from "../../bot/messages/scheduled-task-delivery.js";
5
+ import { config } from "../../config.js";
6
+ import { opencodeAutoRestartService } from "../../opencode/auto-restart.js";
7
+ import { notifyOpencodeReadyIfHealthy, registerOpenCodeReadyRefreshHandler, } from "../../opencode/ready-refresh.js";
8
+ import { loadSettings } from "../stores/settings-store.js";
9
+ import { scheduledTaskRuntime } from "../services/scheduled-task-runtime-service.js";
10
+ import { reconcileStoredModelSelection } from "../services/model-selection-service.js";
11
+ import { getRuntimeMode } from "../../runtime/mode.js";
12
+ import { getRuntimePaths } from "../../runtime/paths.js";
13
+ import { clearServiceStateFile } from "../../runtime/service/manager.js";
14
+ import { getServiceStateFilePathFromEnv, isServiceChildProcess } from "../../runtime/service/env.js";
15
+ import { getLogFilePath, initializeLogger, logger } from "../../utils/logger.js";
16
+ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
16
17
  const SHUTDOWN_TIMEOUT_MS = 5000;
17
18
  async function getBotVersion() {
18
19
  try {
19
- const packageJsonPath = new URL("../../package.json", import.meta.url);
20
+ const packageJsonPath = new URL("../../../package.json", import.meta.url);
20
21
  const packageJsonContent = await readFile(packageJsonPath, "utf-8");
21
22
  const packageJson = JSON.parse(packageJsonContent);
22
23
  return packageJson.version ?? "unknown";
@@ -43,7 +44,7 @@ export async function startBotApp() {
43
44
  await reconcileStoredModelSelection();
44
45
  registerOpenCodeReadyRefreshHandler();
45
46
  const bot = createBot();
46
- await scheduledTaskRuntime.initialize(bot);
47
+ await scheduledTaskRuntime.initialize(bot, createScheduledTaskDeliverySender(bot.api, config.telegram.allowedUserId));
47
48
  safeBackgroundTask({
48
49
  taskName: "app.opencodeStartup",
49
50
  task: async () => {
@@ -1,4 +1,4 @@
1
- import { getAgentDisplayName } from "../../agent/types.js";
1
+ import { getAgentDisplayName } from "../types/agent.js";
2
2
  function formatDuration(elapsedMs) {
3
3
  const safeElapsedMs = Math.max(0, Math.round(elapsedMs));
4
4
  const totalSeconds = Math.floor(safeElapsedMs / 1000);
@@ -1,6 +1,11 @@
1
- import { formatModelDisplayName } from "../pinned/format.js";
2
- import { t } from "../i18n/index.js";
3
- import { formatCompactToolInfo } from "./formatter.js";
1
+ import { t } from "../../i18n/index.js";
2
+ import { formatCompactToolInfo } from "./summary-formatter.js";
3
+ function formatModelDisplayName(providerID, modelID) {
4
+ if (providerID && modelID) {
5
+ return `${providerID}/${modelID}`;
6
+ }
7
+ return t("pinned.unknown");
8
+ }
4
9
  function shouldPreferInputDetails(tool, input) {
5
10
  if (!input) {
6
11
  return false;
@@ -1,12 +1,8 @@
1
1
  import * as path from "path";
2
- import { config } from "../config.js";
3
- import { logger } from "../utils/logger.js";
4
- import { t } from "../i18n/index.js";
5
- import { getCurrentProject } from "../settings/manager.js";
6
- import { convertToTelegramMarkdownV2 } from "./markdown-to-telegram-v2.js";
7
- import { normalizeMarkdownForTelegramRendering } from "../telegram/render/markdown-normalizer.js";
8
- const TELEGRAM_MESSAGE_LIMIT = 4096;
9
- const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
2
+ import { config } from "../../config.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { getCurrentProject } from "../stores/settings-store.js";
10
6
  function truncateWithEllipsis(text, maxLength) {
11
7
  if (text.length <= maxLength) {
12
8
  return text;
@@ -16,50 +12,6 @@ function truncateWithEllipsis(text, maxLength) {
16
12
  }
17
13
  return `${text.slice(0, maxLength - 3).trimEnd()}...`;
18
14
  }
19
- function endsWithOddTrailingBackslashes(text, start, end) {
20
- let backslashCount = 0;
21
- for (let index = end - 1; index >= start; index--) {
22
- if (text[index] !== "\\") {
23
- break;
24
- }
25
- backslashCount += 1;
26
- }
27
- return backslashCount % 2 === 1;
28
- }
29
- function resolveSplitEndIndex(text, currentIndex, maxLength, options) {
30
- const hardLimit = Math.min(text.length, currentIndex + maxLength);
31
- if (hardLimit >= text.length) {
32
- return text.length;
33
- }
34
- let endIndex = hardLimit;
35
- const breakPoint = text.lastIndexOf("\n", endIndex);
36
- if (breakPoint > currentIndex) {
37
- endIndex = breakPoint + 1;
38
- }
39
- if (!options?.avoidTrailingMarkdownEscape) {
40
- return endIndex;
41
- }
42
- while (endIndex > currentIndex && endsWithOddTrailingBackslashes(text, currentIndex, endIndex)) {
43
- endIndex -= 1;
44
- }
45
- return endIndex > currentIndex ? endIndex : hardLimit;
46
- }
47
- function splitText(text, maxLength, options) {
48
- const parts = [];
49
- let currentIndex = 0;
50
- while (currentIndex < text.length) {
51
- const endIndex = resolveSplitEndIndex(text, currentIndex, maxLength, options);
52
- if (endIndex <= currentIndex) {
53
- const fallbackEnd = Math.min(text.length, currentIndex + 1);
54
- parts.push(text.slice(currentIndex, fallbackEnd));
55
- currentIndex = fallbackEnd;
56
- continue;
57
- }
58
- parts.push(text.slice(currentIndex, endIndex));
59
- currentIndex = endIndex;
60
- }
61
- return parts;
62
- }
63
15
  export function normalizePathForDisplay(filePath) {
64
16
  const normalizedPath = filePath.replace(/\\/g, "/");
65
17
  const project = getCurrentProject();
@@ -81,86 +33,6 @@ export function normalizePathForDisplay(filePath) {
81
33
  }
82
34
  return normalizedPath;
83
35
  }
84
- export function formatSummary(text) {
85
- return formatSummaryWithMode(text, config.bot.messageFormatMode);
86
- }
87
- export function escapePlainTextForTelegramMarkdownV2(text) {
88
- return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
89
- }
90
- function formatMarkdownForTelegram(text) {
91
- try {
92
- const preprocessed = normalizeMarkdownForTelegramRendering(text);
93
- return escapeMarkdownV2PipesOutsideCode(convertToTelegramMarkdownV2(preprocessed));
94
- }
95
- catch (error) {
96
- logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
97
- return text;
98
- }
99
- }
100
- function escapeMarkdownV2PipesOutsideCode(text) {
101
- let result = "";
102
- let index = 0;
103
- let inInlineCode = false;
104
- let inCodeFence = false;
105
- while (index < text.length) {
106
- if (text.startsWith("```", index)) {
107
- result += "```";
108
- index += 3;
109
- inCodeFence = !inCodeFence;
110
- continue;
111
- }
112
- const char = text[index];
113
- if (!inCodeFence && char === "`") {
114
- inInlineCode = !inInlineCode;
115
- result += char;
116
- index += 1;
117
- continue;
118
- }
119
- if (!inCodeFence && !inInlineCode && char === "|" && text[index - 1] !== "\\") {
120
- result += "\\|";
121
- index += 1;
122
- continue;
123
- }
124
- result += char;
125
- index += 1;
126
- }
127
- return result;
128
- }
129
- export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
130
- if (!text || text.trim().length === 0) {
131
- return [];
132
- }
133
- const normalizedMaxLength = Math.max(1, Math.floor(maxLength));
134
- const rawTextLimit = mode === "raw" ? Math.max(1, normalizedMaxLength - "```\n\n```".length) : normalizedMaxLength;
135
- const parts = splitText(text, rawTextLimit);
136
- const formattedParts = [];
137
- for (const part of parts) {
138
- const trimmed = part.trim();
139
- if (!trimmed) {
140
- continue;
141
- }
142
- if (mode === "markdown") {
143
- const converted = formatMarkdownForTelegram(trimmed);
144
- const convertedParts = splitText(converted, normalizedMaxLength, {
145
- avoidTrailingMarkdownEscape: true,
146
- });
147
- for (const convertedPart of convertedParts) {
148
- const normalizedPart = convertedPart.trim();
149
- if (normalizedPart) {
150
- formattedParts.push(normalizedPart);
151
- }
152
- }
153
- continue;
154
- }
155
- if (parts.length > 1) {
156
- formattedParts.push(`\`\`\`\n${trimmed}\n\`\`\``);
157
- }
158
- else {
159
- formattedParts.push(trimmed);
160
- }
161
- }
162
- return formattedParts;
163
- }
164
36
  function getToolDetails(tool, input) {
165
37
  if (!input) {
166
38
  return "";
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  export class ToolMessageBatcher {
3
3
  sendText;
4
4
  sendFile;
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class AssistantRunState {
3
3
  runs = new Map();
4
4
  startRun(sessionId, info) {
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class AttachManager {
3
3
  state = null;
4
4
  attach(sessionId, directory) {
@@ -1,5 +1,5 @@
1
- import { isScheduledTaskSessionIgnored } from "../scheduled-task/session-ignore.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { isScheduledTaskSessionIgnored } from "../services/scheduled-task-session-ignore-service.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  class BackgroundSessionTracker {
4
4
  directory = null;
5
5
  onNotification = null;
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class ForegroundSessionState {
3
3
  activeSessions = new Map();
4
4
  markBusy(sessionId, directory) {
@@ -1,4 +1,8 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { permissionManager } from "./permission-manager.js";
2
+ import { questionManager } from "./question-manager.js";
3
+ import { renameManager } from "./rename-manager.js";
4
+ import { taskCreationManager } from "./scheduled-task-creation-manager.js";
5
+ import { logger } from "../../utils/logger.js";
2
6
  export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/abort", "/detach"];
3
7
  function normalizeCommand(command) {
4
8
  const trimmed = command.trim().toLowerCase();
@@ -104,3 +108,29 @@ class InteractionManager {
104
108
  }
105
109
  }
106
110
  export const interactionManager = new InteractionManager();
111
+ export function clearAllInteractionState(reason) {
112
+ const questionActive = questionManager.isActive();
113
+ const permissionActive = permissionManager.isActive();
114
+ const renameActive = renameManager.isWaitingForName();
115
+ const taskCreationActive = taskCreationManager.isActive();
116
+ const interactionSnapshot = interactionManager.getSnapshot();
117
+ questionManager.clear();
118
+ permissionManager.clear();
119
+ renameManager.clear();
120
+ taskCreationManager.clear();
121
+ interactionManager.clear(reason);
122
+ const hasAnyActiveState = questionActive ||
123
+ permissionActive ||
124
+ renameActive ||
125
+ taskCreationActive ||
126
+ interactionSnapshot !== null;
127
+ const message = `[InteractionCleanup] Cleared state: reason=${reason}, ` +
128
+ `questionActive=${questionActive}, permissionActive=${permissionActive}, ` +
129
+ `renameActive=${renameActive}, taskCreationActive=${taskCreationActive}, ` +
130
+ `interactionKind=${interactionSnapshot?.kind || "none"}`;
131
+ if (hasAnyActiveState) {
132
+ logger.info(message);
133
+ return;
134
+ }
135
+ logger.debug(message);
136
+ }
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class PermissionManager {
3
3
  state = {
4
4
  requestsByMessageId: new Map(),
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class QuestionManager {
3
3
  state = {
4
4
  questions: [],
@@ -1,4 +1,4 @@
1
- import { logger } from "../utils/logger.js";
1
+ import { logger } from "../../utils/logger.js";
2
2
  class RenameManager {
3
3
  state = {
4
4
  isWaiting: false,
@@ -1,5 +1,5 @@
1
- import { cloneParsedTaskSchedule, cloneScheduledTaskModel } from "./types.js";
2
- import { logger } from "../utils/logger.js";
1
+ import { cloneParsedTaskSchedule, cloneScheduledTaskModel } from "../types/scheduled-task.js";
2
+ import { logger } from "../../utils/logger.js";
3
3
  function cloneState(state) {
4
4
  return {
5
5
  ...state,
@@ -1,6 +1,6 @@
1
- import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
2
- import { logger } from "../utils/logger.js";
3
- import { getCurrentProject } from "../settings/manager.js";
1
+ import { normalizePathForDisplay, prepareCodeFile } from "../formatters/summary-formatter.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { getCurrentProject } from "../stores/settings-store.js";
4
4
  function extractFirstUpdatedFileFromTitle(title) {
5
5
  for (const rawLine of title.split("\n")) {
6
6
  const line = rawLine.trim();
@@ -1,8 +1,7 @@
1
- import { opencodeClient } from "../opencode/client.js";
2
- import { getCurrentProject } from "../settings/manager.js";
3
- import { getCurrentSession } from "../session/manager.js";
4
- import { getCurrentAgent, setCurrentAgent } from "../settings/manager.js";
5
- import { logger } from "../utils/logger.js";
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { getCurrentAgent, getCurrentProject, setCurrentAgent } from "../stores/settings-store.js";
3
+ import { getCurrentSession } from "./session-service.js";
4
+ import { logger } from "../../utils/logger.js";
6
5
  /**
7
6
  * Get list of available agents from OpenCode API
8
7
  * @returns Array of available agents (filtered by mode and hidden flag)
@@ -1,17 +1,17 @@
1
- import { opencodeClient } from "../opencode/client.js";
2
- import { isOpencodeServerHealthy } from "../opencode/ready-refresh.js";
3
- import { summaryAggregator } from "../summary/aggregator.js";
4
- import { pinnedMessageManager } from "../pinned/manager.js";
5
- import { keyboardManager } from "../keyboard/manager.js";
6
- import { questionManager } from "../question/manager.js";
7
- import { permissionManager } from "../permission/manager.js";
8
- import { showCurrentQuestion } from "../bot/handlers/question.js";
9
- import { showPermissionRequest } from "../bot/handlers/permission.js";
10
- import { getCurrentSession } from "../session/manager.js";
11
- import { getCurrentProject } from "../settings/manager.js";
12
- import { attachManager } from "./manager.js";
13
- import { logger } from "../utils/logger.js";
14
- import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { isOpencodeServerHealthy } from "../../opencode/ready-refresh.js";
3
+ import { summaryAggregator } from "../managers/summary-aggregation-manager.js";
4
+ import { questionManager } from "../managers/question-manager.js";
5
+ import { permissionManager } from "../managers/permission-manager.js";
6
+ import { getCurrentSession } from "./session-service.js";
7
+ import { getCurrentProject } from "../stores/settings-store.js";
8
+ import { attachManager } from "../managers/attach-manager.js";
9
+ import { logger } from "../../utils/logger.js";
10
+ import { isExpectedOpencodeUnavailableError } from "../../utils/opencode-error.js";
11
+ let attachPresentation = null;
12
+ export function configureAttachPresentation(deps) {
13
+ attachPresentation = deps;
14
+ }
15
15
  function getAttachBusyStatus(sessionId, statuses) {
16
16
  if (!statuses || typeof statuses !== "object") {
17
17
  return false;
@@ -19,36 +19,12 @@ function getAttachBusyStatus(sessionId, statuses) {
19
19
  const sessionStatus = statuses[sessionId];
20
20
  return sessionStatus?.type === "busy";
21
21
  }
22
- async function ensureAttachPinnedSession({ api, chatId, session, forceFullRestore = false, }) {
23
- if (!pinnedMessageManager.isInitialized()) {
24
- pinnedMessageManager.initialize(api, chatId);
25
- }
26
- keyboardManager.initialize(api, chatId);
27
- const pinnedState = pinnedMessageManager.getState();
28
- if (pinnedState.sessionId === session.id && pinnedState.messageId) {
29
- if (forceFullRestore) {
30
- await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
31
- }
32
- return;
33
- }
34
- if (pinnedState.messageId && pinnedState.sessionId === null) {
35
- await pinnedMessageManager.restoreExistingSession(session.id, session.title);
36
- }
37
- else {
38
- await pinnedMessageManager.onSessionChange(session.id, session.title);
39
- }
40
- await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
41
- const contextInfo = pinnedMessageManager.getContextInfo();
42
- if (contextInfo) {
43
- keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
44
- }
45
- }
46
22
  async function syncPinnedAttachState() {
47
- if (!pinnedMessageManager.isInitialized()) {
23
+ if (!attachPresentation) {
48
24
  return;
49
25
  }
50
26
  const attached = attachManager.getSnapshot();
51
- await pinnedMessageManager.setAttachState(attached !== null, attached?.busy ?? false);
27
+ await attachPresentation.syncAttachState(attached !== null, attached?.busy ?? false);
52
28
  }
53
29
  async function restorePendingQuestion(bot, chatId, sessionId, directory) {
54
30
  const { data, error } = await opencodeClient.question.list({
@@ -64,11 +40,11 @@ async function restorePendingQuestion(bot, chatId, sessionId, directory) {
64
40
  return false;
65
41
  }
66
42
  const pendingQuestion = data.find((request) => request.sessionID === sessionId);
67
- if (!pendingQuestion) {
43
+ if (!pendingQuestion || !attachPresentation) {
68
44
  return false;
69
45
  }
70
46
  questionManager.startQuestions(pendingQuestion.questions, pendingQuestion.id);
71
- await showCurrentQuestion(bot.api, chatId);
47
+ await attachPresentation.showCurrentQuestion(bot.api, chatId);
72
48
  return true;
73
49
  }
74
50
  async function restorePendingPermissions(bot, chatId, sessionId, directory) {
@@ -85,15 +61,18 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
85
61
  return 0;
86
62
  }
87
63
  const pendingPermissions = data.filter((request) => request.sessionID === sessionId);
64
+ if (!attachPresentation) {
65
+ return 0;
66
+ }
88
67
  for (const request of pendingPermissions) {
89
- await showPermissionRequest(bot.api, chatId, request);
68
+ await attachPresentation.showPermissionRequest(bot.api, chatId, request);
90
69
  }
91
70
  return pendingPermissions.length;
92
71
  }
93
72
  export async function attachToSession(deps) {
94
73
  const { bot, chatId, session, ensureEventSubscription, forceFullRestore = false } = deps;
95
74
  const alreadyAttached = attachManager.isAttachedSession(session.id, session.directory);
96
- await ensureAttachPinnedSession({
75
+ await attachPresentation?.ensurePinnedSession({
97
76
  api: bot.api,
98
77
  chatId,
99
78
  session,
@@ -1,19 +1,22 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
- import { foregroundSessionState, } from "../../scheduled-task/foreground-state.js";
3
- import { scheduledTaskRuntime } from "../../scheduled-task/runtime.js";
4
- import { attachManager } from "../../attach/manager.js";
5
- import { markAttachedSessionBusy, markAttachedSessionIdle } from "../../attach/service.js";
6
- import { assistantRunState } from "../assistant-run-state.js";
7
- import { clearPromptResponseMode } from "../handlers/prompt.js";
2
+ import { foregroundSessionState, } from "../managers/foreground-session-state-manager.js";
3
+ import { scheduledTaskRuntime } from "./scheduled-task-runtime-service.js";
4
+ import { attachManager } from "../managers/attach-manager.js";
5
+ import { markAttachedSessionBusy, markAttachedSessionIdle } from "./attach-service.js";
6
+ import { assistantRunState } from "../managers/assistant-run-state-manager.js";
8
7
  import { logger } from "../../utils/logger.js";
9
8
  const RECONCILE_MIN_INTERVAL_MS = 10_000;
10
9
  const FOREGROUND_BUSY_RECONCILE_GRACE_MS = 2_000;
11
10
  const inFlightDirectories = new Set();
12
11
  const lastReconcileAtByDirectory = new Map();
13
12
  let responseStreamerInstance = null;
13
+ let clearPromptResponseModeForReconciliation = null;
14
14
  export function setResponseStreamerForReconciliation(streamer) {
15
15
  responseStreamerInstance = streamer;
16
16
  }
17
+ export function setPromptResponseModeClearerForReconciliation(clearer) {
18
+ clearPromptResponseModeForReconciliation = clearer;
19
+ }
17
20
  function getReconciliationTargets(directory) {
18
21
  const foregroundBusySessions = foregroundSessionState
19
22
  .getBusySessions()
@@ -38,7 +41,7 @@ function isWithinForegroundBusyGracePeriod(session, now) {
38
41
  async function clearForegroundBusySession(sessionId, reason) {
39
42
  foregroundSessionState.markIdle(sessionId);
40
43
  assistantRunState.clearRun(sessionId, reason);
41
- clearPromptResponseMode(sessionId);
44
+ clearPromptResponseModeForReconciliation?.(sessionId);
42
45
  }
43
46
  export async function reconcileBusyStateNow(directory, now = Date.now()) {
44
47
  if (!directory) {
@@ -118,4 +121,6 @@ export async function reconcileBusyState(directory, now = Date.now()) {
118
121
  export function __resetBusyReconciliationForTests() {
119
122
  inFlightDirectories.clear();
120
123
  lastReconcileAtByDirectory.clear();
124
+ responseStreamerInstance = null;
125
+ clearPromptResponseModeForReconciliation = null;
121
126
  }
@@ -0,0 +1,21 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ function normalizeDirectoryForCommandApi(directory) {
3
+ return directory.replace(/\\/g, "/");
4
+ }
5
+ export async function loadCommandCatalog(projectDirectory) {
6
+ const { data, error } = await opencodeClient.command.list({
7
+ directory: normalizeDirectoryForCommandApi(projectDirectory),
8
+ });
9
+ if (error || !data) {
10
+ throw error || new Error("No command data received");
11
+ }
12
+ return data
13
+ .filter((command) => {
14
+ const source = command.source;
15
+ return (typeof command.name === "string" && command.name.trim().length > 0 && source === "command");
16
+ })
17
+ .map((command) => ({
18
+ name: command.name,
19
+ description: command.description,
20
+ }));
21
+ }