@grinev/opencode-telegram-bot 0.21.0 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) hide show
  1. package/.env.example +18 -2
  2. package/README.md +21 -8
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +4 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +3 -3
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +5 -5
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +3 -3
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +110 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  62. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  63. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  64. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  65. package/dist/bot/commands/command-catalog-command.js +42 -0
  66. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  67. package/dist/bot/commands/ls-command.js +48 -0
  68. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  69. package/dist/bot/commands/messages-command.js +59 -0
  70. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  71. package/dist/bot/commands/open-command.js +45 -0
  72. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  73. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  74. package/dist/bot/commands/projects-command.js +32 -0
  75. package/dist/bot/commands/rename-command.js +33 -0
  76. package/dist/bot/commands/sessions-command.js +42 -0
  77. package/dist/bot/commands/skills-catalog-command.js +42 -0
  78. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  79. package/dist/bot/commands/{status.js → status-command.js} +9 -9
  80. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  81. package/dist/bot/commands/tasklist-command.js +40 -0
  82. package/dist/bot/commands/{tts.js → tts-command.js} +2 -2
  83. package/dist/bot/commands/worktree-command.js +47 -0
  84. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  85. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  86. package/dist/bot/handlers/photo-handler.js +44 -0
  87. package/dist/bot/handlers/prompt.js +16 -16
  88. package/dist/bot/handlers/text-message-handler.js +61 -0
  89. package/dist/bot/handlers/tts-response-handler.js +29 -0
  90. package/dist/bot/handlers/{voice.js → voice-handler.js} +2 -2
  91. package/dist/bot/index.js +27 -1130
  92. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  93. package/dist/bot/keyboards/keyboard-types.js +1 -0
  94. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  95. package/dist/bot/menus/agent-selection-menu.js +54 -0
  96. package/dist/bot/menus/command-catalog-menu.js +101 -0
  97. package/dist/bot/menus/context-control-menu.js +33 -0
  98. package/dist/bot/menus/file-browser-menu.js +265 -0
  99. package/dist/bot/{handlers → menus}/inline-menu.js +4 -33
  100. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  101. package/dist/bot/menus/message-history-menu.js +107 -0
  102. package/dist/bot/menus/model-selection-menu.js +66 -0
  103. package/dist/bot/menus/permission-menu.js +116 -0
  104. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  105. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  106. package/dist/bot/menus/rename-menu.js +6 -0
  107. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  108. package/dist/bot/menus/session-selection-menu.js +115 -0
  109. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  110. package/dist/bot/menus/variant-selection-menu.js +67 -0
  111. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  112. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  113. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  114. package/dist/bot/messages/external-user-input-notification.js +22 -0
  115. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  116. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  117. package/dist/bot/messages/summary-message-formatter.js +130 -0
  118. package/dist/bot/{utils → messages}/telegram-text.js +60 -0
  119. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  120. package/dist/bot/middleware/interaction-guard.js +2 -2
  121. package/dist/bot/middleware/unknown-command.js +1 -1
  122. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  123. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  124. package/dist/bot/pinned/pinned-message-types.js +1 -0
  125. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  126. package/dist/bot/render/types.js +1 -0
  127. package/dist/bot/routers/command-router.js +73 -0
  128. package/dist/bot/routers/message-router.js +150 -0
  129. package/dist/bot/services/attach-presentation.js +40 -0
  130. package/dist/bot/services/event-subscription-service.js +842 -0
  131. package/dist/bot/services/project-switch-presentation.js +26 -0
  132. package/dist/bot/streaming/response-streamer.js +18 -2
  133. package/dist/cli.js +4 -4
  134. package/dist/config.js +21 -3
  135. package/dist/i18n/ar.js +491 -0
  136. package/dist/i18n/index.js +7 -0
  137. package/dist/index.js +1 -1
  138. package/dist/opencode/ready-refresh.js +2 -2
  139. package/dist/{service → runtime/service}/manager.js +2 -2
  140. package/dist/runtime/service/types.js +1 -0
  141. package/package.json +1 -1
  142. package/dist/bot/commands/ls.js +0 -445
  143. package/dist/bot/commands/open.js +0 -319
  144. package/dist/bot/commands/worktree.js +0 -201
  145. package/dist/bot/handlers/permission.js +0 -235
  146. package/dist/bot/utils/browser-roots.js +0 -140
  147. package/dist/bot/utils/file-tree.js +0 -92
  148. package/dist/bot/utils/send-tts-response.js +0 -37
  149. package/dist/bot/utils/switch-project.js +0 -45
  150. package/dist/bot/utils/telegram-file-url.js +0 -11
  151. package/dist/interaction/busy.js +0 -8
  152. package/dist/interaction/cleanup.js +0 -32
  153. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  154. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  155. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  156. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  157. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  158. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  159. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  160. /package/dist/{model/types.js → app/types/model.js} +0 -0
  161. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  162. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  163. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  164. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  165. /package/dist/{question/types.js → app/types/session.js} +0 -0
  166. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  167. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  168. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  169. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  170. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  171. /package/dist/bot/{utils → messages}/thinking-message.js +0 -0
  172. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  173. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  174. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  175. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  176. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  177. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  178. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  179. /package/dist/{telegram → bot}/render/validator.js +0 -0
  180. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  181. /package/dist/bot/{utils → streaming}/finalize-assistant-response.js +0 -0
  182. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
