@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,319 +0,0 @@
1
- import { InlineKeyboard } from "grammy";
2
- import path from "node:path";
3
- import { appendInlineMenuCancelButton, ensureActiveInlineMenu } from "../handlers/inline-menu.js";
4
- import { interactionManager } from "../../interaction/manager.js";
5
- import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
6
- import { pathToDisplayPath, scanDirectory, buildEntryLabel, buildTreeHeader, isScanError, MAX_ENTRIES_PER_PAGE, } from "../utils/file-tree.js";
7
- import { getBrowserRoots, isWithinAllowedRoot, isAllowedRoot } from "../utils/browser-roots.js";
8
- import { upsertSessionDirectory } from "../../session/cache-manager.js";
9
- import { getProjectByWorktree } from "../../project/manager.js";
10
- import { switchToProject } from "../utils/switch-project.js";
11
- import { logger } from "../../utils/logger.js";
12
- import { t } from "../../i18n/index.js";
13
- const CALLBACK_PREFIX = "open:";
14
- const CALLBACK_NAV_PREFIX = "open:nav:";
15
- const CALLBACK_SELECT_PREFIX = "open:sel:";
16
- const CALLBACK_PAGE_PREFIX = "open:pg:";
17
- const CALLBACK_ROOTS = "open:roots";
18
- const MAX_BUTTON_LABEL_LENGTH = 64;
19
- /**
20
- * Separator used inside pagination callback data between the encoded path
21
- * reference and the page number. We avoid `:` because it appears in Windows
22
- * drive letters (e.g. `C:\`) and is already used as a prefix delimiter.
23
- */
24
- const PAGE_SEPARATOR = "|";
25
- function truncateLabel(label, maxLen = MAX_BUTTON_LABEL_LENGTH) {
26
- if (label.length <= maxLen) {
27
- return label;
28
- }
29
- return label.slice(0, Math.max(0, maxLen - 3)) + "...";
30
- }
31
- /**
32
- * Encode a path into callback data. Telegram limits callback_data to 64 bytes.
33
- * Long absolute paths can exceed this, so we encode them as a compact index
34
- * when necessary. The index is stored in a module-level map that lives for the
35
- * duration of the current inline menu interaction.
36
- */
37
- const pathIndex = new Map();
38
- let pathCounter = 0;
39
- /** Clear the path index. Exported so it can be called on menu cancel/cleanup. */
40
- export function clearOpenPathIndex() {
41
- pathIndex.clear();
42
- pathCounter = 0;
43
- }
44
- /**
45
- * @param prefix Callback-data prefix that precedes the path.
46
- * @param fullPath Absolute path to encode.
47
- * @param reserveBytes Extra bytes to reserve for a suffix that will be
48
- * appended *after* the returned value (e.g. the page separator + digits in
49
- * pagination callbacks). The total callback_data must stay ≤ 64 bytes.
50
- */
51
- function encodePathForCallback(prefix, fullPath, reserveBytes = 0) {
52
- const naive = `${prefix}${fullPath}`;
53
- if (Buffer.byteLength(naive, "utf-8") + reserveBytes <= 64) {
54
- return naive;
55
- }
56
- // Use a short numeric key instead
57
- const key = `#${pathCounter++}`;
58
- pathIndex.set(key, fullPath);
59
- return `${prefix}${key}`;
60
- }
61
- function decodePathFromCallback(prefix, data) {
62
- if (!data.startsWith(prefix)) {
63
- return null;
64
- }
65
- const raw = data.slice(prefix.length);
66
- if (raw.startsWith("#")) {
67
- return pathIndex.get(raw) ?? null;
68
- }
69
- return raw;
70
- }
71
- /**
72
- * Encode a pagination callback. The path part goes through the same 64-byte
73
- * safe encoding used by nav/select callbacks, followed by a separator and
74
- * the page number.
75
- *
76
- * We reserve bytes for the page suffix so the 64-byte check inside
77
- * `encodePathForCallback` accounts for the complete final callback length.
78
- */
79
- function encodePaginationCallback(currentPath, page) {
80
- const pageSuffix = `${PAGE_SEPARATOR}${page}`;
81
- const reserveBytes = Buffer.byteLength(pageSuffix, "utf-8");
82
- const pathRef = encodePathForCallback(CALLBACK_PAGE_PREFIX, currentPath, reserveBytes);
83
- return `${pathRef}${pageSuffix}`;
84
- }
85
- /**
86
- * Decode a pagination callback into { path, page } or null on failure.
87
- */
88
- function decodePaginationCallback(data) {
89
- if (!data.startsWith(CALLBACK_PAGE_PREFIX)) {
90
- return null;
91
- }
92
- const payload = data.slice(CALLBACK_PAGE_PREFIX.length);
93
- const sepIndex = payload.lastIndexOf(PAGE_SEPARATOR);
94
- if (sepIndex < 0) {
95
- return null;
96
- }
97
- const pathRef = payload.slice(0, sepIndex);
98
- const pageNum = Number.parseInt(payload.slice(sepIndex + 1), 10);
99
- if (Number.isNaN(pageNum)) {
100
- return null;
101
- }
102
- // Resolve indexed path references
103
- const resolvedPath = pathRef.startsWith("#") ? (pathIndex.get(pathRef) ?? null) : pathRef;
104
- if (resolvedPath === null) {
105
- return null;
106
- }
107
- return { path: resolvedPath, page: pageNum };
108
- }
109
- function buildBrowseKeyboard(entries, currentPath, hasParent, page, totalCount) {
110
- const keyboard = new InlineKeyboard();
111
- const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
112
- // Directory entries
113
- for (const entry of entries) {
114
- const label = truncateLabel(buildEntryLabel(entry));
115
- keyboard.text(label, encodePathForCallback(CALLBACK_NAV_PREFIX, entry.fullPath)).row();
116
- }
117
- // Navigation: Up + Back to roots
118
- // Suppress "Up" when at an allowed root (don't let user navigate above it)
119
- const atRoot = isAllowedRoot(currentPath);
120
- const showUp = hasParent && !atRoot;
121
- const roots = getBrowserRoots();
122
- const showRoots = roots.length > 1;
123
- if (showUp || showRoots) {
124
- if (showUp) {
125
- const parentPath = path.dirname(currentPath);
126
- keyboard.text(t("open.back"), encodePathForCallback(CALLBACK_NAV_PREFIX, parentPath));
127
- }
128
- if (showRoots) {
129
- keyboard.text(t("open.roots"), CALLBACK_ROOTS);
130
- }
131
- keyboard.row();
132
- }
133
- // Pagination
134
- if (totalPages > 1) {
135
- if (page > 0) {
136
- keyboard.text(t("open.prev_page"), encodePaginationCallback(currentPath, page - 1));
137
- }
138
- if (page < totalPages - 1) {
139
- keyboard.text(t("open.next_page"), encodePaginationCallback(currentPath, page + 1));
140
- }
141
- keyboard.row();
142
- }
143
- // Select current folder
144
- keyboard
145
- .text(t("open.select_current"), encodePathForCallback(CALLBACK_SELECT_PREFIX, currentPath))
146
- .row();
147
- // Cancel
148
- appendInlineMenuCancelButton(keyboard, "open");
149
- return keyboard;
150
- }
151
- async function renderBrowseView(dirPath, page = 0) {
152
- const result = await scanDirectory(dirPath, page);
153
- if (isScanError(result)) {
154
- return { error: result.error };
155
- }
156
- const { entries, totalCount, page: clampedPage, currentPath, displayPath, hasParent } = result;
157
- const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
158
- const header = buildTreeHeader(displayPath, totalCount, clampedPage, totalPages);
159
- const keyboard = buildBrowseKeyboard(entries, currentPath, hasParent, clampedPage, totalCount);
160
- return { text: header, keyboard };
161
- }
162
- function buildRootsKeyboard() {
163
- const keyboard = new InlineKeyboard();
164
- const roots = getBrowserRoots();
165
- for (const root of roots) {
166
- const label = truncateLabel(`📂 ${pathToDisplayPath(root)}`);
167
- keyboard.text(label, encodePathForCallback(CALLBACK_NAV_PREFIX, root)).row();
168
- }
169
- appendInlineMenuCancelButton(keyboard, "open");
170
- return keyboard;
171
- }
172
- export async function openCommand(ctx) {
173
- try {
174
- if (isForegroundBusy()) {
175
- await replyBusyBlocked(ctx);
176
- return;
177
- }
178
- // Reset path index for new interaction
179
- clearOpenPathIndex();
180
- const roots = getBrowserRoots();
181
- let text;
182
- let keyboard;
183
- if (roots.length === 1) {
184
- // Single root — navigate directly into it
185
- const view = await renderBrowseView(roots[0]);
186
- if ("error" in view) {
187
- await ctx.reply(t("open.scan_error", { error: view.error }));
188
- return;
189
- }
190
- text = view.text;
191
- keyboard = view.keyboard;
192
- }
193
- else {
194
- // Multiple roots — show root selection
195
- text = t("open.select_root");
196
- keyboard = buildRootsKeyboard();
197
- }
198
- const message = await ctx.reply(text, { reply_markup: keyboard });
199
- interactionManager.start({
200
- kind: "inline",
201
- expectedInput: "callback",
202
- metadata: {
203
- menuKind: "open",
204
- messageId: message.message_id,
205
- },
206
- });
207
- }
208
- catch (error) {
209
- logger.error("[Bot] Error opening directory browser:", error);
210
- await ctx.reply(t("open.open_error"));
211
- }
212
- }
213
- export async function handleOpenCallback(ctx, deps = {}) {
214
- const data = ctx.callbackQuery?.data;
215
- if (!data || !data.startsWith(CALLBACK_PREFIX)) {
216
- return false;
217
- }
218
- if (isForegroundBusy()) {
219
- await replyBusyBlocked(ctx);
220
- return true;
221
- }
222
- const isActiveMenu = await ensureActiveInlineMenu(ctx, "open");
223
- if (!isActiveMenu) {
224
- return true;
225
- }
226
- try {
227
- // Back to root selection (multi-root mode)
228
- if (data === CALLBACK_ROOTS) {
229
- await showRoots(ctx);
230
- return true;
231
- }
232
- // Navigate into a directory (including "up")
233
- const navPath = decodePathFromCallback(CALLBACK_NAV_PREFIX, data);
234
- if (navPath !== null) {
235
- if (!isWithinAllowedRoot(navPath)) {
236
- await ctx.answerCallbackQuery({ text: t("open.access_denied") });
237
- return true;
238
- }
239
- await navigateTo(ctx, navPath);
240
- return true;
241
- }
242
- // Pagination
243
- const pageInfo = decodePaginationCallback(data);
244
- if (pageInfo !== null) {
245
- if (!isWithinAllowedRoot(pageInfo.path)) {
246
- await ctx.answerCallbackQuery({ text: t("open.access_denied") });
247
- return true;
248
- }
249
- await navigateTo(ctx, pageInfo.path, pageInfo.page);
250
- return true;
251
- }
252
- // Select directory as project
253
- const selectPath = decodePathFromCallback(CALLBACK_SELECT_PREFIX, data);
254
- if (selectPath !== null) {
255
- if (!isWithinAllowedRoot(selectPath)) {
256
- await ctx.answerCallbackQuery({ text: t("open.access_denied") });
257
- return true;
258
- }
259
- await selectDirectory(ctx, selectPath, deps);
260
- return true;
261
- }
262
- return false;
263
- }
264
- catch (error) {
265
- logger.error("[Bot] Error handling open callback:", error);
266
- await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
267
- return true;
268
- }
269
- }
270
- async function showRoots(ctx) {
271
- const text = t("open.select_root");
272
- const keyboard = buildRootsKeyboard();
273
- await ctx.answerCallbackQuery();
274
- await ctx.editMessageText(text, { reply_markup: keyboard });
275
- }
276
- async function navigateTo(ctx, dirPath, page = 0) {
277
- const view = await renderBrowseView(dirPath, page);
278
- if ("error" in view) {
279
- await ctx.answerCallbackQuery({ text: view.error });
280
- return;
281
- }
282
- await ctx.answerCallbackQuery();
283
- await ctx.editMessageText(view.text, {
284
- reply_markup: view.keyboard,
285
- });
286
- }
287
- async function selectDirectory(ctx, directory, deps = {}) {
288
- const displayPath = pathToDisplayPath(directory);
289
- try {
290
- logger.info(`[Bot] Adding project directory: ${directory}`);
291
- // Register directory in the session cache first — getProjectByWorktree
292
- // needs the entry to exist so it can resolve the project. If the
293
- // subsequent switch fails, the entry stays in the cache, which is
294
- // acceptable: it's a real directory the user explicitly selected and
295
- // will simply appear in /projects for retry.
296
- await upsertSessionDirectory(directory, Date.now());
297
- const projectInfo = await getProjectByWorktree(directory);
298
- const selectedProjectInfo = { ...projectInfo, name: displayPath };
299
- const replyKeyboard = deps.ensureEventSubscription
300
- ? await switchToProject(ctx, selectedProjectInfo, "open_project_selected", {
301
- ensureEventSubscription: deps.ensureEventSubscription,
302
- })
303
- : await switchToProject(ctx, selectedProjectInfo, "open_project_selected");
304
- await ctx.answerCallbackQuery();
305
- await ctx.reply(t("open.selected", { project: displayPath }), {
306
- reply_markup: replyKeyboard,
307
- });
308
- // Clean up the inline menu message
309
- await ctx.deleteMessage();
310
- // Clear path index after selection
311
- clearOpenPathIndex();
312
- logger.info(`[Bot] Project added and selected: ${displayPath}`);
313
- }
314
- catch (error) {
315
- logger.error("[Bot] Error selecting directory:", error);
316
- await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
317
- await ctx.reply(t("open.select_error"));
318
- }
319
- }
@@ -1,13 +0,0 @@
1
- import { isTtsConfigured } from "../../tts/client.js";
2
- import { isTtsEnabled, setTtsEnabled } from "../../settings/manager.js";
3
- import { t } from "../../i18n/index.js";
4
- export async function ttsCommand(ctx) {
5
- const enabled = !isTtsEnabled();
6
- if (enabled && !isTtsConfigured()) {
7
- await ctx.reply(t("tts.not_configured"));
8
- return;
9
- }
10
- setTtsEnabled(enabled);
11
- const message = enabled ? t("tts.enabled") : t("tts.disabled");
12
- await ctx.reply(message);
13
- }
@@ -1,201 +0,0 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { config } from "../../config.js";
3
- import { getGitWorktreeContext } from "../../git/worktree.js";
4
- import { clearAllInteractionState } from "../../interaction/cleanup.js";
5
- import { getProjectByWorktree } from "../../project/manager.js";
6
- import { upsertSessionDirectory } from "../../session/cache-manager.js";
7
- import { getCurrentProject } from "../../settings/manager.js";
8
- import { logger } from "../../utils/logger.js";
9
- import { t } from "../../i18n/index.js";
10
- import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
11
- import { switchToProject } from "../utils/switch-project.js";
12
- import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
13
- import { buildProjectButtonLabel, calculateProjectsPaginationRange } from "./projects.js";
14
- const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
15
- const WORKTREE_CALLBACK_PREFIX = "worktree:";
16
- const WORKTREE_PAGE_CALLBACK_PREFIX = "worktree:page:";
17
- function formatWorktreeButtonLabel(label, isActive) {
18
- const prefix = isActive ? "✅ " : "";
19
- const availableLength = MAX_INLINE_BUTTON_LABEL_LENGTH - prefix.length;
20
- if (label.length <= availableLength) {
21
- return `${prefix}${label}`;
22
- }
23
- return `${prefix}${label.slice(0, Math.max(0, availableLength - 3))}...`;
24
- }
25
- function buildWorktreeButtonLabel(index, entry) {
26
- return buildProjectButtonLabel(index, entry.path);
27
- }
28
- function parseWorktreePageCallback(data) {
29
- if (!data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
30
- return null;
31
- }
32
- const rawPage = data.slice(WORKTREE_PAGE_CALLBACK_PREFIX.length);
33
- if (!/^\d+$/.test(rawPage)) {
34
- return null;
35
- }
36
- return Number.parseInt(rawPage, 10);
37
- }
38
- function parseWorktreeIndexCallback(data) {
39
- if (!data.startsWith(WORKTREE_CALLBACK_PREFIX) ||
40
- data.startsWith(WORKTREE_PAGE_CALLBACK_PREFIX)) {
41
- return null;
42
- }
43
- const rawIndex = data.slice(WORKTREE_CALLBACK_PREFIX.length);
44
- if (!/^\d+$/.test(rawIndex)) {
45
- return null;
46
- }
47
- return Number.parseInt(rawIndex, 10);
48
- }
49
- function buildWorktreeMenuText(page, totalPages) {
50
- const baseText = t("worktree.select_with_current");
51
- if (totalPages <= 1) {
52
- return baseText;
53
- }
54
- return `${baseText}\n\n${t("projects.page_indicator", {
55
- current: String(page + 1),
56
- total: String(totalPages),
57
- })}`;
58
- }
59
- function buildWorktreeKeyboard(worktrees, page) {
60
- const keyboard = new InlineKeyboard();
61
- const pageSize = config.bot.projectsListLimit;
62
- const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(worktrees.length, page, pageSize);
63
- worktrees.slice(startIndex, endIndex).forEach((entry, index) => {
64
- const label = buildWorktreeButtonLabel(startIndex + index, entry);
65
- keyboard
66
- .text(formatWorktreeButtonLabel(label, entry.isCurrent), `${WORKTREE_CALLBACK_PREFIX}${startIndex + index}`)
67
- .row();
68
- });
69
- if (totalPages > 1) {
70
- if (normalizedPage > 0) {
71
- keyboard.text(t("projects.prev_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage - 1}`);
72
- }
73
- if (normalizedPage < totalPages - 1) {
74
- keyboard.text(t("projects.next_page"), `${WORKTREE_PAGE_CALLBACK_PREFIX}${normalizedPage + 1}`);
75
- }
76
- }
77
- return keyboard;
78
- }
79
- function buildWorktreeMenuView(worktrees, page) {
80
- const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(worktrees.length, page, config.bot.projectsListLimit);
81
- return {
82
- text: buildWorktreeMenuText(normalizedPage, totalPages),
83
- keyboard: buildWorktreeKeyboard(worktrees, normalizedPage),
84
- };
85
- }
86
- async function loadCurrentWorktreeContext() {
87
- const currentProject = getCurrentProject();
88
- if (!currentProject) {
89
- return { currentProject: null, context: null };
90
- }
91
- const context = await getGitWorktreeContext(currentProject.worktree);
92
- return { currentProject, context };
93
- }
94
- export async function worktreeCommand(ctx) {
95
- try {
96
- if (isForegroundBusy()) {
97
- await replyBusyBlocked(ctx);
98
- return;
99
- }
100
- const { currentProject, context } = await loadCurrentWorktreeContext();
101
- if (!currentProject) {
102
- await ctx.reply(t("worktree.project_not_selected"));
103
- return;
104
- }
105
- if (!context) {
106
- await ctx.reply(t("worktree.not_git_repo"));
107
- return;
108
- }
109
- if (context.worktrees.length === 0) {
110
- await ctx.reply(t("worktree.empty"));
111
- return;
112
- }
113
- const { text, keyboard } = buildWorktreeMenuView(context.worktrees, 0);
114
- await replyWithInlineMenu(ctx, {
115
- menuKind: "worktree",
116
- text,
117
- keyboard,
118
- });
119
- }
120
- catch (error) {
121
- logger.error("[Bot] Error loading worktrees:", error);
122
- await ctx.reply(t("worktree.fetch_error"));
123
- }
124
- }
125
- export async function handleWorktreeCallback(ctx, deps = {}) {
126
- const callbackQuery = ctx.callbackQuery;
127
- if (!callbackQuery?.data || !callbackQuery.data.startsWith(WORKTREE_CALLBACK_PREFIX)) {
128
- return false;
129
- }
130
- if (isForegroundBusy()) {
131
- await replyBusyBlocked(ctx);
132
- return true;
133
- }
134
- const page = parseWorktreePageCallback(callbackQuery.data);
135
- const index = parseWorktreeIndexCallback(callbackQuery.data);
136
- const isActiveMenu = await ensureActiveInlineMenu(ctx, "worktree");
137
- if (!isActiveMenu) {
138
- return true;
139
- }
140
- try {
141
- const { currentProject, context } = await loadCurrentWorktreeContext();
142
- if (!currentProject) {
143
- clearAllInteractionState("worktree_project_missing");
144
- await ctx.answerCallbackQuery();
145
- await ctx.reply(t("worktree.project_not_selected"));
146
- return true;
147
- }
148
- if (!context) {
149
- clearAllInteractionState("worktree_git_context_missing");
150
- await ctx.answerCallbackQuery({ text: t("worktree.not_git_repo_callback") });
151
- return true;
152
- }
153
- if (page !== null) {
154
- if (context.worktrees.length === 0) {
155
- await ctx.answerCallbackQuery({ text: t("worktree.page_empty_callback") });
156
- return true;
157
- }
158
- const { text, keyboard } = buildWorktreeMenuView(context.worktrees, page);
159
- await ctx.answerCallbackQuery();
160
- await ctx.editMessageText(text, {
161
- reply_markup: appendInlineMenuCancelButton(keyboard, "worktree"),
162
- });
163
- return true;
164
- }
165
- if (index === null) {
166
- await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
167
- return true;
168
- }
169
- const selectedWorktree = context.worktrees[index];
170
- if (!selectedWorktree) {
171
- await ctx.answerCallbackQuery({ text: t("worktree.selection_missing_callback") });
172
- return true;
173
- }
174
- if (selectedWorktree.isCurrent) {
175
- await ctx.answerCallbackQuery({ text: t("worktree.already_selected_callback") });
176
- return true;
177
- }
178
- logger.info(`[Bot] Worktree selected: ${selectedWorktree.path}`);
179
- await upsertSessionDirectory(selectedWorktree.path, Date.now());
180
- const projectInfo = await getProjectByWorktree(selectedWorktree.path);
181
- const selectedProjectInfo = { ...projectInfo, name: selectedWorktree.path };
182
- const replyKeyboard = deps.ensureEventSubscription
183
- ? await switchToProject(ctx, selectedProjectInfo, "worktree_switched", {
184
- ensureEventSubscription: deps.ensureEventSubscription,
185
- })
186
- : await switchToProject(ctx, selectedProjectInfo, "worktree_switched");
187
- await ctx.answerCallbackQuery();
188
- await ctx.reply(t("worktree.selected", { worktree: selectedWorktree.path }), {
189
- reply_markup: replyKeyboard,
190
- });
191
- await ctx.deleteMessage();
192
- return true;
193
- }
194
- catch (error) {
195
- logger.error("[Bot] Error handling worktree callback:", error);
196
- clearAllInteractionState("worktree_select_error");
197
- await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
198
- await ctx.reply(t("worktree.select_error"));
199
- return true;
200
- }
201
- }