@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,64 +1,19 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { opencodeClient } from "../../opencode/client.js";
3
- import { getCurrentProject } from "../../settings/manager.js";
4
- import { clearSession, getCurrentSession, setCurrentSession, } from "../../session/manager.js";
5
- import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
6
- import { interactionManager } from "../../interaction/manager.js";
7
- import { summaryAggregator } from "../../summary/aggregator.js";
8
- import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
9
- import { getStoredModel } from "../../model/manager.js";
1
+ import { config } from "../../config.js";
2
+ import { clearSession, getCurrentSession, setCurrentSession, } from "../../app/services/session-service.js";
3
+ import { ingestSessionInfoForCache } from "../../app/services/session-cache-service.js";
4
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
5
+ import { summaryAggregator } from "../../app/managers/summary-aggregation-manager.js";
6
+ import { getStoredAgent, resolveProjectAgent } from "../../app/services/agent-selection-service.js";
7
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
10
8
  import { safeBackgroundTask } from "../../utils/safe-background-task.js";
11
9
  import { logger } from "../../utils/logger.js";
12
10
  import { t } from "../../i18n/index.js";
13
- import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
14
- import { config } from "../../config.js";
15
- import { assistantRunState } from "../assistant-run-state.js";
16
- import { attachToSession, detachAttachedSession, markAttachedSessionBusy, markAttachedSessionIdle, } from "../../attach/service.js";
17
- import { externalUserInputSuppressionManager } from "../../external-input/suppression.js";
18
- const COMMANDS_CALLBACK_PREFIX = "commands:";
19
- const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
20
- const COMMANDS_CALLBACK_PAGE_PREFIX = `${COMMANDS_CALLBACK_PREFIX}page:`;
21
- const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
22
- const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
23
- const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
24
- function formatExecutingCommandMessage(commandName, args) {
25
- const prefix = t("commands.executing_prefix");
26
- const commandText = `/${commandName}`;
27
- const argsSuffix = args ? ` ${args}` : "";
28
- return {
29
- text: `${prefix}\n${commandText}${argsSuffix}`,
30
- entities: [
31
- {
32
- type: "code",
33
- offset: prefix.length + 1,
34
- length: commandText.length,
35
- },
36
- ],
37
- };
38
- }
39
- export function buildCommandPageCallback(page) {
40
- return `${COMMANDS_CALLBACK_PAGE_PREFIX}${page}`;
41
- }
42
- export function parseCommandPageCallback(data) {
43
- if (!data.startsWith(COMMANDS_CALLBACK_PAGE_PREFIX)) {
44
- return null;
45
- }
46
- const rawPage = data.slice(COMMANDS_CALLBACK_PAGE_PREFIX.length);
47
- const page = Number(rawPage);
48
- if (!Number.isInteger(page) || page < 0) {
49
- return null;
50
- }
51
- return page;
52
- }
53
- export function formatCommandsSelectText(page) {
54
- if (page === 0) {
55
- return t("commands.select");
56
- }
57
- return t("commands.select_page", { page: page + 1 });
58
- }
59
- function normalizeDirectoryForCommandApi(directory) {
60
- return directory.replace(/\\/g, "/");
61
- }
11
+ import { foregroundSessionState } from "../../app/managers/foreground-session-state-manager.js";
12
+ import { assistantRunState } from "../../app/managers/assistant-run-state-manager.js";
13
+ import { attachToSession, detachAttachedSession, markAttachedSessionBusy, markAttachedSessionIdle, } from "../../app/services/attach-service.js";
14
+ import { externalUserInputSuppressionManager } from "../../app/managers/external-input-suppression-manager.js";
15
+ import { opencodeClient } from "../../opencode/client.js";
16
+ import { buildCommandsConfirmKeyboard, buildCommandsListKeyboard, calculateCommandsPaginationRange, COMMANDS_CALLBACK_CANCEL, COMMANDS_CALLBACK_EXECUTE, COMMANDS_CALLBACK_PREFIX, formatCommandsSelectText, formatExecutingCommandMessage, parseCommandPageCallback, parseCommandSelectCallback, } from "../menus/command-catalog-menu.js";
62
17
  function getCallbackMessageId(ctx) {
63
18
  const message = ctx.callbackQuery?.message;
64
19
  if (!message || !("message_id" in message)) {
@@ -67,53 +22,6 @@ function getCallbackMessageId(ctx) {
67
22
  const messageId = message.message_id;
68
23
  return typeof messageId === "number" ? messageId : null;
69
24
  }
70
- function formatCommandButtonLabel(command) {
71
- const description = command.description?.trim() || t("commands.no_description");
72
- const rawLabel = `/${command.name} - ${description}`;
73
- if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
74
- return rawLabel;
75
- }
76
- return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
77
- }
78
- export function calculateCommandsPaginationRange(totalCommands, page, pageSize) {
79
- const safePageSize = Math.max(1, pageSize);
80
- const totalPages = Math.max(1, Math.ceil(totalCommands / safePageSize));
81
- const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
82
- const startIndex = normalizedPage * safePageSize;
83
- const endIndex = Math.min(startIndex + safePageSize, totalCommands);
84
- return {
85
- page: normalizedPage,
86
- totalPages,
87
- startIndex,
88
- endIndex,
89
- };
90
- }
91
- function buildCommandsListKeyboard(commands, page, pageSize) {
92
- const keyboard = new InlineKeyboard();
93
- const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateCommandsPaginationRange(commands.length, page, pageSize);
94
- commands.slice(startIndex, endIndex).forEach((command, index) => {
95
- const globalIndex = startIndex + index;
96
- keyboard
97
- .text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
98
- .row();
99
- });
100
- if (totalPages > 1) {
101
- if (normalizedPage > 0) {
102
- keyboard.text(t("commands.button.prev_page"), buildCommandPageCallback(normalizedPage - 1));
103
- }
104
- if (normalizedPage < totalPages - 1) {
105
- keyboard.text(t("commands.button.next_page"), buildCommandPageCallback(normalizedPage + 1));
106
- }
107
- keyboard.row();
108
- }
109
- keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
110
- return keyboard;
111
- }
112
- function buildCommandsConfirmKeyboard() {
113
- return new InlineKeyboard()
114
- .text(t("commands.button.execute"), COMMANDS_CALLBACK_EXECUTE)
115
- .text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
116
- }
117
25
  function parseCommandItems(value) {
118
26
  if (!Array.isArray(value)) {
119
27
  return null;
@@ -135,7 +43,7 @@ function parseCommandItems(value) {
135
43
  }
136
44
  return commands;
137
45
  }
138
- function parseCommandsMetadata(state) {
46
+ export function parseCommandsMetadata(state) {
139
47
  if (!state || state.kind !== "custom") {
140
48
  return null;
141
49
  }
@@ -180,40 +88,12 @@ function parseCommandsMetadata(state) {
180
88
  }
181
89
  return null;
182
90
  }
183
- function clearCommandsInteraction(reason) {
91
+ export function clearCommandsInteraction(reason) {
184
92
  const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
185
93
  if (metadata) {
186
94
  interactionManager.clear(reason);
187
95
  }
188
96
  }
189
- async function getCommandList(projectDirectory) {
190
- const { data, error } = await opencodeClient.command.list({
191
- directory: normalizeDirectoryForCommandApi(projectDirectory),
192
- });
193
- if (error || !data) {
194
- throw error || new Error("No command data received");
195
- }
196
- return data
197
- .filter((command) => {
198
- const source = command.source;
199
- return (typeof command.name === "string" && command.name.trim().length > 0 && source === "command");
200
- })
201
- .map((command) => ({
202
- name: command.name,
203
- description: command.description,
204
- }));
205
- }
206
- function parseSelectIndex(data) {
207
- if (!data.startsWith(COMMANDS_CALLBACK_SELECT_PREFIX)) {
208
- return null;
209
- }
210
- const rawIndex = data.slice(COMMANDS_CALLBACK_SELECT_PREFIX.length);
211
- const index = Number(rawIndex);
212
- if (!Number.isInteger(index) || index < 0) {
213
- return null;
214
- }
215
- return index;
216
- }
217
97
  async function isSessionBusy(sessionId, directory) {
218
98
  try {
219
99
  const { data, error } = await opencodeClient.session.status({ directory });
@@ -265,7 +145,7 @@ async function ensureSessionForProject(ctx, projectDirectory) {
265
145
  await ctx.reply(t("bot.session_created", { title: session.title }));
266
146
  return sessionInfo;
267
147
  }
268
- async function executeCommand(ctx, deps, params) {
148
+ export async function executeCommand(ctx, deps, params) {
269
149
  if (!ctx.chat) {
270
150
  return;
271
151
  }
@@ -342,41 +222,6 @@ async function executeCommand(ctx, deps, params) {
342
222
  },
343
223
  });
344
224
  }
345
- export async function commandsCommand(ctx) {
346
- try {
347
- const currentProject = getCurrentProject();
348
- if (!currentProject) {
349
- await ctx.reply(t("bot.project_not_selected"));
350
- return;
351
- }
352
- const commands = await getCommandList(currentProject.worktree);
353
- if (commands.length === 0) {
354
- await ctx.reply(t("commands.empty"));
355
- return;
356
- }
357
- const pageSize = config.bot.commandsListLimit;
358
- const keyboard = buildCommandsListKeyboard(commands, 0, pageSize);
359
- const message = await ctx.reply(formatCommandsSelectText(0), {
360
- reply_markup: keyboard,
361
- });
362
- interactionManager.start({
363
- kind: "custom",
364
- expectedInput: "callback",
365
- metadata: {
366
- flow: "commands",
367
- stage: "list",
368
- messageId: message.message_id,
369
- projectDirectory: currentProject.worktree,
370
- commands,
371
- page: 0,
372
- },
373
- });
374
- }
375
- catch (error) {
376
- logger.error("[Commands] Error fetching commands list:", error);
377
- await ctx.reply(t("commands.fetch_error"));
378
- }
379
- }
380
225
  export async function handleCommandsCallback(ctx, deps) {
381
226
  const data = ctx.callbackQuery?.data;
382
227
  if (!data || !data.startsWith(COMMANDS_CALLBACK_PREFIX)) {
@@ -440,7 +285,7 @@ export async function handleCommandsCallback(ctx, deps) {
440
285
  });
441
286
  return true;
442
287
  }
443
- const commandIndex = parseSelectIndex(data);
288
+ const commandIndex = parseCommandSelectCallback(data);
444
289
  if (commandIndex === null || metadata.stage !== "list") {
445
290
  await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
446
291
  return true;
@@ -473,28 +318,3 @@ export async function handleCommandsCallback(ctx, deps) {
473
318
  return true;
474
319
  }
475
320
  }
476
- export async function handleCommandTextArguments(ctx, deps) {
477
- const text = ctx.message?.text;
478
- if (!text || text.startsWith("/")) {
479
- return false;
480
- }
481
- const metadata = parseCommandsMetadata(interactionManager.getSnapshot());
482
- if (!metadata || metadata.stage !== "confirm") {
483
- return false;
484
- }
485
- const argumentsText = text.trim();
486
- if (!argumentsText) {
487
- await ctx.reply(t("commands.arguments_empty"));
488
- return true;
489
- }
490
- clearCommandsInteraction("commands_arguments_submitted");
491
- if (ctx.chat) {
492
- await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
493
- }
494
- await executeCommand(ctx, deps, {
495
- projectDirectory: metadata.projectDirectory,
496
- commandName: metadata.commandName,
497
- argumentsText,
498
- });
499
- return true;
500
- }
@@ -1,38 +1,9 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { getCurrentSession } from "../../session/manager.js";
1
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
3
2
  import { opencodeClient } from "../../opencode/client.js";
4
- import { getStoredModel } from "../../model/manager.js";
5
- import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
3
+ import { getCurrentSession } from "../../app/services/session-service.js";
6
4
  import { logger } from "../../utils/logger.js";
7
5
  import { t } from "../../i18n/index.js";
8
- /**
9
- * Build inline keyboard with compact confirmation menu
10
- * @returns InlineKeyboard with confirmation button
11
- */
12
- export function buildCompactConfirmationMenu() {
13
- const keyboard = new InlineKeyboard();
14
- keyboard.text(t("context.button.confirm"), "compact:confirm");
15
- return keyboard;
16
- }
17
- /**
18
- * Handle context button press (text message from Reply Keyboard)
19
- * Shows inline menu with compact confirmation
20
- * @param ctx grammY context
21
- */
22
- export async function handleContextButtonPress(ctx) {
23
- logger.debug("[ContextHandler] Context button pressed");
24
- const session = getCurrentSession();
25
- if (!session) {
26
- await ctx.reply(t("context.no_active_session"));
27
- return;
28
- }
29
- const keyboard = buildCompactConfirmationMenu();
30
- await replyWithInlineMenu(ctx, {
31
- menuKind: "context",
32
- text: t("context.confirm_text", { title: session.title }),
33
- keyboard,
34
- });
35
- }
6
+ import { clearActiveInlineMenu, ensureActiveInlineMenu } from "../menus/inline-menu.js";
36
7
  /**
37
8
  * Handle compact confirmation callback
38
9
  * Calls OpenCode API to compact the session
@@ -0,0 +1,219 @@
1
+ import { getProjectRoot, isPathWithinDirectory, isWithinAllowedRoot, isWithinProjectRoot, pathToDisplayPath, } from "../../app/services/file-browser-service.js";
2
+ import { isForegroundBusy } from "../../app/services/run-control-service.js";
3
+ import { t } from "../../i18n/index.js";
4
+ import { getProjectByWorktree } from "../../app/services/project-service.js";
5
+ import { upsertSessionDirectory } from "../../app/services/session-cache-service.js";
6
+ import { logger } from "../../utils/logger.js";
7
+ import { replyBusyBlocked } from "../messages/busy-blocked-renderer.js";
8
+ import { ensureActiveInlineMenu, clearActiveInlineMenu } from "../menus/inline-menu.js";
9
+ import { sendDownloadedFile } from "../messages/send-downloaded-file.js";
10
+ import { switchToProject } from "../../app/services/project-switch-service.js";
11
+ import { createProjectSwitchPresentation } from "../services/project-switch-presentation.js";
12
+ import { buildOpenRootsKeyboard, clearLsPathIndex, clearOpenPathIndex, decodeLsBackCallback, decodeLsFileCallback, decodeLsPaginationCallback, decodeLsPathFromCallback, decodeOpenPaginationCallback, decodeOpenPathFromCallback, LS_CALLBACK_DOWNLOAD_PREFIX, LS_CALLBACK_NAV_PREFIX, LS_CALLBACK_PREFIX, OPEN_CALLBACK_NAV_PREFIX, OPEN_CALLBACK_PREFIX, OPEN_CALLBACK_ROOTS, OPEN_CALLBACK_SELECT_PREFIX, renderLsBrowseView, renderLsFileDetailsView, renderOpenBrowseView, } from "../menus/file-browser-menu.js";
13
+ const sessionDirectories = new Map();
14
+ export function clearSessionDirectories() {
15
+ sessionDirectories.clear();
16
+ }
17
+ export function resolveInitialLsDirectory(userId) {
18
+ const currentProject = getProjectRoot();
19
+ if (!currentProject) {
20
+ return null;
21
+ }
22
+ if (userId) {
23
+ const cachedDirectory = sessionDirectories.get(userId);
24
+ if (cachedDirectory && isPathWithinDirectory(cachedDirectory, currentProject)) {
25
+ return cachedDirectory;
26
+ }
27
+ }
28
+ return currentProject;
29
+ }
30
+ export function rememberLsDirectory(userId, directory) {
31
+ if (userId) {
32
+ sessionDirectories.set(userId, directory);
33
+ }
34
+ }
35
+ export async function handleOpenCallback(ctx, deps = {}) {
36
+ const data = ctx.callbackQuery?.data;
37
+ if (!data || !data.startsWith(OPEN_CALLBACK_PREFIX)) {
38
+ return false;
39
+ }
40
+ if (isForegroundBusy()) {
41
+ await replyBusyBlocked(ctx);
42
+ return true;
43
+ }
44
+ const isActiveMenu = await ensureActiveInlineMenu(ctx, "open");
45
+ if (!isActiveMenu) {
46
+ return true;
47
+ }
48
+ try {
49
+ if (data === OPEN_CALLBACK_ROOTS) {
50
+ await showOpenRoots(ctx);
51
+ return true;
52
+ }
53
+ const navPath = decodeOpenPathFromCallback(OPEN_CALLBACK_NAV_PREFIX, data);
54
+ if (navPath !== null) {
55
+ if (!isWithinAllowedRoot(navPath)) {
56
+ await ctx.answerCallbackQuery({ text: t("open.access_denied") });
57
+ return true;
58
+ }
59
+ await navigateOpenTo(ctx, navPath);
60
+ return true;
61
+ }
62
+ const pageInfo = decodeOpenPaginationCallback(data);
63
+ if (pageInfo !== null) {
64
+ if (!isWithinAllowedRoot(pageInfo.path)) {
65
+ await ctx.answerCallbackQuery({ text: t("open.access_denied") });
66
+ return true;
67
+ }
68
+ await navigateOpenTo(ctx, pageInfo.path, pageInfo.page);
69
+ return true;
70
+ }
71
+ const selectPath = decodeOpenPathFromCallback(OPEN_CALLBACK_SELECT_PREFIX, data);
72
+ if (selectPath !== null) {
73
+ if (!isWithinAllowedRoot(selectPath)) {
74
+ await ctx.answerCallbackQuery({ text: t("open.access_denied") });
75
+ return true;
76
+ }
77
+ await selectDirectory(ctx, selectPath, deps);
78
+ return true;
79
+ }
80
+ return false;
81
+ }
82
+ catch (error) {
83
+ logger.error("[Bot] Error handling open callback:", error);
84
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
85
+ return true;
86
+ }
87
+ }
88
+ async function showOpenRoots(ctx) {
89
+ await ctx.answerCallbackQuery();
90
+ await ctx.editMessageText(t("open.select_root"), { reply_markup: buildOpenRootsKeyboard() });
91
+ }
92
+ async function navigateOpenTo(ctx, dirPath, page = 0) {
93
+ const view = await renderOpenBrowseView(dirPath, page);
94
+ if ("error" in view) {
95
+ await ctx.answerCallbackQuery({ text: view.error });
96
+ return;
97
+ }
98
+ await ctx.answerCallbackQuery();
99
+ await ctx.editMessageText(view.text, { reply_markup: view.keyboard });
100
+ }
101
+ async function selectDirectory(ctx, directory, deps = {}) {
102
+ const displayPath = pathToDisplayPath(directory);
103
+ try {
104
+ logger.info(`[Bot] Adding project directory: ${directory}`);
105
+ await upsertSessionDirectory(directory, Date.now());
106
+ const projectInfo = await getProjectByWorktree(directory);
107
+ const selectedProjectInfo = { ...projectInfo, name: displayPath };
108
+ const replyKeyboard = await switchToProject(ctx, selectedProjectInfo, "open_project_selected", {
109
+ ensureEventSubscription: deps.ensureEventSubscription,
110
+ presentation: createProjectSwitchPresentation(),
111
+ });
112
+ await ctx.answerCallbackQuery();
113
+ await ctx.reply(t("open.selected", { project: displayPath }), { reply_markup: replyKeyboard });
114
+ await ctx.deleteMessage();
115
+ clearOpenPathIndex();
116
+ logger.info(`[Bot] Project added and selected: ${displayPath}`);
117
+ }
118
+ catch (error) {
119
+ logger.error("[Bot] Error selecting directory:", error);
120
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
121
+ await ctx.reply(t("open.select_error"));
122
+ }
123
+ }
124
+ export async function handleLsCallback(ctx) {
125
+ const data = ctx.callbackQuery?.data;
126
+ if (!data || !data.startsWith(LS_CALLBACK_PREFIX)) {
127
+ return false;
128
+ }
129
+ if (isForegroundBusy()) {
130
+ await replyBusyBlocked(ctx);
131
+ return true;
132
+ }
133
+ const isActiveMenu = await ensureActiveInlineMenu(ctx, "ls");
134
+ if (!isActiveMenu) {
135
+ return true;
136
+ }
137
+ try {
138
+ const navPath = decodeLsPathFromCallback(LS_CALLBACK_NAV_PREFIX, data);
139
+ if (navPath !== null) {
140
+ if (!isWithinProjectRoot(navPath)) {
141
+ await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
142
+ return true;
143
+ }
144
+ await navigateLsTo(ctx, navPath);
145
+ return true;
146
+ }
147
+ const pageInfo = decodeLsPaginationCallback(data);
148
+ if (pageInfo !== null) {
149
+ if (!isWithinProjectRoot(pageInfo.path)) {
150
+ await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
151
+ return true;
152
+ }
153
+ await navigateLsTo(ctx, pageInfo.path, pageInfo.page);
154
+ return true;
155
+ }
156
+ const fileInfo = decodeLsFileCallback(data);
157
+ if (fileInfo !== null) {
158
+ if (!isWithinProjectRoot(fileInfo.path)) {
159
+ await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
160
+ return true;
161
+ }
162
+ await showLsFileDetails(ctx, fileInfo.path, fileInfo.page);
163
+ return true;
164
+ }
165
+ const downloadPath = decodeLsPathFromCallback(LS_CALLBACK_DOWNLOAD_PREFIX, data);
166
+ if (downloadPath !== null) {
167
+ if (!isWithinProjectRoot(downloadPath)) {
168
+ await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
169
+ return true;
170
+ }
171
+ await downloadFileAndClose(ctx, downloadPath);
172
+ return true;
173
+ }
174
+ const backInfo = decodeLsBackCallback(data);
175
+ if (backInfo !== null) {
176
+ if (!isWithinProjectRoot(backInfo.path)) {
177
+ await ctx.answerCallbackQuery({ text: t("ls.access_denied") });
178
+ return true;
179
+ }
180
+ await navigateLsTo(ctx, backInfo.path, backInfo.page);
181
+ return true;
182
+ }
183
+ return false;
184
+ }
185
+ catch (error) {
186
+ logger.error("[Ls] Error handling callback:", error);
187
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
188
+ return true;
189
+ }
190
+ }
191
+ async function navigateLsTo(ctx, dirPath, page = 0) {
192
+ const view = await renderLsBrowseView(dirPath, page);
193
+ if ("error" in view) {
194
+ await ctx.answerCallbackQuery({ text: view.error });
195
+ return;
196
+ }
197
+ rememberLsDirectory(ctx.from?.id, dirPath);
198
+ await ctx.answerCallbackQuery();
199
+ await ctx.editMessageText(view.text, { parse_mode: "HTML", reply_markup: view.keyboard });
200
+ }
201
+ async function showLsFileDetails(ctx, filePath, page) {
202
+ const view = await renderLsFileDetailsView(filePath, page);
203
+ if ("error" in view) {
204
+ await ctx.answerCallbackQuery({ text: view.error });
205
+ return;
206
+ }
207
+ await ctx.answerCallbackQuery();
208
+ await ctx.editMessageText(view.text, { parse_mode: "HTML", reply_markup: view.keyboard });
209
+ }
210
+ async function downloadFileAndClose(ctx, filePath) {
211
+ await ctx.answerCallbackQuery({ text: t("commands.download.downloading") });
212
+ const downloaded = await sendDownloadedFile(ctx, filePath, { announce: false });
213
+ if (!downloaded) {
214
+ return;
215
+ }
216
+ clearActiveInlineMenu("ls_downloaded");
217
+ clearLsPathIndex();
218
+ await ctx.deleteMessage().catch(() => { });
219
+ }
@@ -0,0 +1,32 @@
1
+ import { clearActiveInlineMenu, ensureActiveInlineMenu, INLINE_MENU_CANCEL_PREFIX, isInlineMenuKind, LEGACY_CONTEXT_CANCEL_CALLBACK, } from "../menus/inline-menu.js";
2
+ import { t } from "../../i18n/index.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ export async function handleInlineMenuCancel(ctx) {
5
+ const data = ctx.callbackQuery?.data;
6
+ if (!data) {
7
+ return false;
8
+ }
9
+ let menuKind = null;
10
+ if (data === LEGACY_CONTEXT_CANCEL_CALLBACK) {
11
+ menuKind = "context";
12
+ }
13
+ else if (data.startsWith(INLINE_MENU_CANCEL_PREFIX)) {
14
+ const rawKind = data.slice(INLINE_MENU_CANCEL_PREFIX.length);
15
+ if (!isInlineMenuKind(rawKind)) {
16
+ return false;
17
+ }
18
+ menuKind = rawKind;
19
+ }
20
+ else {
21
+ return false;
22
+ }
23
+ const isActive = await ensureActiveInlineMenu(ctx, menuKind);
24
+ if (!isActive) {
25
+ return true;
26
+ }
27
+ clearActiveInlineMenu(`inline_menu_cancel:${menuKind}`);
28
+ await ctx.answerCallbackQuery({ text: t("inline.cancelled_callback") }).catch(() => { });
29
+ await ctx.deleteMessage().catch(() => { });
30
+ logger.debug(`[InlineMenu] Menu cancelled: kind=${menuKind}`);
31
+ return true;
32
+ }