@@ -1,9 +1,9 @@
1
- import { createMainKeyboard } from "../bot/utils/keyboard.js";
2
- import { getStoredAgent } from "../agent/manager.js";
3
- import { getStoredModel } from "../model/manager.js";
4
- import { formatVariantForButton } from "../variant/manager.js";
5
- import { logger } from "../utils/logger.js";
6
- import { t } from "../i18n/index.js";
1
+ import { createMainKeyboard } from "./main-reply-keyboard.js";
2
+ import { getStoredAgent } from "../../app/services/agent-selection-service.js";
3
+ import { getStoredModel } from "../../app/services/model-selection-service.js";
4
+ import { formatVariantForButton } from "../../app/services/variant-selection-service.js";
5
+ import { logger } from "../../utils/logger.js";
6
+ import { t } from "../../i18n/index.js";
7
7
  /**
8
8
  * Keyboard Manager - manages Reply Keyboard state and updates
9
9
  * Singleton pattern
@@ -0,0 +1 @@
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import { Keyboard } from "grammy";
2
- import { getAgentButtonLabel } from "../../agent/types.js";
3
- import { formatModelForButton } from "../../model/types.js";
2
+ import { getAgentButtonLabel } from "../../app/types/agent.js";
3
+ import { formatModelForButton } from "../../app/types/model.js";
4
4
  import { t } from "../../i18n/index.js";
5
5
  /**
6
6
  * Format token count for display (e.g., 150000 -> "150K", 1500000 -> "1.5M")
@@ -0,0 +1,54 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { fetchCurrentAgent, getAvailableAgents } from "../../app/services/agent-selection-service.js";
3
+ import { getAgentDisplayName } from "../../app/types/agent.js";
4
+ import { logger } from "../../utils/logger.js";
5
+ import { t } from "../../i18n/index.js";
6
+ import { replyWithInlineMenu } from "./inline-menu.js";
7
+ /**
8
+ * Build inline keyboard with available agents
9
+ * @param currentAgent Current agent name for highlighting
10
+ * @returns InlineKeyboard with agent selection buttons
11
+ */
12
+ export async function buildAgentSelectionMenu(currentAgent) {
13
+ const keyboard = new InlineKeyboard();
14
+ const agents = await getAvailableAgents();
15
+ if (agents.length === 0) {
16
+ logger.warn("[AgentHandler] No available agents found");
17
+ return keyboard;
18
+ }
19
+ // Add button for each agent
20
+ agents.forEach((agent) => {
21
+ const isActive = agent.name === currentAgent;
22
+ const label = isActive
23
+ ? `✅ ${getAgentDisplayName(agent.name)}`
24
+ : getAgentDisplayName(agent.name);
25
+ keyboard.text(label, `agent:${agent.name}`).row();
26
+ });
27
+ return keyboard;
28
+ }
29
+ /**
30
+ * Show agent selection menu
31
+ * @param ctx grammY context
32
+ */
33
+ export async function showAgentSelectionMenu(ctx) {
34
+ try {
35
+ const currentAgent = await fetchCurrentAgent();
36
+ const keyboard = await buildAgentSelectionMenu(currentAgent);
37
+ if (keyboard.inline_keyboard.length === 0) {
38
+ await ctx.reply(t("agent.menu.empty"));
39
+ return;
40
+ }
41
+ const text = currentAgent
42
+ ? t("agent.menu.current", { name: getAgentDisplayName(currentAgent) })
43
+ : t("agent.menu.select");
44
+ await replyWithInlineMenu(ctx, {
45
+ menuKind: "agent",
46
+ text,
47
+ keyboard,
48
+ });
49
+ }
50
+ catch (err) {
51
+ logger.error("[AgentHandler] Error showing agent menu:", err);
52
+ await ctx.reply(t("agent.menu.error"));
53
+ }
54
+ }
@@ -0,0 +1,101 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { t } from "../../i18n/index.js";
3
+ export const COMMANDS_CALLBACK_PREFIX = "commands:";
4
+ export const COMMANDS_CALLBACK_SELECT_PREFIX = `${COMMANDS_CALLBACK_PREFIX}select:`;
5
+ const COMMANDS_CALLBACK_PAGE_PREFIX = `${COMMANDS_CALLBACK_PREFIX}page:`;
6
+ export const COMMANDS_CALLBACK_CANCEL = `${COMMANDS_CALLBACK_PREFIX}cancel`;
7
+ export const COMMANDS_CALLBACK_EXECUTE = `${COMMANDS_CALLBACK_PREFIX}execute`;
8
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
9
+ export function formatExecutingCommandMessage(commandName, args) {
10
+ const prefix = t("commands.executing_prefix");
11
+ const commandText = `/${commandName}`;
12
+ const argsSuffix = args ? ` ${args}` : "";
13
+ return {
14
+ text: `${prefix}\n${commandText}${argsSuffix}`,
15
+ entities: [
16
+ {
17
+ type: "code",
18
+ offset: prefix.length + 1,
19
+ length: commandText.length,
20
+ },
21
+ ],
22
+ };
23
+ }
24
+ export function buildCommandPageCallback(page) {
25
+ return `${COMMANDS_CALLBACK_PAGE_PREFIX}${page}`;
26
+ }
27
+ export function parseCommandPageCallback(data) {
28
+ if (!data.startsWith(COMMANDS_CALLBACK_PAGE_PREFIX)) {
29
+ return null;
30
+ }
31
+ const rawPage = data.slice(COMMANDS_CALLBACK_PAGE_PREFIX.length);
32
+ const page = Number(rawPage);
33
+ if (!Number.isInteger(page) || page < 0) {
34
+ return null;
35
+ }
36
+ return page;
37
+ }
38
+ export function parseCommandSelectCallback(data) {
39
+ if (!data.startsWith(COMMANDS_CALLBACK_SELECT_PREFIX)) {
40
+ return null;
41
+ }
42
+ const rawIndex = data.slice(COMMANDS_CALLBACK_SELECT_PREFIX.length);
43
+ const index = Number(rawIndex);
44
+ if (!Number.isInteger(index) || index < 0) {
45
+ return null;
46
+ }
47
+ return index;
48
+ }
49
+ export function formatCommandsSelectText(page) {
50
+ if (page === 0) {
51
+ return t("commands.select");
52
+ }
53
+ return t("commands.select_page", { page: page + 1 });
54
+ }
55
+ function formatCommandButtonLabel(command) {
56
+ const description = command.description?.trim() || t("commands.no_description");
57
+ const rawLabel = `/${command.name} - ${description}`;
58
+ if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
59
+ return rawLabel;
60
+ }
61
+ return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
62
+ }
63
+ export function calculateCommandsPaginationRange(totalCommands, page, pageSize) {
64
+ const safePageSize = Math.max(1, pageSize);
65
+ const totalPages = Math.max(1, Math.ceil(totalCommands / safePageSize));
66
+ const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
67
+ const startIndex = normalizedPage * safePageSize;
68
+ const endIndex = Math.min(startIndex + safePageSize, totalCommands);
69
+ return {
70
+ page: normalizedPage,
71
+ totalPages,
72
+ startIndex,
73
+ endIndex,
74
+ };
75
+ }
76
+ export function buildCommandsListKeyboard(commands, page, pageSize) {
77
+ const keyboard = new InlineKeyboard();
78
+ const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateCommandsPaginationRange(commands.length, page, pageSize);
79
+ commands.slice(startIndex, endIndex).forEach((command, index) => {
80
+ const globalIndex = startIndex + index;
81
+ keyboard
82
+ .text(formatCommandButtonLabel(command), `${COMMANDS_CALLBACK_SELECT_PREFIX}${globalIndex}`)
83
+ .row();
84
+ });
85
+ if (totalPages > 1) {
86
+ if (normalizedPage > 0) {
87
+ keyboard.text(t("commands.button.prev_page"), buildCommandPageCallback(normalizedPage - 1));
88
+ }
89
+ if (normalizedPage < totalPages - 1) {
90
+ keyboard.text(t("commands.button.next_page"), buildCommandPageCallback(normalizedPage + 1));
91
+ }
92
+ keyboard.row();
93
+ }
94
+ keyboard.text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
95
+ return keyboard;
96
+ }
97
+ export function buildCommandsConfirmKeyboard() {
98
+ return new InlineKeyboard()
99
+ .text(t("commands.button.execute"), COMMANDS_CALLBACK_EXECUTE)
100
+ .text(t("commands.button.cancel"), COMMANDS_CALLBACK_CANCEL);
101
+ }
@@ -0,0 +1,33 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { getCurrentSession } from "../../app/services/session-service.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { replyWithInlineMenu } from "./inline-menu.js";
6
+ /**
7
+ * Build inline keyboard with compact confirmation menu
8
+ * @returns InlineKeyboard with confirmation button
9
+ */
10
+ export function buildCompactConfirmationMenu() {
11
+ const keyboard = new InlineKeyboard();
12
+ keyboard.text(t("context.button.confirm"), "compact:confirm");
13
+ return keyboard;
14
+ }
15
+ /**
16
+ * Handle context button press (text message from Reply Keyboard)
17
+ * Shows inline menu with compact confirmation
18
+ * @param ctx grammY context
19
+ */
20
+ export async function handleContextButtonPress(ctx) {
21
+ logger.debug("[ContextHandler] Context button pressed");
22
+ const session = getCurrentSession();
23
+ if (!session) {
24
+ await ctx.reply(t("context.no_active_session"));
25
+ return;
26
+ }
27
+ const keyboard = buildCompactConfirmationMenu();
28
+ await replyWithInlineMenu(ctx, {
29
+ menuKind: "context",
30
+ text: t("context.confirm_text", { title: session.title }),
31
+ keyboard,
32
+ });
33
+ }
@@ -0,0 +1,265 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import path from "node:path";
3
+ import { formatFileSize } from "../../app/services/file-download-service.js";
4
+ import { buildEntryLabel, buildTreeHeader, getBrowserRoots, getFileDetails, getParentPath, isAllowedRoot, isProjectRoot, MAX_ENTRIES_PER_PAGE, pathToDisplayPath, scanDirectory, scanLsDirectory, } from "../../app/services/file-browser-service.js";
5
+ import { t } from "../../i18n/index.js";
6
+ import { appendInlineMenuCancelButton } from "./inline-menu.js";
7
+ export const OPEN_CALLBACK_PREFIX = "open:";
8
+ export const OPEN_CALLBACK_NAV_PREFIX = "open:nav:";
9
+ export const OPEN_CALLBACK_SELECT_PREFIX = "open:sel:";
10
+ export const OPEN_CALLBACK_PAGE_PREFIX = "open:pg:";
11
+ export const OPEN_CALLBACK_ROOTS = "open:roots";
12
+ export const LS_CALLBACK_PREFIX = "ls:";
13
+ export const LS_CALLBACK_NAV_PREFIX = "ls:nav:";
14
+ export const LS_CALLBACK_FILE_PREFIX = "ls:file:";
15
+ export const LS_CALLBACK_DOWNLOAD_PREFIX = "ls:download:";
16
+ export const LS_CALLBACK_BACK_PREFIX = "ls:back:";
17
+ export const LS_CALLBACK_PAGE_PREFIX = "ls:pg:";
18
+ const PAGE_SEPARATOR = "|";
19
+ const MAX_BUTTON_LABEL_LENGTH = 64;
20
+ const openPathIndex = new Map();
21
+ const lsPathIndex = new Map();
22
+ let openPathCounter = 0;
23
+ let lsPathCounter = 0;
24
+ export function clearOpenPathIndex() {
25
+ openPathIndex.clear();
26
+ openPathCounter = 0;
27
+ }
28
+ export function clearLsPathIndex() {
29
+ lsPathIndex.clear();
30
+ lsPathCounter = 0;
31
+ }
32
+ function truncateLabel(label, maxLen = MAX_BUTTON_LABEL_LENGTH) {
33
+ if (label.length <= maxLen) {
34
+ return label;
35
+ }
36
+ return `${label.slice(0, Math.max(0, maxLen - 3))}...`;
37
+ }
38
+ function encodePathForCallback(pathIndex, nextCounter, prefix, fullPath, reserveBytes = 0) {
39
+ const naive = `${prefix}${fullPath}`;
40
+ if (Buffer.byteLength(naive, "utf-8") + reserveBytes <= 64) {
41
+ return naive;
42
+ }
43
+ const key = `#${nextCounter()}`;
44
+ pathIndex.set(key, fullPath);
45
+ return `${prefix}${key}`;
46
+ }
47
+ function decodePathFromCallback(pathIndex, prefix, data) {
48
+ if (!data.startsWith(prefix)) {
49
+ return null;
50
+ }
51
+ const raw = data.slice(prefix.length);
52
+ if (raw.startsWith("#")) {
53
+ return pathIndex.get(raw) ?? null;
54
+ }
55
+ return raw;
56
+ }
57
+ export function encodeOpenPathForCallback(prefix, fullPath, reserveBytes = 0) {
58
+ return encodePathForCallback(openPathIndex, () => openPathCounter++, prefix, fullPath, reserveBytes);
59
+ }
60
+ export function decodeOpenPathFromCallback(prefix, data) {
61
+ return decodePathFromCallback(openPathIndex, prefix, data);
62
+ }
63
+ export function encodeLsPathForCallback(prefix, fullPath, reserveBytes = 0) {
64
+ return encodePathForCallback(lsPathIndex, () => lsPathCounter++, prefix, fullPath, reserveBytes);
65
+ }
66
+ export function decodeLsPathFromCallback(prefix, data) {
67
+ return decodePathFromCallback(lsPathIndex, prefix, data);
68
+ }
69
+ function encodeOpenPaginationCallback(currentPath, page) {
70
+ const pageSuffix = `${PAGE_SEPARATOR}${page}`;
71
+ const reserveBytes = Buffer.byteLength(pageSuffix, "utf-8");
72
+ return `${encodeOpenPathForCallback(OPEN_CALLBACK_PAGE_PREFIX, currentPath, reserveBytes)}${pageSuffix}`;
73
+ }
74
+ export function decodeOpenPaginationCallback(data) {
75
+ return decodePathWithPageCallback(openPathIndex, data, OPEN_CALLBACK_PAGE_PREFIX);
76
+ }
77
+ function encodeLsPathWithPageCallback(prefix, fullPath, page) {
78
+ const pageSuffix = `${PAGE_SEPARATOR}${page}`;
79
+ const reserveBytes = Buffer.byteLength(pageSuffix, "utf-8");
80
+ return `${encodeLsPathForCallback(prefix, fullPath, reserveBytes)}${pageSuffix}`;
81
+ }
82
+ export function decodeLsPathWithPageCallback(data, prefix) {
83
+ return decodePathWithPageCallback(lsPathIndex, data, prefix);
84
+ }
85
+ function decodePathWithPageCallback(pathIndex, data, prefix) {
86
+ if (!data.startsWith(prefix)) {
87
+ return null;
88
+ }
89
+ const payload = data.slice(prefix.length);
90
+ const separatorIndex = payload.lastIndexOf(PAGE_SEPARATOR);
91
+ if (separatorIndex < 0) {
92
+ return null;
93
+ }
94
+ const pathRef = payload.slice(0, separatorIndex);
95
+ const page = Number.parseInt(payload.slice(separatorIndex + 1), 10);
96
+ if (Number.isNaN(page)) {
97
+ return null;
98
+ }
99
+ const resolvedPath = pathRef.startsWith("#") ? (pathIndex.get(pathRef) ?? null) : pathRef;
100
+ if (resolvedPath === null) {
101
+ return null;
102
+ }
103
+ return { path: resolvedPath, page };
104
+ }
105
+ function buildOpenBrowseKeyboard(entries, currentPath, hasParent, page, totalCount) {
106
+ const keyboard = new InlineKeyboard();
107
+ const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
108
+ for (const entry of entries) {
109
+ const label = truncateLabel(buildEntryLabel(entry));
110
+ keyboard.text(label, encodeOpenPathForCallback(OPEN_CALLBACK_NAV_PREFIX, entry.fullPath)).row();
111
+ }
112
+ const atRoot = isAllowedRoot(currentPath);
113
+ const showUp = hasParent && !atRoot;
114
+ const showRoots = getBrowserRoots().length > 1;
115
+ if (showUp || showRoots) {
116
+ if (showUp) {
117
+ const parentPath = path.dirname(currentPath);
118
+ keyboard.text(t("open.back"), encodeOpenPathForCallback(OPEN_CALLBACK_NAV_PREFIX, parentPath));
119
+ }
120
+ if (showRoots) {
121
+ keyboard.text(t("open.roots"), OPEN_CALLBACK_ROOTS);
122
+ }
123
+ keyboard.row();
124
+ }
125
+ if (totalPages > 1) {
126
+ if (page > 0) {
127
+ keyboard.text(t("open.prev_page"), encodeOpenPaginationCallback(currentPath, page - 1));
128
+ }
129
+ if (page < totalPages - 1) {
130
+ keyboard.text(t("open.next_page"), encodeOpenPaginationCallback(currentPath, page + 1));
131
+ }
132
+ keyboard.row();
133
+ }
134
+ keyboard
135
+ .text(t("open.select_current"), encodeOpenPathForCallback(OPEN_CALLBACK_SELECT_PREFIX, currentPath))
136
+ .row();
137
+ appendInlineMenuCancelButton(keyboard, "open");
138
+ return keyboard;
139
+ }
140
+ export async function renderOpenBrowseView(dirPath, page = 0) {
141
+ const result = await scanDirectory(dirPath, page);
142
+ if ("error" in result) {
143
+ return { error: result.error };
144
+ }
145
+ const totalPages = Math.max(1, Math.ceil(result.totalCount / MAX_ENTRIES_PER_PAGE));
146
+ return {
147
+ text: buildTreeHeader(result.displayPath, result.totalCount, result.page, totalPages),
148
+ keyboard: buildOpenBrowseKeyboard(result.entries, result.currentPath, result.hasParent, result.page, result.totalCount),
149
+ };
150
+ }
151
+ export function buildOpenRootsKeyboard() {
152
+ const keyboard = new InlineKeyboard();
153
+ for (const root of getBrowserRoots()) {
154
+ const label = truncateLabel(`📂 ${pathToDisplayPath(root)}`);
155
+ keyboard.text(label, encodeOpenPathForCallback(OPEN_CALLBACK_NAV_PREFIX, root)).row();
156
+ }
157
+ appendInlineMenuCancelButton(keyboard, "open");
158
+ return keyboard;
159
+ }
160
+ function escapeHtml(text) {
161
+ return text
162
+ .replace(/&/g, "&amp;")
163
+ .replace(/</g, "&lt;")
164
+ .replace(/>/g, "&gt;")
165
+ .replace(/\"/g, "&quot;")
166
+ .replace(/'/g, "&#39;");
167
+ }
168
+ function buildLsEntryLabel(entry) {
169
+ return `${entry.type === "directory" ? "📁" : "📄"} ${entry.name}`;
170
+ }
171
+ function buildLsHeader(displayPath, totalCount, page, totalPages) {
172
+ let header = `📁 ${t("ls.header")}\n<code>${escapeHtml(displayPath)}</code>`;
173
+ if (totalPages > 1) {
174
+ header += `\n(${page + 1}/${totalPages})`;
175
+ }
176
+ header += `\n${t("ls.total", { count: totalCount })}`;
177
+ return header;
178
+ }
179
+ function buildFileDetailsText(fileDetails) {
180
+ return (`📄 ${t("ls.file.header")}\n<code>${escapeHtml(fileDetails.name)}</code>\n` +
181
+ `${t("commands.download.size")}: ${formatFileSize(fileDetails.size)}\n` +
182
+ `${t("commands.download.modified")}: ${fileDetails.modified.toLocaleDateString()}`);
183
+ }
184
+ function encodeLsPaginationCallback(currentPath, page) {
185
+ return encodeLsPathWithPageCallback(LS_CALLBACK_PAGE_PREFIX, currentPath, page);
186
+ }
187
+ export function decodeLsPaginationCallback(data) {
188
+ return decodeLsPathWithPageCallback(data, LS_CALLBACK_PAGE_PREFIX);
189
+ }
190
+ function encodeLsFileCallback(fullPath, page) {
191
+ return encodeLsPathWithPageCallback(LS_CALLBACK_FILE_PREFIX, fullPath, page);
192
+ }
193
+ export function decodeLsFileCallback(data) {
194
+ return decodeLsPathWithPageCallback(data, LS_CALLBACK_FILE_PREFIX);
195
+ }
196
+ function encodeLsBackCallback(directoryPath, page) {
197
+ return encodeLsPathWithPageCallback(LS_CALLBACK_BACK_PREFIX, directoryPath, page);
198
+ }
199
+ export function decodeLsBackCallback(data) {
200
+ return decodeLsPathWithPageCallback(data, LS_CALLBACK_BACK_PREFIX);
201
+ }
202
+ function buildLsBrowseKeyboard(entries, currentPath, hasParent, page, totalCount) {
203
+ const keyboard = new InlineKeyboard();
204
+ const totalPages = Math.max(1, Math.ceil(totalCount / MAX_ENTRIES_PER_PAGE));
205
+ for (const entry of entries) {
206
+ const label = truncateLabel(buildLsEntryLabel(entry));
207
+ const callbackData = entry.type === "directory"
208
+ ? encodeLsPathForCallback(LS_CALLBACK_NAV_PREFIX, entry.fullPath)
209
+ : encodeLsFileCallback(entry.fullPath, page);
210
+ keyboard.text(label, callbackData).row();
211
+ }
212
+ if (hasParent && !isProjectRoot(currentPath)) {
213
+ keyboard
214
+ .text(t("open.back"), encodeLsPathForCallback(LS_CALLBACK_NAV_PREFIX, getParentPath(currentPath)))
215
+ .row();
216
+ }
217
+ if (totalPages > 1) {
218
+ if (page > 0) {
219
+ keyboard.text(t("open.prev_page"), encodeLsPaginationCallback(currentPath, page - 1));
220
+ }
221
+ if (page < totalPages - 1) {
222
+ keyboard.text(t("open.next_page"), encodeLsPaginationCallback(currentPath, page + 1));
223
+ }
224
+ keyboard.row();
225
+ }
226
+ appendInlineMenuCancelButton(keyboard, "ls");
227
+ return keyboard;
228
+ }
229
+ function buildLsFileDetailsKeyboard(filePath, page) {
230
+ const keyboard = new InlineKeyboard();
231
+ const parentPath = getParentPath(filePath);
232
+ keyboard.text(t("ls.file.download"), encodeLsPathForCallback(LS_CALLBACK_DOWNLOAD_PREFIX, filePath));
233
+ keyboard.text(t("ls.file.back"), encodeLsBackCallback(parentPath, page));
234
+ keyboard.row();
235
+ appendInlineMenuCancelButton(keyboard, "ls");
236
+ return keyboard;
237
+ }
238
+ function hasBrowseActions(currentPath, hasParent, totalCount) {
239
+ if (totalCount > 0) {
240
+ return true;
241
+ }
242
+ return hasParent && !isProjectRoot(currentPath);
243
+ }
244
+ export async function renderLsBrowseView(dirPath, page = 0) {
245
+ const result = await scanLsDirectory(dirPath, page);
246
+ if ("error" in result) {
247
+ return result;
248
+ }
249
+ const totalPages = Math.max(1, Math.ceil(result.totalCount / MAX_ENTRIES_PER_PAGE));
250
+ return {
251
+ text: buildLsHeader(result.displayPath, result.totalCount, result.page, totalPages),
252
+ hasActions: hasBrowseActions(result.currentPath, result.hasParent, result.totalCount),
253
+ keyboard: buildLsBrowseKeyboard(result.entries, result.currentPath, result.hasParent, result.page, result.totalCount),
254
+ };
255
+ }
256
+ export async function renderLsFileDetailsView(filePath, page) {
257
+ const fileDetails = await getFileDetails(filePath);
258
+ if ("error" in fileDetails) {
259
+ return fileDetails;
260
+ }
261
+ return {
262
+ text: buildFileDetailsText(fileDetails),
263
+ keyboard: buildLsFileDetailsKeyboard(fileDetails.fullPath, page),
264
+ };
265
+ }
@@ -1,8 +1,8 @@
1
- import { interactionManager } from "../../interaction/manager.js";
1
+ import { interactionManager } from "../../app/managers/interaction-manager.js";
2
2
  import { logger } from "../../utils/logger.js";
3
3
  import { t } from "../../i18n/index.js";
4
- const INLINE_MENU_CANCEL_PREFIX = "inline:cancel:";
5
- const LEGACY_CONTEXT_CANCEL_CALLBACK = "compact:cancel";
4
+ export const INLINE_MENU_CANCEL_PREFIX = "inline:cancel:";
5
+ export const LEGACY_CONTEXT_CANCEL_CALLBACK = "compact:cancel";
6
6
  const INLINE_MENU_KINDS = [
7
7
  "project",
8
8
  "session",
@@ -14,7 +14,7 @@ const INLINE_MENU_KINDS = [
14
14
  "ls",
15
15
  "worktree",
16
16
  ];
17
- function isInlineMenuKind(value) {
17
+ export function isInlineMenuKind(value) {
18
18
  return INLINE_MENU_KINDS.includes(value);
19
19
  }
20
20
  function getCallbackMessageId(ctx) {
@@ -98,32 +98,3 @@ export function clearActiveInlineMenu(reason) {
98
98
  interactionManager.clear(reason);
99
99
  }
100
100
  }
101
- export async function handleInlineMenuCancel(ctx) {
102
- const data = ctx.callbackQuery?.data;
103
- if (!data) {
104
- return false;
105
- }
106
- let menuKind = null;
107
- if (data === LEGACY_CONTEXT_CANCEL_CALLBACK) {
108
- menuKind = "context";
109
- }
110
- else if (data.startsWith(INLINE_MENU_CANCEL_PREFIX)) {
111
- const rawKind = data.slice(INLINE_MENU_CANCEL_PREFIX.length);
112
- if (!isInlineMenuKind(rawKind)) {
113
- return false;
114
- }
115
- menuKind = rawKind;
116
- }
117
- else {
118
- return false;
119
- }
120
- const isActive = await ensureActiveInlineMenu(ctx, menuKind);
121
- if (!isActive) {
122
- return true;
123
- }
124
- clearActiveInlineMenu(`inline_menu_cancel:${menuKind}`);
125
- await ctx.answerCallbackQuery({ text: t("inline.cancelled_callback") }).catch(() => { });
126
- await ctx.deleteMessage().catch(() => { });
127
- logger.debug(`[InlineMenu] Menu cancelled: kind=${menuKind}`);
128
- return true;
129
- }
@@ -0,0 +1,97 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { t } from "../../i18n/index.js";
3
+ export const MCPS_CALLBACK_PREFIX = "mcps:";
4
+ export const MCPS_CALLBACK_SELECT_PREFIX = `${MCPS_CALLBACK_PREFIX}select:`;
5
+ export const MCPS_CALLBACK_TOGGLE = `${MCPS_CALLBACK_PREFIX}toggle`;
6
+ export const MCPS_CALLBACK_BACK = `${MCPS_CALLBACK_PREFIX}back`;
7
+ export const MCPS_CALLBACK_CANCEL = `${MCPS_CALLBACK_PREFIX}cancel`;
8
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
9
+ function getStatusLabel(status) {
10
+ switch (status.status) {
11
+ case "connected":
12
+ return t("mcps.status.connected");
13
+ case "disabled":
14
+ return t("mcps.status.disabled");
15
+ case "failed":
16
+ return t("mcps.status.failed");
17
+ case "needs_auth":
18
+ return t("mcps.status.needs_auth");
19
+ case "needs_client_registration":
20
+ return t("mcps.status.needs_client_registration");
21
+ default:
22
+ return t("common.unknown");
23
+ }
24
+ }
25
+ function getStatusEmoji(status) {
26
+ switch (status.status) {
27
+ case "connected":
28
+ return "🟢";
29
+ case "disabled":
30
+ return "🔴";
31
+ case "failed":
32
+ return "⚠️";
33
+ case "needs_auth":
34
+ return "🔒";
35
+ case "needs_client_registration":
36
+ return "🔒";
37
+ default:
38
+ return "❓";
39
+ }
40
+ }
41
+ function formatMcpButtonLabel(server) {
42
+ const rawLabel = `${getStatusEmoji(server.status)} ${server.name}`;
43
+ if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
44
+ return rawLabel;
45
+ }
46
+ return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
47
+ }
48
+ export function parseMcpSelectCallback(data) {
49
+ if (!data.startsWith(MCPS_CALLBACK_SELECT_PREFIX)) {
50
+ return null;
51
+ }
52
+ const index = Number(data.slice(MCPS_CALLBACK_SELECT_PREFIX.length));
53
+ if (!Number.isInteger(index) || index < 0) {
54
+ return null;
55
+ }
56
+ return index;
57
+ }
58
+ export function buildMcpsListKeyboard(servers) {
59
+ const keyboard = new InlineKeyboard();
60
+ servers.forEach((server, index) => {
61
+ keyboard.text(formatMcpButtonLabel(server), `${MCPS_CALLBACK_SELECT_PREFIX}${index}`).row();
62
+ });
63
+ keyboard.text(t("inline.button.cancel"), MCPS_CALLBACK_CANCEL);
64
+ return keyboard;
65
+ }
66
+ export function buildMcpsDetailKeyboard(server) {
67
+ const keyboard = new InlineKeyboard();
68
+ let hasToggleButton = false;
69
+ if (server.status.status === "connected") {
70
+ keyboard.text(t("mcps.button.disable"), MCPS_CALLBACK_TOGGLE);
71
+ hasToggleButton = true;
72
+ }
73
+ else if (server.status.status === "disabled" || server.status.status === "failed") {
74
+ keyboard.text(t("mcps.button.enable"), MCPS_CALLBACK_TOGGLE);
75
+ hasToggleButton = true;
76
+ }
77
+ if (hasToggleButton) {
78
+ keyboard.row();
79
+ }
80
+ keyboard.text(t("mcps.button.back"), MCPS_CALLBACK_BACK);
81
+ keyboard.text(t("inline.button.cancel"), MCPS_CALLBACK_CANCEL);
82
+ return keyboard;
83
+ }
84
+ export function buildMcpsDetailText(server) {
85
+ const lines = [];
86
+ lines.push(t("mcps.detail.title", { name: server.name }));
87
+ lines.push("");
88
+ lines.push(t("mcps.detail.status", { status: getStatusLabel(server.status) }));
89
+ if (server.status.status === "failed" || server.status.status === "needs_client_registration") {
90
+ lines.push(t("mcps.detail.error", { error: server.status.error }));
91
+ }
92
+ if (server.status.status === "needs_auth" || server.status.status === "needs_client_registration") {
93
+ lines.push("");
94
+ lines.push(t("mcps.auth_required"));
95
+ }
96
+ return lines.join("\n");
97
+ }