@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,75 +1,24 @@
1
- import { InlineKeyboard } from "grammy";
2
1
  import { opencodeClient } from "../../opencode/client.js";
3
- import { resolveProjectAgent } from "../../agent/manager.js";
4
- import { setCurrentSession } from "../../session/manager.js";
5
- import { getCurrentProject } from "../../settings/manager.js";
6
- import { clearAllInteractionState } from "../../interaction/cleanup.js";
7
- import { interactionManager } from "../../interaction/manager.js";
8
- import { keyboardManager } from "../../keyboard/manager.js";
9
- import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
10
- import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
2
+ import { resolveProjectAgent } from "../../app/services/agent-selection-service.js";
3
+ import { setCurrentSession } from "../../app/services/session-service.js";
4
+ import { getCurrentProject } from "../../app/stores/settings-store.js";
5
+ import { clearAllInteractionState, interactionManager } from "../../app/managers/interaction-manager.js";
6
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
7
+ import { appendInlineMenuCancelButton, ensureActiveInlineMenu } from "../menus/inline-menu.js";
8
+ import { isForegroundBusy } from "../../app/services/run-control-service.js";
9
+ import { replyBusyBlocked } from "../messages/busy-blocked-renderer.js";
11
10
  import { logger } from "../../utils/logger.js";
12
11
  import { safeBackgroundTask } from "../../utils/safe-background-task.js";
13
12
  import { config } from "../../config.js";
