@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.
- package/.env.example +10 -15
- package/README.md +33 -13
- package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
- package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
- package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
- package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +20 -132
- package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
- package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
- package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
- package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
- package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
- package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
- package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
- package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
- package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
- package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
- package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +177 -13
- package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
- package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
- package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
- package/dist/app/services/command-catalog-service.js +21 -0
- package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
- package/dist/app/services/file-browser-service.js +265 -0
- package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
- package/dist/app/services/mcp-catalog-service.js +79 -0
- package/dist/app/services/message-history-service.js +81 -0
- package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
- package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
- package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
- package/dist/{project/manager.js → app/services/project-service.js} +3 -3
- package/dist/app/services/project-switch-service.js +39 -0
- package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
- package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +27 -12
- package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
- package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
- package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
- package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
- package/dist/{session/manager.js → app/services/session-service.js} +1 -1
- package/dist/app/services/skills-catalog-service.js +21 -0
- package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
- package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
- package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
- package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
- package/dist/{settings/manager.js → app/stores/settings-store.js} +49 -7
- package/dist/app/types/worktree.js +1 -0
- package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
- package/dist/bot/callbacks/callback-router.js +113 -0
- package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
- package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
- package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
- package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
- package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
- package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
- package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
- package/dist/bot/callbacks/permission-callback-handler.js +114 -0
- package/dist/bot/callbacks/project-callback-handler.js +79 -0
- package/dist/bot/callbacks/question-callback-handler.js +159 -0
- package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
- package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
- package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
- package/dist/bot/callbacks/settings-callback-handler.js +105 -0
- package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
- package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
- package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
- package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
- package/dist/bot/commands/command-catalog-command.js +42 -0
- package/dist/bot/commands/definitions.js +1 -1
- package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
- package/dist/bot/commands/ls-command.js +48 -0
- package/dist/bot/commands/mcp-catalog-command.js +39 -0
- package/dist/bot/commands/messages-command.js +59 -0
- package/dist/bot/commands/{new.js → new-command.js} +12 -11
- package/dist/bot/commands/open-command.js +45 -0
- package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
- package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
- package/dist/bot/commands/projects-command.js +32 -0
- package/dist/bot/commands/rename-command.js +33 -0
- package/dist/bot/commands/sessions-command.js +42 -0
- package/dist/bot/commands/settings-command.js +10 -0
- package/dist/bot/commands/skills-catalog-command.js +42 -0
- package/dist/bot/commands/{start.js → start-command.js} +12 -12
- package/dist/bot/commands/{status.js → status-command.js} +15 -10
- package/dist/bot/commands/{task.js → task-command.js} +9 -81
- package/dist/bot/commands/tasklist-command.js +40 -0
- package/dist/bot/commands/worktree-command.js +47 -0
- package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
- package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
- package/dist/bot/handlers/photo-handler.js +44 -0
- package/dist/bot/handlers/prompt.js +17 -17
- package/dist/bot/handlers/text-message-handler.js +61 -0
- package/dist/bot/handlers/tts-response-handler.js +29 -0
- package/dist/bot/handlers/{voice.js → voice-handler.js} +6 -3
- package/dist/bot/index.js +27 -1179
- package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
- package/dist/bot/keyboards/keyboard-types.js +1 -0
- package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
- package/dist/bot/menus/agent-selection-menu.js +54 -0
- package/dist/bot/menus/command-catalog-menu.js +101 -0
- package/dist/bot/menus/context-control-menu.js +33 -0
- package/dist/bot/menus/file-browser-menu.js +265 -0
- package/dist/bot/{handlers → menus}/inline-menu.js +7 -34
- package/dist/bot/menus/mcp-catalog-menu.js +97 -0
- package/dist/bot/menus/message-history-menu.js +107 -0
- package/dist/bot/menus/model-selection-menu.js +66 -0
- package/dist/bot/menus/permission-menu.js +116 -0
- package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
- package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
- package/dist/bot/menus/rename-menu.js +6 -0
- package/dist/bot/menus/scheduled-task-menu.js +42 -0
- package/dist/bot/menus/session-selection-menu.js +115 -0
- package/dist/bot/menus/settings-menu.js +52 -0
- package/dist/bot/menus/skills-catalog-menu.js +99 -0
- package/dist/bot/menus/variant-selection-menu.js +67 -0
- package/dist/bot/menus/worktree-selection-menu.js +76 -0
- package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
- package/dist/bot/messages/busy-blocked-renderer.js +11 -0
- package/dist/bot/messages/external-user-input-notification.js +22 -0
- package/dist/bot/messages/scheduled-task-delivery.js +63 -0
- package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
- package/dist/bot/messages/summary-message-formatter.js +130 -0
- package/dist/bot/{utils → messages}/thinking-message.js +1 -4
- package/dist/bot/messages/thinking-rendering.js +66 -0
- package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
- package/dist/bot/middleware/interaction-guard.js +2 -2
- package/dist/bot/middleware/unknown-command.js +1 -1
- package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
- package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
- package/dist/bot/pinned/pinned-message-types.js +1 -0
- package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
- package/dist/bot/render/types.js +1 -0
- package/dist/bot/routers/command-router.js +73 -0
- package/dist/bot/routers/message-router.js +150 -0
- package/dist/bot/services/attach-presentation.js +40 -0
- package/dist/bot/services/event-subscription-service.js +1116 -0
- package/dist/bot/services/project-switch-presentation.js +26 -0
- package/dist/bot/streaming/compact-progress-streamer.js +162 -0
- package/dist/bot/{utils → streaming}/finalize-assistant-response.js +9 -3
- package/dist/bot/streaming/response-streamer.js +16 -1
- package/dist/cli.js +4 -4
- package/dist/config.js +9 -18
- package/dist/i18n/ar.js +31 -6
- package/dist/i18n/de.js +32 -7
- package/dist/i18n/en.js +32 -7
- package/dist/i18n/es.js +32 -7
- package/dist/i18n/fr.js +32 -7
- package/dist/i18n/ru.js +32 -7
- package/dist/i18n/zh.js +32 -7
- package/dist/index.js +1 -1
- package/dist/opencode/ready-refresh.js +2 -2
- package/dist/{service → runtime/service}/manager.js +2 -2
- package/dist/runtime/service/types.js +1 -0
- package/package.json +1 -1
- package/dist/bot/commands/ls.js +0 -445
- package/dist/bot/commands/open.js +0 -319
- package/dist/bot/commands/tts.js +0 -13
- package/dist/bot/commands/worktree.js +0 -201
- package/dist/bot/handlers/permission.js +0 -235
- package/dist/bot/utils/browser-roots.js +0 -140
- package/dist/bot/utils/file-tree.js +0 -92
- package/dist/bot/utils/send-tts-response.js +0 -37
- package/dist/bot/utils/switch-project.js +0 -45
- package/dist/bot/utils/telegram-file-url.js +0 -11
- package/dist/interaction/busy.js +0 -8
- package/dist/interaction/cleanup.js +0 -32
- /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
- /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
- /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
- /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
- /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
- /package/dist/{agent/types.js → app/types/agent.js} +0 -0
- /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
- /package/dist/{model/types.js → app/types/model.js} +0 -0
- /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
- /package/dist/{permission/types.js → app/types/project.js} +0 -0
- /package/dist/{pinned/types.js → app/types/question.js} +0 -0
- /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
- /package/dist/{question/types.js → app/types/session.js} +0 -0
- /package/dist/{service/types.js → app/types/settings.js} +0 -0
- /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
- /package/dist/{variant/types.js → app/types/variant.js} +0 -0
- /package/dist/bot/commands/{help.js → help-command.js} +0 -0
- /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
- /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
- /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
- /package/dist/{telegram → bot}/render/block-parser.js +0 -0
- /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
- /package/dist/{telegram → bot}/render/chunker.js +0 -0
- /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
- /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
- /package/dist/{telegram → bot}/render/pipeline.js +0 -0
- /package/dist/{telegram → bot}/render/validator.js +0 -0
- /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
- /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { createMainKeyboard } from "
|
|
2
|
-
import { getStoredAgent } from "
|
|
3
|
-
import { getStoredModel } from "
|
|
4
|
-
import { formatVariantForButton } from "
|
|
5
|
-
import { logger } from "
|
|
6
|
-
import { t } from "
|
|
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 "../../
|
|
3
|
-
import { formatModelForButton } from "../../
|
|
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, "&")
|
|
163
|
+
.replace(/</g, "<")
|
|
164
|
+
.replace(/>/g, ">")
|
|
165
|
+
.replace(/\"/g, """)
|
|
166
|
+
.replace(/'/g, "'");
|
|
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
|
|
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",
|
|
@@ -13,8 +13,9 @@ const INLINE_MENU_KINDS = [
|
|
|
13
13
|
"open",
|
|
14
14
|
"ls",
|
|
15
15
|
"worktree",
|
|
16
|
+
"settings",
|
|
16
17
|
];
|
|
17
|
-
function isInlineMenuKind(value) {
|
|
18
|
+
export function isInlineMenuKind(value) {
|
|
18
19
|
return INLINE_MENU_KINDS.includes(value);
|
|
19
20
|
}
|
|
20
21
|
function getCallbackMessageId(ctx) {
|
|
@@ -53,7 +54,8 @@ export function appendInlineMenuCancelButton(keyboard, menuKind) {
|
|
|
53
54
|
if (keyboard.inline_keyboard.length > 0) {
|
|
54
55
|
keyboard.row();
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
+
const buttonText = menuKind === "settings" ? t("inline.button.close") : t("inline.button.cancel");
|
|
58
|
+
keyboard.text(buttonText, getInlineCancelCallbackData(menuKind));
|
|
57
59
|
return keyboard;
|
|
58
60
|
}
|
|
59
61
|
export async function replyWithInlineMenu(ctx, options) {
|
|
@@ -98,32 +100,3 @@ export function clearActiveInlineMenu(reason) {
|
|
|
98
100
|
interactionManager.clear(reason);
|
|
99
101
|
}
|
|
100
102
|
}
|
|
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
|
+
}
|