@grinev/opencode-telegram-bot 0.4.0 → 0.6.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 +11 -0
- package/README.md +26 -15
- package/dist/bot/commands/definitions.js +7 -4
- package/dist/bot/commands/help.js +7 -1
- package/dist/bot/commands/new.js +4 -0
- package/dist/bot/commands/projects.js +18 -9
- package/dist/bot/commands/rename.js +49 -1
- package/dist/bot/commands/sessions.js +15 -2
- package/dist/bot/commands/stop.js +3 -5
- package/dist/bot/handlers/agent.js +12 -1
- package/dist/bot/handlers/context.js +15 -25
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +15 -2
- package/dist/bot/handlers/permission.js +81 -12
- package/dist/bot/handlers/question.js +97 -9
- package/dist/bot/handlers/variant.js +12 -1
- package/dist/bot/index.js +92 -16
- package/dist/bot/middleware/interaction-guard.js +80 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/config.js +31 -0
- package/dist/i18n/en.js +23 -4
- package/dist/i18n/ru.js +23 -4
- package/dist/interaction/cleanup.js +24 -0
- package/dist/interaction/guard.js +87 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/permission/manager.js +60 -38
- package/dist/pinned/manager.js +7 -5
- package/dist/question/manager.js +33 -0
- package/dist/rename/manager.js +3 -0
- package/dist/settings/manager.js +6 -1
- package/dist/summary/aggregator.js +87 -6
- package/dist/summary/formatter.js +91 -15
- package/dist/summary/tool-message-batcher.js +182 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import { createMainKeyboard } from "../utils/keyboard.js";
|
|
|
7
7
|
import { getStoredAgent } from "../../agent/manager.js";
|
|
8
8
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
9
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
|
+
import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
|
|
10
11
|
import { t } from "../../i18n/index.js";
|
|
11
12
|
/**
|
|
12
13
|
* Handle model selection callback
|
|
@@ -18,6 +19,10 @@ export async function handleModelSelect(ctx) {
|
|
|
18
19
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith("model:")) {
|
|
19
20
|
return false;
|
|
20
21
|
}
|
|
22
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "model");
|
|
23
|
+
if (!isActiveMenu) {
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
21
26
|
logger.debug(`[ModelHandler] Received callback: ${callbackQuery.data}`);
|
|
22
27
|
try {
|
|
23
28
|
if (ctx.chat) {
|
|
@@ -27,7 +32,9 @@ export async function handleModelSelect(ctx) {
|
|
|
27
32
|
const parts = callbackQuery.data.split(":");
|
|
28
33
|
if (parts.length < 3) {
|
|
29
34
|
logger.error(`[ModelHandler] Invalid callback data format: ${callbackQuery.data}`);
|
|
30
|
-
|
|
35
|
+
clearActiveInlineMenu("model_select_invalid_callback");
|
|
36
|
+
await ctx.answerCallbackQuery({ text: t("model.change_error_callback") }).catch(() => { });
|
|
37
|
+
return true;
|
|
31
38
|
}
|
|
32
39
|
const providerID = parts[1];
|
|
33
40
|
const modelID = parts.slice(2).join(":"); // Handle model IDs that may contain ":"
|
|
@@ -54,6 +61,7 @@ export async function handleModelSelect(ctx) {
|
|
|
54
61
|
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
55
62
|
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
56
63
|
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
64
|
+
clearActiveInlineMenu("model_selected");
|
|
57
65
|
// Send confirmation message with updated keyboard
|
|
58
66
|
await ctx.answerCallbackQuery({ text: t("model.changed_callback", { name: displayName }) });
|
|
59
67
|
await ctx.reply(t("model.changed_message", { name: displayName }), {
|
|
@@ -64,6 +72,7 @@ export async function handleModelSelect(ctx) {
|
|
|
64
72
|
return true;
|
|
65
73
|
}
|
|
66
74
|
catch (err) {
|
|
75
|
+
clearActiveInlineMenu("model_select_error");
|
|
67
76
|
logger.error("[ModelHandler] Error handling model select:", err);
|
|
68
77
|
await ctx.answerCallbackQuery({ text: t("model.change_error_callback") }).catch(() => { });
|
|
69
78
|
return false;
|
|
@@ -107,7 +116,11 @@ export async function showModelSelectionMenu(ctx) {
|
|
|
107
116
|
}
|
|
108
117
|
const displayName = formatModelForDisplay(currentModel.providerID, currentModel.modelID);
|
|
109
118
|
const text = t("model.menu.current", { name: displayName });
|
|
110
|
-
await ctx
|
|
119
|
+
await replyWithInlineMenu(ctx, {
|
|
120
|
+
menuKind: "model",
|
|
121
|
+
text,
|
|
122
|
+
keyboard,
|
|
123
|
+
});
|
|
111
124
|
}
|
|
112
125
|
catch (err) {
|
|
113
126
|
logger.error("[ModelHandler] Error showing model menu:", err);
|
|
@@ -4,6 +4,7 @@ import { opencodeClient } from "../../opencode/client.js";
|
|
|
4
4
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
5
|
import { getCurrentSession } from "../../session/manager.js";
|
|
6
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
7
8
|
import { logger } from "../../utils/logger.js";
|
|
8
9
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
9
10
|
import { t } from "../../i18n/index.js";
|
|
@@ -35,6 +36,47 @@ const PERMISSION_EMOJIS = {
|
|
|
35
36
|
task: "⚙️",
|
|
36
37
|
lsp: "🔧",
|
|
37
38
|
};
|
|
39
|
+
function getCallbackMessageId(ctx) {
|
|
40
|
+
const message = ctx.callbackQuery?.message;
|
|
41
|
+
if (!message || !("message_id" in message)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const messageId = message.message_id;
|
|
45
|
+
return typeof messageId === "number" ? messageId : null;
|
|
46
|
+
}
|
|
47
|
+
function clearPermissionInteraction(reason) {
|
|
48
|
+
const state = interactionManager.getSnapshot();
|
|
49
|
+
if (state?.kind === "permission") {
|
|
50
|
+
interactionManager.clear(reason);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function syncPermissionInteractionState(metadata = {}) {
|
|
54
|
+
const pendingCount = permissionManager.getPendingCount();
|
|
55
|
+
if (pendingCount === 0) {
|
|
56
|
+
clearPermissionInteraction("permission_no_pending_requests");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const nextMetadata = {
|
|
60
|
+
pendingCount,
|
|
61
|
+
...metadata,
|
|
62
|
+
};
|
|
63
|
+
const state = interactionManager.getSnapshot();
|
|
64
|
+
if (state?.kind === "permission") {
|
|
65
|
+
interactionManager.transition({
|
|
66
|
+
expectedInput: "callback",
|
|
67
|
+
metadata: nextMetadata,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
interactionManager.start({
|
|
72
|
+
kind: "permission",
|
|
73
|
+
expectedInput: "callback",
|
|
74
|
+
metadata: nextMetadata,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function isPermissionReply(value) {
|
|
78
|
+
return value === "once" || value === "always" || value === "reject";
|
|
79
|
+
}
|
|
38
80
|
/**
|
|
39
81
|
* Handle permission callback from inline buttons
|
|
40
82
|
*/
|
|
@@ -47,13 +89,31 @@ export async function handlePermissionCallback(ctx) {
|
|
|
47
89
|
}
|
|
48
90
|
logger.debug(`[PermissionHandler] Received callback: ${data}`);
|
|
49
91
|
if (!permissionManager.isActive()) {
|
|
92
|
+
clearPermissionInteraction("permission_inactive_callback");
|
|
93
|
+
await ctx.answerCallbackQuery({ text: t("permission.inactive_callback"), show_alert: true });
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
97
|
+
if (!permissionManager.isActiveMessage(callbackMessageId)) {
|
|
98
|
+
await ctx.answerCallbackQuery({ text: t("permission.inactive_callback"), show_alert: true });
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
const requestID = permissionManager.getRequestID(callbackMessageId);
|
|
102
|
+
if (!requestID) {
|
|
50
103
|
await ctx.answerCallbackQuery({ text: t("permission.inactive_callback"), show_alert: true });
|
|
51
104
|
return true;
|
|
52
105
|
}
|
|
53
106
|
const parts = data.split(":");
|
|
54
107
|
const action = parts[1];
|
|
108
|
+
if (!isPermissionReply(action)) {
|
|
109
|
+
await ctx.answerCallbackQuery({
|
|
110
|
+
text: t("permission.processing_error_callback"),
|
|
111
|
+
show_alert: true,
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
55
115
|
try {
|
|
56
|
-
await handlePermissionReply(ctx, action);
|
|
116
|
+
await handlePermissionReply(ctx, action, requestID, callbackMessageId);
|
|
57
117
|
}
|
|
58
118
|
catch (err) {
|
|
59
119
|
logger.error("[PermissionHandler] Error handling callback:", err);
|
|
@@ -67,13 +127,14 @@ export async function handlePermissionCallback(ctx) {
|
|
|
67
127
|
/**
|
|
68
128
|
* Handle permission reply (once/always/reject)
|
|
69
129
|
*/
|
|
70
|
-
async function handlePermissionReply(ctx, reply) {
|
|
71
|
-
const requestID = permissionManager.getRequestID();
|
|
130
|
+
async function handlePermissionReply(ctx, reply, requestID, callbackMessageId) {
|
|
72
131
|
const currentProject = getCurrentProject();
|
|
73
132
|
const currentSession = getCurrentSession();
|
|
74
133
|
const chatId = ctx.chat?.id;
|
|
75
134
|
const directory = currentSession?.directory ?? currentProject?.worktree;
|
|
76
|
-
if (!
|
|
135
|
+
if (!directory || !chatId) {
|
|
136
|
+
permissionManager.clear();
|
|
137
|
+
clearPermissionInteraction("permission_invalid_runtime_context");
|
|
77
138
|
await ctx.answerCallbackQuery({
|
|
78
139
|
text: t("permission.no_active_request_callback"),
|
|
79
140
|
show_alert: true,
|
|
@@ -111,14 +172,20 @@ async function handlePermissionReply(ctx, reply) {
|
|
|
111
172
|
logger.info("[PermissionHandler] Permission reply sent successfully");
|
|
112
173
|
},
|
|
113
174
|
});
|
|
114
|
-
permissionManager.
|
|
175
|
+
permissionManager.removeByMessageId(callbackMessageId);
|
|
176
|
+
if (!permissionManager.isActive()) {
|
|
177
|
+
clearPermissionInteraction("permission_replied");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
syncPermissionInteractionState({
|
|
181
|
+
lastRepliedRequestID: requestID,
|
|
182
|
+
});
|
|
115
183
|
}
|
|
116
184
|
/**
|
|
117
185
|
* Show permission request message with inline buttons
|
|
118
186
|
*/
|
|
119
187
|
export async function showPermissionRequest(bot, chatId, request) {
|
|
120
188
|
logger.debug(`[PermissionHandler] Showing permission request: ${request.permission}`);
|
|
121
|
-
permissionManager.startPermission(request);
|
|
122
189
|
const text = formatPermissionText(request);
|
|
123
190
|
const keyboard = buildPermissionKeyboard();
|
|
124
191
|
try {
|
|
@@ -127,7 +194,11 @@ export async function showPermissionRequest(bot, chatId, request) {
|
|
|
127
194
|
parse_mode: "Markdown",
|
|
128
195
|
});
|
|
129
196
|
logger.debug(`[PermissionHandler] Message sent, messageId=${message.message_id}`);
|
|
130
|
-
permissionManager.
|
|
197
|
+
permissionManager.startPermission(request, message.message_id);
|
|
198
|
+
syncPermissionInteractionState({
|
|
199
|
+
requestID: request.id,
|
|
200
|
+
messageId: message.message_id,
|
|
201
|
+
});
|
|
131
202
|
summaryAggregator.stopTypingIndicator();
|
|
132
203
|
}
|
|
133
204
|
catch (err) {
|
|
@@ -158,10 +229,8 @@ function formatPermissionText(request) {
|
|
|
158
229
|
*/
|
|
159
230
|
function buildPermissionKeyboard() {
|
|
160
231
|
const keyboard = new InlineKeyboard();
|
|
161
|
-
|
|
162
|
-
keyboard
|
|
163
|
-
|
|
164
|
-
.text(t("permission.button.always"), "permission:always")
|
|
165
|
-
.text(t("permission.button.reject"), "permission:reject");
|
|
232
|
+
keyboard.text(t("permission.button.allow"), "permission:once").row();
|
|
233
|
+
keyboard.text(t("permission.button.always"), "permission:always").row();
|
|
234
|
+
keyboard.text(t("permission.button.reject"), "permission:reject");
|
|
166
235
|
return keyboard;
|
|
167
236
|
}
|
|
@@ -4,10 +4,51 @@ import { opencodeClient } from "../../opencode/client.js";
|
|
|
4
4
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
5
|
import { getCurrentSession } from "../../session/manager.js";
|
|
6
6
|
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
7
8
|
import { logger } from "../../utils/logger.js";
|
|
8
9
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
9
10
|
import { t } from "../../i18n/index.js";
|
|
10
11
|
const MAX_BUTTON_LENGTH = 60;
|
|
12
|
+
function getCallbackMessageId(ctx) {
|
|
13
|
+
const message = ctx.callbackQuery?.message;
|
|
14
|
+
if (!message || !("message_id" in message)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const messageId = message.message_id;
|
|
18
|
+
return typeof messageId === "number" ? messageId : null;
|
|
19
|
+
}
|
|
20
|
+
function clearQuestionInteraction(reason) {
|
|
21
|
+
const state = interactionManager.getSnapshot();
|
|
22
|
+
if (state?.kind === "question") {
|
|
23
|
+
interactionManager.clear(reason);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function syncQuestionInteractionState(expectedInput, questionIndex, messageId) {
|
|
27
|
+
const metadata = {
|
|
28
|
+
questionIndex,
|
|
29
|
+
inputMode: expectedInput === "mixed" ? "custom" : "options",
|
|
30
|
+
};
|
|
31
|
+
const requestID = questionManager.getRequestID();
|
|
32
|
+
if (requestID) {
|
|
33
|
+
metadata.requestID = requestID;
|
|
34
|
+
}
|
|
35
|
+
if (messageId !== null) {
|
|
36
|
+
metadata.messageId = messageId;
|
|
37
|
+
}
|
|
38
|
+
const state = interactionManager.getSnapshot();
|
|
39
|
+
if (state?.kind === "question") {
|
|
40
|
+
interactionManager.transition({
|
|
41
|
+
expectedInput,
|
|
42
|
+
metadata,
|
|
43
|
+
});
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
interactionManager.start({
|
|
47
|
+
kind: "question",
|
|
48
|
+
expectedInput,
|
|
49
|
+
metadata,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
11
52
|
export async function handleQuestionCallback(ctx) {
|
|
12
53
|
const data = ctx.callbackQuery?.data;
|
|
13
54
|
if (!data)
|
|
@@ -17,16 +58,36 @@ export async function handleQuestionCallback(ctx) {
|
|
|
17
58
|
}
|
|
18
59
|
logger.debug(`[QuestionHandler] Received callback: ${data}`);
|
|
19
60
|
if (!questionManager.isActive()) {
|
|
61
|
+
clearQuestionInteraction("question_inactive_callback");
|
|
62
|
+
await ctx.answerCallbackQuery({ text: t("question.inactive_callback"), show_alert: true });
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
66
|
+
if (!questionManager.isActiveMessage(callbackMessageId)) {
|
|
20
67
|
await ctx.answerCallbackQuery({ text: t("question.inactive_callback"), show_alert: true });
|
|
21
68
|
return true;
|
|
22
69
|
}
|
|
23
70
|
const parts = data.split(":");
|
|
24
71
|
const action = parts[1];
|
|
25
72
|
const questionIndex = parseInt(parts[2], 10);
|
|
73
|
+
if (Number.isNaN(questionIndex) || questionIndex !== questionManager.getCurrentIndex()) {
|
|
74
|
+
await ctx.answerCallbackQuery({ text: t("question.inactive_callback"), show_alert: true });
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
26
77
|
try {
|
|
27
78
|
switch (action) {
|
|
28
79
|
case "select":
|
|
29
|
-
|
|
80
|
+
{
|
|
81
|
+
const optionIndex = parseInt(parts[3], 10);
|
|
82
|
+
if (Number.isNaN(optionIndex)) {
|
|
83
|
+
await ctx.answerCallbackQuery({
|
|
84
|
+
text: t("question.processing_error_callback"),
|
|
85
|
+
show_alert: true,
|
|
86
|
+
});
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
await handleSelectOption(ctx, questionIndex, optionIndex);
|
|
90
|
+
}
|
|
30
91
|
break;
|
|
31
92
|
case "submit":
|
|
32
93
|
await handleSubmitAnswer(ctx, questionIndex);
|
|
@@ -37,6 +98,12 @@ export async function handleQuestionCallback(ctx) {
|
|
|
37
98
|
case "cancel":
|
|
38
99
|
await handleCancelPoll(ctx);
|
|
39
100
|
break;
|
|
101
|
+
default:
|
|
102
|
+
await ctx.answerCallbackQuery({
|
|
103
|
+
text: t("question.processing_error_callback"),
|
|
104
|
+
show_alert: true,
|
|
105
|
+
});
|
|
106
|
+
break;
|
|
40
107
|
}
|
|
41
108
|
}
|
|
42
109
|
catch (err) {
|
|
@@ -55,6 +122,10 @@ async function handleSelectOption(ctx, questionIndex, optionIndex) {
|
|
|
55
122
|
logger.debug("[QuestionHandler] No current question");
|
|
56
123
|
return;
|
|
57
124
|
}
|
|
125
|
+
if (questionManager.isWaitingForCustomInput(questionIndex)) {
|
|
126
|
+
questionManager.clearCustomInput();
|
|
127
|
+
syncQuestionInteractionState("callback", questionIndex, questionManager.getActiveMessageId());
|
|
128
|
+
}
|
|
58
129
|
questionManager.selectOption(questionIndex, optionIndex);
|
|
59
130
|
if (question.multiple) {
|
|
60
131
|
logger.debug("[QuestionHandler] Multiple choice mode, updating message");
|
|
@@ -74,6 +145,10 @@ async function handleSelectOption(ctx, questionIndex, optionIndex) {
|
|
|
74
145
|
}
|
|
75
146
|
}
|
|
76
147
|
async function handleSubmitAnswer(ctx, questionIndex) {
|
|
148
|
+
if (questionManager.isWaitingForCustomInput(questionIndex)) {
|
|
149
|
+
questionManager.clearCustomInput();
|
|
150
|
+
syncQuestionInteractionState("callback", questionIndex, questionManager.getActiveMessageId());
|
|
151
|
+
}
|
|
77
152
|
const answer = questionManager.getSelectedAnswer(questionIndex);
|
|
78
153
|
if (!answer) {
|
|
79
154
|
await ctx.answerCallbackQuery({
|
|
@@ -90,7 +165,9 @@ async function handleSubmitAnswer(ctx, questionIndex) {
|
|
|
90
165
|
// All answers will be sent together after the user answers all questions
|
|
91
166
|
await showNextQuestion(ctx);
|
|
92
167
|
}
|
|
93
|
-
async function handleCustomAnswer(ctx,
|
|
168
|
+
async function handleCustomAnswer(ctx, questionIndex) {
|
|
169
|
+
questionManager.startCustomInput(questionIndex);
|
|
170
|
+
syncQuestionInteractionState("mixed", questionIndex, questionManager.getActiveMessageId());
|
|
94
171
|
await ctx.answerCallbackQuery({
|
|
95
172
|
text: t("question.enter_custom_callback"),
|
|
96
173
|
show_alert: true,
|
|
@@ -98,8 +175,10 @@ async function handleCustomAnswer(ctx, _questionIndex) {
|
|
|
98
175
|
}
|
|
99
176
|
async function handleCancelPoll(ctx) {
|
|
100
177
|
questionManager.cancel();
|
|
101
|
-
|
|
178
|
+
clearQuestionInteraction("question_cancelled");
|
|
179
|
+
await ctx.editMessageText(t("question.cancelled")).catch(() => { });
|
|
102
180
|
await ctx.answerCallbackQuery();
|
|
181
|
+
questionManager.clear();
|
|
103
182
|
}
|
|
104
183
|
async function updateQuestionMessage(ctx) {
|
|
105
184
|
const question = questionManager.getCurrentQuestion();
|
|
@@ -137,9 +216,13 @@ export async function showCurrentQuestion(bot, chatId) {
|
|
|
137
216
|
});
|
|
138
217
|
logger.debug(`[QuestionHandler] Message sent, messageId=${message.message_id}`);
|
|
139
218
|
questionManager.addMessageId(message.message_id);
|
|
219
|
+
questionManager.setActiveMessageId(message.message_id);
|
|
220
|
+
syncQuestionInteractionState("callback", questionManager.getCurrentIndex(), questionManager.getActiveMessageId());
|
|
140
221
|
summaryAggregator.stopTypingIndicator();
|
|
141
222
|
}
|
|
142
223
|
catch (err) {
|
|
224
|
+
questionManager.clear();
|
|
225
|
+
clearQuestionInteraction("question_message_send_failed");
|
|
143
226
|
logger.error("[QuestionHandler] Failed to send question message:", err);
|
|
144
227
|
throw err;
|
|
145
228
|
}
|
|
@@ -149,17 +232,21 @@ export async function handleQuestionTextAnswer(ctx) {
|
|
|
149
232
|
if (!text)
|
|
150
233
|
return;
|
|
151
234
|
const currentIndex = questionManager.getCurrentIndex();
|
|
235
|
+
if (!questionManager.isWaitingForCustomInput(currentIndex)) {
|
|
236
|
+
await ctx.reply(t("question.use_custom_button_first"));
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
152
239
|
if (questionManager.hasCustomAnswer(currentIndex)) {
|
|
153
240
|
await ctx.reply(t("question.answer_already_received"));
|
|
154
241
|
return;
|
|
155
242
|
}
|
|
156
243
|
logger.debug(`[QuestionHandler] Custom text answer for question ${currentIndex}: ${text}`);
|
|
157
244
|
questionManager.setCustomAnswer(currentIndex, text);
|
|
245
|
+
questionManager.clearCustomInput();
|
|
158
246
|
// Delete the previous question message
|
|
159
|
-
const
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
await ctx.api.deleteMessage(ctx.chat.id, lastMessageId).catch(() => { });
|
|
247
|
+
const activeMessageId = questionManager.getActiveMessageId();
|
|
248
|
+
if (activeMessageId !== null && ctx.chat) {
|
|
249
|
+
await ctx.api.deleteMessage(ctx.chat.id, activeMessageId).catch(() => { });
|
|
163
250
|
}
|
|
164
251
|
// DO NOT send the answer immediately - move to the next question
|
|
165
252
|
// All answers will be sent together after the user answers all questions
|
|
@@ -190,6 +277,7 @@ async function showPollSummary(bot, chatId) {
|
|
|
190
277
|
const summary = formatAnswersSummary(answers);
|
|
191
278
|
await bot.sendMessage(chatId, summary);
|
|
192
279
|
}
|
|
280
|
+
clearQuestionInteraction("question_completed");
|
|
193
281
|
questionManager.clear();
|
|
194
282
|
logger.debug("[QuestionHandler] Poll completed and cleared");
|
|
195
283
|
}
|
|
@@ -271,10 +359,10 @@ function buildQuestionKeyboard(question, selectedOptions) {
|
|
|
271
359
|
keyboard.text(buttonText, callbackData).row();
|
|
272
360
|
});
|
|
273
361
|
if (question.multiple) {
|
|
274
|
-
keyboard.text(t("question.button.submit"), `question:submit:${questionIndex}`);
|
|
362
|
+
keyboard.text(t("question.button.submit"), `question:submit:${questionIndex}`).row();
|
|
275
363
|
logger.debug(`[QuestionHandler] Added submit button`);
|
|
276
364
|
}
|
|
277
|
-
keyboard.text(t("question.button.custom"), `question:custom:${questionIndex}`);
|
|
365
|
+
keyboard.text(t("question.button.custom"), `question:custom:${questionIndex}`).row();
|
|
278
366
|
logger.debug(`[QuestionHandler] Added custom answer button`);
|
|
279
367
|
keyboard.text(t("question.button.cancel"), `question:cancel:${questionIndex}`);
|
|
280
368
|
logger.debug(`[QuestionHandler] Added cancel button`);
|
|
@@ -6,6 +6,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
6
6
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
7
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
8
|
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
9
|
+
import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
|
|
9
10
|
import { t } from "../../i18n/index.js";
|
|
10
11
|
/**
|
|
11
12
|
* Handle variant selection callback
|
|
@@ -17,6 +18,10 @@ export async function handleVariantSelect(ctx) {
|
|
|
17
18
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith("variant:")) {
|
|
18
19
|
return false;
|
|
19
20
|
}
|
|
21
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "variant");
|
|
22
|
+
if (!isActiveMenu) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
20
25
|
logger.debug(`[VariantHandler] Received callback: ${callbackQuery.data}`);
|
|
21
26
|
try {
|
|
22
27
|
if (ctx.chat) {
|
|
@@ -54,6 +59,7 @@ export async function handleVariantSelect(ctx) {
|
|
|
54
59
|
const keyboard = createMainKeyboard(currentAgent, updatedModel, contextInfo ?? undefined, variantName);
|
|
55
60
|
// Send confirmation message with updated keyboard
|
|
56
61
|
const displayName = formatVariantForDisplay(variantId);
|
|
62
|
+
clearActiveInlineMenu("variant_selected");
|
|
57
63
|
await ctx.answerCallbackQuery({ text: t("variant.changed_callback", { name: displayName }) });
|
|
58
64
|
await ctx.reply(t("variant.changed_message", { name: displayName }), {
|
|
59
65
|
reply_markup: keyboard,
|
|
@@ -63,6 +69,7 @@ export async function handleVariantSelect(ctx) {
|
|
|
63
69
|
return true;
|
|
64
70
|
}
|
|
65
71
|
catch (err) {
|
|
72
|
+
clearActiveInlineMenu("variant_select_error");
|
|
66
73
|
logger.error("[VariantHandler] Error handling variant select:", err);
|
|
67
74
|
await ctx.answerCallbackQuery({ text: t("variant.change_error_callback") }).catch(() => { });
|
|
68
75
|
return false;
|
|
@@ -118,7 +125,11 @@ export async function showVariantSelectionMenu(ctx) {
|
|
|
118
125
|
}
|
|
119
126
|
const displayName = formatVariantForDisplay(currentVariant);
|
|
120
127
|
const text = t("variant.menu.current", { name: displayName });
|
|
121
|
-
await ctx
|
|
128
|
+
await replyWithInlineMenu(ctx, {
|
|
129
|
+
menuKind: "variant",
|
|
130
|
+
text,
|
|
131
|
+
keyboard,
|
|
132
|
+
});
|
|
122
133
|
}
|
|
123
134
|
catch (err) {
|
|
124
135
|
logger.error("[VariantHandler] Error showing variant menu:", err);
|