14
- import { getDateLocale, t } from "../../i18n/index.js";
15
- import { attachToSession } from "../../attach/service.js";
16
- import { renderAssistantFinalPartsSafe } from "../utils/assistant-rendering.js";
17
- import { sendRenderedBotPart } from "../utils/telegram-text.js";
18
- const SESSION_CALLBACK_PREFIX = "session:";
19
- const SESSION_PAGE_CALLBACK_PREFIX = "session:page:";
20
- const BACKGROUND_SESSION_CALLBACK_PREFIX = "background-session:";
21
- const SESSION_FETCH_EXTRA_COUNT = 1;
22
- const BACKGROUND_SESSION_KIND_CALLBACK_MARKERS = {
23
- assistant_response: "a",
24
- question_asked: "q",
25
- permission_asked: "p",
26
- };
27
- const BACKGROUND_SESSION_KIND_BY_CALLBACK_MARKER = {
28
- a: "assistant_response",
29
- q: "question_asked",
30
- p: "permission_asked",
31
- };
32
- function buildSessionPageCallback(page) {
33
- return `${SESSION_PAGE_CALLBACK_PREFIX}${page}`;
34
- }
35
- function parseSessionPageCallback(data) {
36
- if (!data.startsWith(SESSION_PAGE_CALLBACK_PREFIX)) {
37
- return null;
38
- }
39
- const rawPage = data.slice(SESSION_PAGE_CALLBACK_PREFIX.length);
40
- const page = Number(rawPage);
41
- if (!Number.isInteger(page) || page < 0) {
42
- return null;
43
- }
44
- return page;
45
- }
46
- function parseSessionIdCallback(data) {
47
- if (!data.startsWith(SESSION_CALLBACK_PREFIX)) {
48
- return null;
49
- }
50
- if (data.startsWith(SESSION_PAGE_CALLBACK_PREFIX)) {
51
- return null;
52
- }
53
- const sessionId = data.slice(SESSION_CALLBACK_PREFIX.length);
54
- return sessionId.length > 0 ? sessionId : null;
55
- }
56
- function parseBackgroundSessionCallback(data) {
57
- if (!data.startsWith(BACKGROUND_SESSION_CALLBACK_PREFIX)) {
58
- return null;
59
- }
60
- const payload = data.slice(BACKGROUND_SESSION_CALLBACK_PREFIX.length);
61
- const markerSeparatorIndex = payload.indexOf(":");
62
- if (markerSeparatorIndex < 0) {
63
- return payload.length > 0 ? { sessionId: payload, kind: null } : null;
64
- }
65
- const marker = payload.slice(0, markerSeparatorIndex);
66
- const sessionId = payload.slice(markerSeparatorIndex + 1);
67
- const kind = BACKGROUND_SESSION_KIND_BY_CALLBACK_MARKER[marker];
68
- if (!kind || sessionId.length === 0) {
69
- return null;
70
- }
71
- return { sessionId, kind };
72
- }
13
+ import { t } from "../../i18n/index.js";
14
+ import { attachToSession } from "../../app/services/attach-service.js";
15
+ import { renderAssistantFinalPartsSafe } from "../messages/assistant-rendering.js";
16
+ import { sendRenderedBotPart } from "../messages/telegram-text.js";
17
+ import { buildSessionSelectionMenuView, parseBackgroundSessionCallback, parseSessionIdCallback, parseSessionPageCallback, SESSION_CALLBACK_PREFIX, loadSessionPage, } from "../menus/session-selection-menu.js";
18
+ const PREVIEW_MESSAGES_LIMIT = 6;
19
+ const LATEST_ASSISTANT_RESPONSE_MESSAGES_LIMIT = 20;
20
+ const PREVIEW_ITEM_MAX_LENGTH = 420;
21
+ const TELEGRAM_MESSAGE_LIMIT = 4096;
73
22
  async function removeCallbackReplyMarkup(ctx) {
74
23
  try {
75
24
  await ctx.editMessageReplyMarkup();
@@ -78,90 +27,6 @@ async function removeCallbackReplyMarkup(ctx) {
78
27
  logger.debug("[Sessions] Failed to remove background session button:", err);
79
28
  }
80
29
  }
81
- export function buildBackgroundSessionOpenKeyboard(sessionId, kind) {
82
- const marker = BACKGROUND_SESSION_KIND_CALLBACK_MARKERS[kind];
83
- return new InlineKeyboard().text(t("background.open_session_button"), `${BACKGROUND_SESSION_CALLBACK_PREFIX}${marker}:${sessionId}`);
84
- }
85
- function formatSessionsSelectText(page) {
86
- if (page === 0) {
87
- return t("sessions.select");
88
- }
89
- return t("sessions.select_page", { page: page + 1 });
90
- }
91
- async function loadSessionPage(directory, page, pageSize) {
92
- const startIndex = page * pageSize;
93
- const endExclusive = startIndex + pageSize;
94
- const { data: sessions, error } = await opencodeClient.session.list({
95
- directory,
96
- limit: endExclusive + SESSION_FETCH_EXTRA_COUNT,
97
- roots: true,
98
- });
99
- if (error || !sessions) {
100
- throw error || new Error("No data received from server");
101
- }
102
- const hasNext = sessions.length > endExclusive;
103
- const pagedSessions = sessions.slice(startIndex, endExclusive);
104
- logger.debug(`[Sessions] Loaded page=${page + 1}, startIndex=${startIndex}, endExclusive=${endExclusive}, pageSize=${pageSize}, items=${pagedSessions.length}, hasNext=${hasNext}`);
105
- return {
106
- sessions: pagedSessions,
107
- hasNext,
108
- page,
109
- };
110
- }
111
- function buildSessionsKeyboard(pageData, pageSize) {
112
- const keyboard = new InlineKeyboard();
113
- const localeForDate = getDateLocale();
114
- const pageStartIndex = pageData.page * pageSize;
115
- pageData.sessions.forEach((session, index) => {
116
- const date = new Date(session.time.created).toLocaleDateString(localeForDate);
117
- const label = `${pageStartIndex + index + 1}. ${session.title} (${date})`;
118
- keyboard.text(label, `${SESSION_CALLBACK_PREFIX}${session.id}`).row();
119
- });
120
- if (pageData.page > 0) {
121
- keyboard.text(t("sessions.button.prev_page"), buildSessionPageCallback(pageData.page - 1));
122
- }
123
- if (pageData.hasNext) {
124
- keyboard.text(t("sessions.button.next_page"), buildSessionPageCallback(pageData.page + 1));
125
- }
126
- if (pageData.page > 0 || pageData.hasNext) {
127
- keyboard.row();
128
- }
129
- return keyboard;
130
- }
131
- export async function sessionsCommand(ctx) {
132
- try {
133
- if (isForegroundBusy()) {
134
- await replyBusyBlocked(ctx);
135
- return;
136
- }
137
- const pageSize = config.bot.sessionsListLimit;
138
- const currentProject = getCurrentProject();
139
- if (!currentProject) {
140
- await ctx.reply(t("sessions.project_not_selected"));
141
- return;
142
- }
143
- logger.debug(`[Sessions] Fetching sessions for directory: ${currentProject.worktree}`);
144
- const firstPage = await loadSessionPage(currentProject.worktree, 0, pageSize);
145
- logger.debug(`[Sessions] Found ${firstPage.sessions.length} sessions on page 1`);
146
- firstPage.sessions.forEach((session) => {
147
- logger.debug(`[Sessions] Session: ${session.title} | ${session.directory}`);
148
- });
149
- if (firstPage.sessions.length === 0) {
150
- await ctx.reply(t("sessions.empty"));
151
- return;
152
- }
153
- const keyboard = buildSessionsKeyboard(firstPage, pageSize);
154
- await replyWithInlineMenu(ctx, {
155
- menuKind: "session",
156
- text: formatSessionsSelectText(firstPage.page),
157
- keyboard,
158
- });
159
- }
160
- catch (error) {
161
- logger.error("[Sessions] Error fetching sessions:", error);
162
- await ctx.reply(t("sessions.fetch_error"));
163
- }
164
- }
165
30
  async function selectSessionById(ctx, deps, sessionId, options) {
166
31
  const currentProject = getCurrentProject();
167
32
  if (!currentProject) {
@@ -327,9 +192,9 @@ export async function handleSessionSelect(ctx, deps) {
327
192
  await ctx.answerCallbackQuery({ text: t("sessions.page_empty_callback") });
328
193
  return true;
329
194
  }
330
- const keyboard = buildSessionsKeyboard(pageData, pageSize);
195
+ const { text, keyboard } = buildSessionSelectionMenuView(pageData, pageSize);
331
196
  appendInlineMenuCancelButton(keyboard, "session");
332
- await ctx.editMessageText(formatSessionsSelectText(pageData.page), {
197
+ await ctx.editMessageText(text, {
333
198
  reply_markup: keyboard,
334
199
  });
335
200
  await ctx.answerCallbackQuery();
@@ -359,10 +224,6 @@ export async function handleSessionSelect(ctx, deps) {
359
224
  }
360
225
  return true;
361
226
  }
362
- const PREVIEW_MESSAGES_LIMIT = 6;
363
- const LATEST_ASSISTANT_RESPONSE_MESSAGES_LIMIT = 20;
364
- const PREVIEW_ITEM_MAX_LENGTH = 420;
365
- const TELEGRAM_MESSAGE_LIMIT = 4096;
366
227
  function extractTextParts(parts, options = {}) {
367
228
  const textParts = parts
368
229
  .filter((part) => part.type === "text" && typeof part.text === "string")
@@ -0,0 +1,105 @@
1
+ import { isTtsConfigured } from "../../app/services/tts-service.js";
2
+ import { getCompactOutputMode, getResponseStreamingMode, getSendDiffFileAttachments, getShowAssistantRunFooter, getShowThinkingContent, getTtsMode, setCompactOutputMode, setResponseStreamingMode, setSendDiffFileAttachments, setShowAssistantRunFooter, setShowThinkingContent, setTtsMode, } from "../../app/stores/settings-store.js";
3
+ import { t } from "../../i18n/index.js";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { appendInlineMenuCancelButton, ensureActiveInlineMenu } from "../menus/inline-menu.js";
6
+ import { buildSettingsMenuView, SETTINGS_ASSISTANT_FOOTER_CALLBACK, SETTINGS_CALLBACK_PREFIX, SETTINGS_COMPACT_OUTPUT_CALLBACK, SETTINGS_DIFF_FILES_CALLBACK, SETTINGS_RESPONSE_STREAMING_CALLBACK, SETTINGS_THINKING_CONTENT_CALLBACK, SETTINGS_TTS_CALLBACK, } from "../menus/settings-menu.js";
7
+ function getTtsSavedMessageKey(mode) {
8
+ if (mode === "all") {
9
+ return "tts.all";
10
+ }
11
+ if (mode === "auto") {
12
+ return "tts.auto";
13
+ }
14
+ return "tts.off";
15
+ }
16
+ function getNextTtsMode(mode) {
17
+ if (mode === "off") {
18
+ return "all";
19
+ }
20
+ if (mode === "all") {
21
+ return "auto";
22
+ }
23
+ return "off";
24
+ }
25
+ function getNextResponseStreamingMode(mode) {
26
+ return mode === "edit" ? "draft" : "edit";
27
+ }
28
+ export async function handleSettingsCallback(ctx) {
29
+ const callbackData = ctx.callbackQuery?.data;
30
+ if (!callbackData?.startsWith(SETTINGS_CALLBACK_PREFIX)) {
31
+ return false;
32
+ }
33
+ const isActiveMenu = await ensureActiveInlineMenu(ctx, "settings");
34
+ if (!isActiveMenu) {
35
+ return true;
36
+ }
37
+ try {
38
+ if (callbackData === SETTINGS_COMPACT_OUTPUT_CALLBACK) {
39
+ setCompactOutputMode(!getCompactOutputMode());
40
+ const { text, keyboard } = buildSettingsMenuView();
41
+ await ctx.answerCallbackQuery({ text: t("settings.saved") });
42
+ await ctx.editMessageText(text, {
43
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
44
+ });
45
+ return true;
46
+ }
47
+ if (callbackData === SETTINGS_THINKING_CONTENT_CALLBACK) {
48
+ setShowThinkingContent(!getShowThinkingContent());
49
+ const { text, keyboard } = buildSettingsMenuView();
50
+ await ctx.answerCallbackQuery({ text: t("settings.saved") });
51
+ await ctx.editMessageText(text, {
52
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
53
+ });
54
+ return true;
55
+ }
56
+ if (callbackData === SETTINGS_RESPONSE_STREAMING_CALLBACK) {
57
+ setResponseStreamingMode(getNextResponseStreamingMode(getResponseStreamingMode()));
58
+ const { text, keyboard } = buildSettingsMenuView();
59
+ await ctx.answerCallbackQuery({ text: t("settings.saved") });
60
+ await ctx.editMessageText(text, {
61
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
62
+ });
63
+ return true;
64
+ }
65
+ if (callbackData === SETTINGS_DIFF_FILES_CALLBACK) {
66
+ setSendDiffFileAttachments(!getSendDiffFileAttachments());
67
+ const { text, keyboard } = buildSettingsMenuView();
68
+ await ctx.answerCallbackQuery({ text: t("settings.saved") });
69
+ await ctx.editMessageText(text, {
70
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
71
+ });
72
+ return true;
73
+ }
74
+ if (callbackData === SETTINGS_ASSISTANT_FOOTER_CALLBACK) {
75
+ setShowAssistantRunFooter(!getShowAssistantRunFooter());
76
+ const { text, keyboard } = buildSettingsMenuView();
77
+ await ctx.answerCallbackQuery({ text: t("settings.saved") });
78
+ await ctx.editMessageText(text, {
79
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
80
+ });
81
+ return true;
82
+ }
83
+ if (callbackData === SETTINGS_TTS_CALLBACK) {
84
+ const nextMode = getNextTtsMode(getTtsMode());
85
+ if (nextMode !== "off" && !isTtsConfigured()) {
86
+ await ctx.answerCallbackQuery({ text: t("tts.not_configured"), show_alert: true });
87
+ return true;
88
+ }
89
+ setTtsMode(nextMode);
90
+ const { text, keyboard } = buildSettingsMenuView();
91
+ await ctx.answerCallbackQuery({ text: t(getTtsSavedMessageKey(nextMode)) });
92
+ await ctx.editMessageText(text, {
93
+ reply_markup: appendInlineMenuCancelButton(keyboard, "settings"),
94
+ });
95
+ return true;
96
+ }
97
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
98
+ return true;
99
+ }
100
+ catch (error) {
101
+ logger.error("[Settings] Error handling settings callback:", error);
102
+ await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
103
+ return true;
104
+ }
105
+ }
@@ -1,55 +1,10 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { opencodeClient } from "../../opencode/client.js";
3
- import { getCurrentProject } from "../../settings/manager.js";
4
- import { interactionManager } from "../../interaction/manager.js";
1
+ import { config } from "../../config.js";
2
+ import { getCurrentProject } from "../../app/stores/settings-store.js";
3
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
5
4
  import { logger } from "../../utils/logger.js";
6
5
  import { t } from "../../i18n/index.js";
7
- import { config } from "../../config.js";
8
6
  import { processUserPrompt } from "../handlers/prompt.js";
9
- const SKILLS_CALLBACK_PREFIX = "skills:";
10
- const SKILLS_CALLBACK_SELECT_PREFIX = `${SKILLS_CALLBACK_PREFIX}select:`;
11
- const SKILLS_CALLBACK_PAGE_PREFIX = `${SKILLS_CALLBACK_PREFIX}page:`;
12
- const SKILLS_CALLBACK_CANCEL = `${SKILLS_CALLBACK_PREFIX}cancel`;
13
- const SKILLS_CALLBACK_EXECUTE = `${SKILLS_CALLBACK_PREFIX}execute`;
14
- const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
15
- function formatExecutingSkillMessage(skillName, args) {
16
- const prefix = t("skills.executing_prefix");
17
- const skillText = `/${skillName}`;
18
- const argsSuffix = args ? ` ${args}` : "";
19
- return {
20
- text: `${prefix}\n${skillText}${argsSuffix}`,
21
- entities: [
22
- {
23
- type: "code",
24
- offset: prefix.length + 1,
25
- length: skillText.length,
26
- },
27
- ],
28
- };
29
- }
30
- export function buildSkillPageCallback(page) {
31
- return `${SKILLS_CALLBACK_PAGE_PREFIX}${page}`;
32
- }
33
- export function parseSkillPageCallback(data) {
34
- if (!data.startsWith(SKILLS_CALLBACK_PAGE_PREFIX)) {
35
- return null;
36
- }
37
- const rawPage = data.slice(SKILLS_CALLBACK_PAGE_PREFIX.length);
38
- const page = Number(rawPage);
39
- if (!Number.isInteger(page) || page < 0) {
40
- return null;
41
- }
42
- return page;
43
- }
44
- export function formatSkillsSelectText(page) {
45
- if (page === 0) {
46
- return t("skills.select");
47
- }
48
- return t("skills.select_page", { page: page + 1 });
49
- }
50
- function normalizeDirectoryForCommandApi(directory) {
51
- return directory.replace(/\\/g, "/");
52
- }
7
+ import { buildSkillsConfirmKeyboard, buildSkillsListKeyboard, calculateSkillsPaginationRange, formatExecutingSkillMessage, formatSkillsSelectText, parseSkillPageCallback, parseSkillSelectCallback, SKILLS_CALLBACK_CANCEL, SKILLS_CALLBACK_EXECUTE, SKILLS_CALLBACK_PREFIX, } from "../menus/skills-catalog-menu.js";
53
8
  function getCallbackMessageId(ctx) {
54
9
  const message = ctx.callbackQuery?.message;
55
10
  if (!message || !("message_id" in message)) {
@@ -58,53 +13,6 @@ function getCallbackMessageId(ctx) {
58
13
  const messageId = message.message_id;
59
14
  return typeof messageId === "number" ? messageId : null;
60
15
  }
61
- function formatSkillButtonLabel(skill) {
62
- const description = skill.description?.trim() || t("skills.no_description");
63
- const rawLabel = `/${skill.name} - ${description}`;
64
- if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
65
- return rawLabel;
66
- }
67
- return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
68
- }
69
- export function calculateSkillsPaginationRange(totalSkills, page, pageSize) {
70
- const safePageSize = Math.max(1, pageSize);
71
- const totalPages = Math.max(1, Math.ceil(totalSkills / safePageSize));
72
- const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
73
- const startIndex = normalizedPage * safePageSize;
74
- const endIndex = Math.min(startIndex + safePageSize, totalSkills);
75
- return {
76
- page: normalizedPage,
77
- totalPages,
78
- startIndex,
79
- endIndex,
80
- };
81
- }
82
- function buildSkillsListKeyboard(skills, page, pageSize) {
83
- const keyboard = new InlineKeyboard();
84
- const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateSkillsPaginationRange(skills.length, page, pageSize);
85
- skills.slice(startIndex, endIndex).forEach((skill, index) => {
86
- const globalIndex = startIndex + index;
87
- keyboard
88
- .text(formatSkillButtonLabel(skill), `${SKILLS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
89
- .row();
90
- });
91
- if (totalPages > 1) {
92
- if (normalizedPage > 0) {
93
- keyboard.text(t("skills.button.prev_page"), buildSkillPageCallback(normalizedPage - 1));
94
- }
95
- if (normalizedPage < totalPages - 1) {
96
- keyboard.text(t("skills.button.next_page"), buildSkillPageCallback(normalizedPage + 1));
97
- }
98
- keyboard.row();
99
- }
100
- keyboard.text(t("skills.button.cancel"), SKILLS_CALLBACK_CANCEL);
101
- return keyboard;
102
- }
103
- function buildSkillsConfirmKeyboard() {
104
- return new InlineKeyboard()
105
- .text(t("skills.button.execute"), SKILLS_CALLBACK_EXECUTE)
106
- .text(t("skills.button.cancel"), SKILLS_CALLBACK_CANCEL);
107
- }
108
16
  function parseSkillItems(value) {
109
17
  if (!Array.isArray(value)) {
110
18
  return null;
@@ -126,7 +34,7 @@ function parseSkillItems(value) {
126
34
  }
127
35
  return skills;
128
36
  }
129
- function parseSkillsMetadata(state) {
37
+ export function parseSkillsMetadata(state) {
130
38
  if (!state || state.kind !== "custom") {
131
39
  return null;
132
40
  }
@@ -169,41 +77,13 @@ function parseSkillsMetadata(state) {
169
77
  }
170
78
  return null;
171
79
  }
172
- function clearSkillsInteraction(reason) {
80
+ export function clearSkillsInteraction(reason) {
173
81
  const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
174
82
  if (metadata) {
175
83
  interactionManager.clear(reason);
176
84
  }
177
85
  }
178
- async function getSkillList(projectDirectory) {
179
- const { data, error } = await opencodeClient.command.list({
180
- directory: normalizeDirectoryForCommandApi(projectDirectory),
181
- });
182
- if (error || !data) {
183
- throw error || new Error("No skill data received");
184
- }
185
- return data
186
- .filter((skill) => {
187
- const source = skill.source;
188
- return typeof skill.name === "string" && skill.name.trim().length > 0 && source === "skill";
189
- })
190
- .map((skill) => ({
191
- name: skill.name,
192
- description: skill.description,
193
- }));
194
- }
195
- function parseSelectIndex(data) {
196
- if (!data.startsWith(SKILLS_CALLBACK_SELECT_PREFIX)) {
197
- return null;
198
- }
199
- const rawIndex = data.slice(SKILLS_CALLBACK_SELECT_PREFIX.length);
200
- const index = Number(rawIndex);
201
- if (!Number.isInteger(index) || index < 0) {
202
- return null;
203
- }
204
- return index;
205
- }
206
- async function executeSkill(ctx, deps, params) {
86
+ export async function executeSkill(ctx, deps, params) {
207
87
  const currentProject = getCurrentProject();
208
88
  if (!currentProject) {
209
89
  await ctx.reply(t("bot.project_not_selected"));
@@ -218,41 +98,6 @@ async function executeSkill(ctx, deps, params) {
218
98
  const promptText = args ? `/${params.skillName} ${args}` : `/${params.skillName}`;
219
99
  await processUserPrompt(ctx, promptText, deps);
220
100
  }
221
- export async function skillsCommand(ctx) {
222
- try {
223
- const currentProject = getCurrentProject();
224
- if (!currentProject) {
225
- await ctx.reply(t("bot.project_not_selected"));
226
- return;
227
- }
228
- const skills = await getSkillList(currentProject.worktree);
229
- if (skills.length === 0) {
230
- await ctx.reply(t("skills.empty"));
231
- return;
232
- }
233
- const pageSize = config.bot.commandsListLimit;
234
- const keyboard = buildSkillsListKeyboard(skills, 0, pageSize);
235
- const message = await ctx.reply(formatSkillsSelectText(0), {
236
- reply_markup: keyboard,
237
- });
238
- interactionManager.start({
239
- kind: "custom",
240
- expectedInput: "callback",
241
- metadata: {
242
- flow: "skills",
243
- stage: "list",
244
- messageId: message.message_id,
245
- projectDirectory: currentProject.worktree,
246
- skills,
247
- page: 0,
248
- },
249
- });
250
- }
251
- catch (error) {
252
- logger.error("[Skills] Error fetching skills list:", error);
253
- await ctx.reply(t("skills.fetch_error"));
254
- }
255
- }
256
101
  export async function handleSkillsCallback(ctx, deps) {
257
102
  const data = ctx.callbackQuery?.data;
258
103
  if (!data || !data.startsWith(SKILLS_CALLBACK_PREFIX)) {
@@ -316,7 +161,7 @@ export async function handleSkillsCallback(ctx, deps) {
316
161
  });
317
162
  return true;
318
163
  }
319
- const skillIndex = parseSelectIndex(data);
164
+ const skillIndex = parseSkillSelectCallback(data);
320
165
  if (skillIndex === null || metadata.stage !== "list") {
321
166
  await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
322
167
  return true;
@@ -349,28 +194,3 @@ export async function handleSkillsCallback(ctx, deps) {
349
194
  return true;
350
195
  }
351
196
  }
352
- export async function handleSkillTextArguments(ctx, deps) {
353
- const text = ctx.message?.text;
354
- if (!text || text.startsWith("/")) {
355
- return false;
356
- }
357
- const metadata = parseSkillsMetadata(interactionManager.getSnapshot());
358
- if (!metadata || metadata.stage !== "confirm") {
359
- return false;
360
- }
361
- const argumentsText = text.trim();
362
- if (!argumentsText) {
363
- await ctx.reply(t("skills.arguments_empty"));
364
- return true;
365
- }
366
- clearSkillsInteraction("skills_arguments_submitted");
367
- if (ctx.chat) {
368
- await ctx.api.deleteMessage(ctx.chat.id, metadata.messageId).catch(() => { });
369
- }
370
- await executeSkill(ctx, deps, {
371
- projectDirectory: metadata.projectDirectory,
372
- skillName: metadata.skillName,
373
- argumentsText,
374
- });
375
- return true;
376
- }
@@ -1,13 +1,12 @@
1
- import { InlineKeyboard } from "grammy";
2
- import { getAvailableVariants, getCurrentVariant, setCurrentVariant, formatVariantForDisplay, formatVariantForButton, } from "../../variant/manager.js";
3
- import { getStoredModel } from "../../model/manager.js";
4
- import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
1
+ import { getStoredAgent, resolveProjectAgent } from "../../app/services/agent-selection-service.js";
2
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
3
+ import { formatVariantForButton, formatVariantForDisplay, setCurrentVariant, } from "../../app/services/variant-selection-service.js";
5
4
  import { logger } from "../../utils/logger.js";
6
- import { keyboardManager } from "../../keyboard/manager.js";
7
- import { pinnedMessageManager } from "../../pinned/manager.js";
8
- import { createMainKeyboard } from "../utils/keyboard.js";
9
- import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
10
5
  import { t } from "../../i18n/index.js";
6
+ import { createMainKeyboard } from "../keyboards/main-reply-keyboard.js";
7
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
8
+ import { pinnedMessageManager } from "../pinned/pinned-message-manager.js";
9
+ import { clearActiveInlineMenu, ensureActiveInlineMenu } from "../menus/inline-menu.js";
11
10
  /**
12
11
  * Handle variant selection callback
13
12
  * @param ctx grammY context
@@ -76,64 +75,3 @@ export async function handleVariantSelect(ctx) {
76
75
  return false;
77
76
  }
78
77
  }
79
- /**
80
- * Build inline keyboard with available variants
81
- * @param currentVariant Current variant for highlighting
82
- * @param providerID Provider ID
83
- * @param modelID Model ID
84
- * @returns InlineKeyboard with variant selection buttons
85
- */
86
- export async function buildVariantSelectionMenu(currentVariant, providerID, modelID) {
87
- const keyboard = new InlineKeyboard();
88
- const variants = await getAvailableVariants(providerID, modelID);
89
- if (variants.length === 0) {
90
- logger.warn("[VariantHandler] No variants found");
91
- return keyboard;
92
- }
93
- // Filter only active variants (not disabled)
94
- const activeVariants = variants.filter((v) => !v.disabled);
95
- if (activeVariants.length === 0) {
96
- logger.warn("[VariantHandler] No active variants found");
97
- // If no active variants, show default at least
98
- keyboard.text(`✅ ${formatVariantForDisplay("default")}`, "variant:default").row();
99
- return keyboard;
100
- }
101
- // Add button for each variant (one per row)
102
- activeVariants.forEach((variant) => {
103
- const isActive = variant.id === currentVariant;
104
- const label = formatVariantForDisplay(variant.id);
105
- const labelWithCheck = isActive ? `✅ ${label}` : label;
106
- keyboard.text(labelWithCheck, `variant:${variant.id}`).row();
107
- });
108
- return keyboard;
109
- }
110
- /**
111
- * Show variant selection menu
112
- * @param ctx grammY context
113
- */
114
- export async function showVariantSelectionMenu(ctx) {
115
- try {
116
- const currentModel = getStoredModel();
117
- if (!currentModel.providerID || !currentModel.modelID) {
118
- await ctx.reply(t("variant.select_model_first"));
119
- return;
120
- }
121
- const currentVariant = getCurrentVariant();
122
- const keyboard = await buildVariantSelectionMenu(currentVariant, currentModel.providerID, currentModel.modelID);
123
- if (keyboard.inline_keyboard.length === 0) {
124
- await ctx.reply(t("variant.menu.empty"));
125
- return;
126
- }
127
- const displayName = formatVariantForDisplay(currentVariant);
128
- const text = t("variant.menu.current", { name: displayName });
129
- await replyWithInlineMenu(ctx, {
130
- menuKind: "variant",
131
- text,
132
- keyboard,
133
- });
134
- }
135
- catch (err) {
136
- logger.error("[VariantHandler] Error showing variant menu:", err);
137
- await ctx.reply(t("variant.menu.error"));
138
- }
139
- }