@grinev/opencode-telegram-bot 0.3.0 → 0.5.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/README.md +10 -1
- package/dist/bot/commands/definitions.js +8 -4
- package/dist/bot/commands/help.js +7 -1
- package/dist/bot/commands/new.js +2 -0
- package/dist/bot/commands/projects.js +18 -9
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +13 -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 +50 -5
- package/dist/bot/handlers/question.js +97 -9
- package/dist/bot/handlers/variant.js +12 -1
- package/dist/bot/index.js +57 -9
- 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/i18n/en.js +31 -4
- package/dist/i18n/ru.js +31 -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 +6 -0
- package/dist/question/manager.js +33 -0
- package/dist/rename/manager.js +53 -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,20 @@ 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
|
+
}
|
|
38
53
|
/**
|
|
39
54
|
* Handle permission callback from inline buttons
|
|
40
55
|
*/
|
|
@@ -47,6 +62,12 @@ export async function handlePermissionCallback(ctx) {
|
|
|
47
62
|
}
|
|
48
63
|
logger.debug(`[PermissionHandler] Received callback: ${data}`);
|
|
49
64
|
if (!permissionManager.isActive()) {
|
|
65
|
+
clearPermissionInteraction("permission_inactive_callback");
|
|
66
|
+
await ctx.answerCallbackQuery({ text: t("permission.inactive_callback"), show_alert: true });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
70
|
+
if (!permissionManager.isActiveMessage(callbackMessageId)) {
|
|
50
71
|
await ctx.answerCallbackQuery({ text: t("permission.inactive_callback"), show_alert: true });
|
|
51
72
|
return true;
|
|
52
73
|
}
|
|
@@ -74,6 +95,8 @@ async function handlePermissionReply(ctx, reply) {
|
|
|
74
95
|
const chatId = ctx.chat?.id;
|
|
75
96
|
const directory = currentSession?.directory ?? currentProject?.worktree;
|
|
76
97
|
if (!requestID || !directory || !chatId) {
|
|
98
|
+
permissionManager.clear();
|
|
99
|
+
clearPermissionInteraction("permission_invalid_runtime_context");
|
|
77
100
|
await ctx.answerCallbackQuery({
|
|
78
101
|
text: t("permission.no_active_request_callback"),
|
|
79
102
|
show_alert: true,
|
|
@@ -112,13 +135,29 @@ async function handlePermissionReply(ctx, reply) {
|
|
|
112
135
|
},
|
|
113
136
|
});
|
|
114
137
|
permissionManager.clear();
|
|
138
|
+
clearPermissionInteraction("permission_replied");
|
|
115
139
|
}
|
|
116
140
|
/**
|
|
117
141
|
* Show permission request message with inline buttons
|
|
118
142
|
*/
|
|
119
143
|
export async function showPermissionRequest(bot, chatId, request) {
|
|
120
144
|
logger.debug(`[PermissionHandler] Showing permission request: ${request.permission}`);
|
|
145
|
+
const previousMessageId = permissionManager.getMessageId();
|
|
146
|
+
const hadActivePermission = permissionManager.isActive();
|
|
147
|
+
if (hadActivePermission && previousMessageId !== null) {
|
|
148
|
+
logger.debug(`[PermissionHandler] Replacing active permission request, deleting previous message: ${previousMessageId}`);
|
|
149
|
+
await bot.deleteMessage(chatId, previousMessageId).catch((err) => {
|
|
150
|
+
logger.debug("[PermissionHandler] Failed to delete previous permission message:", err);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
121
153
|
permissionManager.startPermission(request);
|
|
154
|
+
interactionManager.start({
|
|
155
|
+
kind: "permission",
|
|
156
|
+
expectedInput: "callback",
|
|
157
|
+
metadata: {
|
|
158
|
+
requestID: request.id,
|
|
159
|
+
},
|
|
160
|
+
});
|
|
122
161
|
const text = formatPermissionText(request);
|
|
123
162
|
const keyboard = buildPermissionKeyboard();
|
|
124
163
|
try {
|
|
@@ -128,9 +167,17 @@ export async function showPermissionRequest(bot, chatId, request) {
|
|
|
128
167
|
});
|
|
129
168
|
logger.debug(`[PermissionHandler] Message sent, messageId=${message.message_id}`);
|
|
130
169
|
permissionManager.setMessageId(message.message_id);
|
|
170
|
+
interactionManager.transition({
|
|
171
|
+
metadata: {
|
|
172
|
+
requestID: request.id,
|
|
173
|
+
messageId: message.message_id,
|
|
174
|
+
},
|
|
175
|
+
});
|
|
131
176
|
summaryAggregator.stopTypingIndicator();
|
|
132
177
|
}
|
|
133
178
|
catch (err) {
|
|
179
|
+
permissionManager.clear();
|
|
180
|
+
clearPermissionInteraction("permission_message_send_failed");
|
|
134
181
|
logger.error("[PermissionHandler] Failed to send permission message:", err);
|
|
135
182
|
throw err;
|
|
136
183
|
}
|
|
@@ -158,10 +205,8 @@ function formatPermissionText(request) {
|
|
|
158
205
|
*/
|
|
159
206
|
function buildPermissionKeyboard() {
|
|
160
207
|
const keyboard = new InlineKeyboard();
|
|
161
|
-
|
|
162
|
-
keyboard
|
|
163
|
-
|
|
164
|
-
.text(t("permission.button.always"), "permission:always")
|
|
165
|
-
.text(t("permission.button.reject"), "permission:reject");
|
|
208
|
+
keyboard.text(t("permission.button.allow"), "permission:once").row();
|
|
209
|
+
keyboard.text(t("permission.button.always"), "permission:always").row();
|
|
210
|
+
keyboard.text(t("permission.button.reject"), "permission:reject");
|
|
166
211
|
return keyboard;
|
|
167
212
|
}
|
|
@@ -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);
|
package/dist/bot/index.js
CHANGED
|
@@ -6,6 +6,8 @@ import { SocksProxyAgent } from "socks-proxy-agent";
|
|
|
6
6
|
import { HttpsProxyAgent } from "https-proxy-agent";
|
|
7
7
|
import { config } from "../config.js";
|
|
8
8
|
import { authMiddleware } from "./middleware/auth.js";
|
|
9
|
+
import { interactionGuardMiddleware } from "./middleware/interaction-guard.js";
|
|
10
|
+
import { unknownCommandMiddleware } from "./middleware/unknown-command.js";
|
|
9
11
|
import { BOT_COMMANDS } from "./commands/definitions.js";
|
|
10
12
|
import { startCommand } from "./commands/start.js";
|
|
11
13
|
import { helpCommand } from "./commands/help.js";
|
|
@@ -19,14 +21,17 @@ import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
|
19
21
|
import { opencodeStopCommand } from "./commands/opencode-stop.js";
|
|
20
22
|
import { handleAgentCommand } from "./commands/agent.js";
|
|
21
23
|
import { handleModelCommand } from "./commands/model.js";
|
|
24
|
+
import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
|
|
22
25
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
23
26
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
24
27
|
import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
|
|
25
28
|
import { handleModelSelect, showModelSelectionMenu } from "./handlers/model.js";
|
|
26
29
|
import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
|
|
27
|
-
import { handleContextButtonPress, handleCompactConfirm
|
|
30
|
+
import { handleContextButtonPress, handleCompactConfirm } from "./handlers/context.js";
|
|
31
|
+
import { handleInlineMenuCancel } from "./handlers/inline-menu.js";
|
|
28
32
|
import { questionManager } from "../question/manager.js";
|
|
29
|
-
import {
|
|
33
|
+
import { interactionManager } from "../interaction/manager.js";
|
|
34
|
+
import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
30
35
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
31
36
|
import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
|
|
32
37
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
@@ -167,6 +172,14 @@ async function ensureEventSubscription(directory) {
|
|
|
167
172
|
logger.error("Bot or chat ID not available for showing questions");
|
|
168
173
|
return;
|
|
169
174
|
}
|
|
175
|
+
if (questionManager.isActive()) {
|
|
176
|
+
logger.warn("[Bot] Replacing active poll with a new one");
|
|
177
|
+
const previousMessageIds = questionManager.getMessageIds();
|
|
178
|
+
for (const messageId of previousMessageIds) {
|
|
179
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch(() => { });
|
|
180
|
+
}
|
|
181
|
+
clearAllInteractionState("question_replaced_by_new_poll");
|
|
182
|
+
}
|
|
170
183
|
logger.info(`[Bot] Received ${questions.length} questions from agent, requestID=${requestID}`);
|
|
171
184
|
questionManager.startQuestions(questions, requestID);
|
|
172
185
|
await showCurrentQuestion(botInstance.api, chatIdInstance);
|
|
@@ -182,7 +195,7 @@ async function ensureEventSubscription(directory) {
|
|
|
182
195
|
});
|
|
183
196
|
}
|
|
184
197
|
}
|
|
185
|
-
|
|
198
|
+
clearAllInteractionState("question_error");
|
|
186
199
|
});
|
|
187
200
|
summaryAggregator.setOnPermission(async (request) => {
|
|
188
201
|
if (!botInstance || !chatIdInstance) {
|
|
@@ -297,8 +310,7 @@ async function isSessionBusy(sessionId, directory) {
|
|
|
297
310
|
async function resetMismatchedSessionContext() {
|
|
298
311
|
stopEventListening();
|
|
299
312
|
summaryAggregator.clear();
|
|
300
|
-
|
|
301
|
-
permissionManager.clear();
|
|
313
|
+
clearAllInteractionState("session_mismatch_reset");
|
|
302
314
|
clearSession();
|
|
303
315
|
keyboardManager.clearContext();
|
|
304
316
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -312,6 +324,7 @@ async function resetMismatchedSessionContext() {
|
|
|
312
324
|
}
|
|
313
325
|
}
|
|
314
326
|
export function createBot() {
|
|
327
|
+
clearAllInteractionState("bot_startup");
|
|
315
328
|
const botOptions = {};
|
|
316
329
|
if (config.telegram.proxyUrl) {
|
|
317
330
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -364,6 +377,16 @@ export function createBot() {
|
|
|
364
377
|
});
|
|
365
378
|
bot.use(authMiddleware);
|
|
366
379
|
bot.use(ensureCommandsInitialized);
|
|
380
|
+
bot.use(interactionGuardMiddleware);
|
|
381
|
+
const blockMenuWhileInteractionActive = async (ctx) => {
|
|
382
|
+
const activeInteraction = interactionManager.getSnapshot();
|
|
383
|
+
if (!activeInteraction) {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
logger.debug(`[Bot] Blocking menu open while interaction active: kind=${activeInteraction.kind}, expectedInput=${activeInteraction.expectedInput}`);
|
|
387
|
+
await ctx.reply(t("interaction.blocked.finish_current"));
|
|
388
|
+
return true;
|
|
389
|
+
};
|
|
367
390
|
bot.command("start", startCommand);
|
|
368
391
|
bot.command("help", helpCommand);
|
|
369
392
|
bot.command("status", statusCommand);
|
|
@@ -375,10 +398,13 @@ export function createBot() {
|
|
|
375
398
|
bot.command("agent", handleAgentCommand);
|
|
376
399
|
bot.command("model", handleModelCommand);
|
|
377
400
|
bot.command("stop", stopCommand);
|
|
401
|
+
bot.command("rename", renameCommand);
|
|
402
|
+
bot.on("message:text", unknownCommandMiddleware);
|
|
378
403
|
bot.on("callback_query:data", async (ctx) => {
|
|
379
404
|
logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
|
|
380
405
|
logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
|
|
381
406
|
try {
|
|
407
|
+
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
382
408
|
const handledSession = await handleSessionSelect(ctx);
|
|
383
409
|
const handledProject = await handleProjectSelect(ctx);
|
|
384
410
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
@@ -387,9 +413,10 @@ export function createBot() {
|
|
|
387
413
|
const handledModel = await handleModelSelect(ctx);
|
|
388
414
|
const handledVariant = await handleVariantSelect(ctx);
|
|
389
415
|
const handledCompactConfirm = await handleCompactConfirm(ctx);
|
|
390
|
-
const
|
|
391
|
-
logger.debug(`[Bot] Callback handled: session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant},
|
|
392
|
-
if (!
|
|
416
|
+
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
417
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, rename=${handledRenameCancel}`);
|
|
418
|
+
if (!handledInlineCancel &&
|
|
419
|
+
!handledSession &&
|
|
393
420
|
!handledProject &&
|
|
394
421
|
!handledQuestion &&
|
|
395
422
|
!handledPermission &&
|
|
@@ -397,13 +424,14 @@ export function createBot() {
|
|
|
397
424
|
!handledModel &&
|
|
398
425
|
!handledVariant &&
|
|
399
426
|
!handledCompactConfirm &&
|
|
400
|
-
!
|
|
427
|
+
!handledRenameCancel) {
|
|
401
428
|
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
402
429
|
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
403
430
|
}
|
|
404
431
|
}
|
|
405
432
|
catch (err) {
|
|
406
433
|
logger.error("[Bot] Error handling callback:", err);
|
|
434
|
+
clearAllInteractionState("callback_handler_error");
|
|
407
435
|
await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
|
|
408
436
|
}
|
|
409
437
|
});
|
|
@@ -411,6 +439,9 @@ export function createBot() {
|
|
|
411
439
|
bot.hears(/^(📋|🛠️|💬|🔍|📝|📄|📦|🤖) \w+ Mode$/, async (ctx) => {
|
|
412
440
|
logger.debug(`[Bot] Agent mode button pressed: ${ctx.message?.text}`);
|
|
413
441
|
try {
|
|
442
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
414
445
|
await showAgentSelectionMenu(ctx);
|
|
415
446
|
}
|
|
416
447
|
catch (err) {
|
|
@@ -423,6 +454,9 @@ export function createBot() {
|
|
|
423
454
|
bot.hears(MODEL_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
424
455
|
logger.debug(`[Bot] Model button pressed: ${ctx.message?.text}`);
|
|
425
456
|
try {
|
|
457
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
426
460
|
await showModelSelectionMenu(ctx);
|
|
427
461
|
}
|
|
428
462
|
catch (err) {
|
|
@@ -434,6 +468,9 @@ export function createBot() {
|
|
|
434
468
|
bot.hears(/^📊(?:\s|$)/, async (ctx) => {
|
|
435
469
|
logger.debug(`[Bot] Context button pressed: ${ctx.message?.text}`);
|
|
436
470
|
try {
|
|
471
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
437
474
|
await handleContextButtonPress(ctx);
|
|
438
475
|
}
|
|
439
476
|
catch (err) {
|
|
@@ -446,6 +483,9 @@ export function createBot() {
|
|
|
446
483
|
bot.hears(VARIANT_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
447
484
|
logger.debug(`[Bot] Variant button pressed: ${ctx.message?.text}`);
|
|
448
485
|
try {
|
|
486
|
+
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
449
489
|
await showVariantSelectionMenu(ctx);
|
|
450
490
|
}
|
|
451
491
|
catch (err) {
|
|
@@ -496,6 +536,10 @@ export function createBot() {
|
|
|
496
536
|
await handleQuestionTextAnswer(ctx);
|
|
497
537
|
return;
|
|
498
538
|
}
|
|
539
|
+
const handledRename = await handleRenameTextAnswer(ctx);
|
|
540
|
+
if (handledRename) {
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
499
543
|
const currentProject = getCurrentProject();
|
|
500
544
|
if (!currentProject) {
|
|
501
545
|
await ctx.reply(t("bot.project_not_selected"));
|
|
@@ -620,12 +664,16 @@ export function createBot() {
|
|
|
620
664
|
}
|
|
621
665
|
catch (err) {
|
|
622
666
|
logger.error("Error in prompt handler:", err);
|
|
667
|
+
if (interactionManager.getSnapshot()) {
|
|
668
|
+
clearAllInteractionState("message_handler_error");
|
|
669
|
+
}
|
|
623
670
|
await ctx.reply(t("error.generic"));
|
|
624
671
|
}
|
|
625
672
|
logger.debug("[Bot] message:text handler completed (prompt sent in background)");
|
|
626
673
|
});
|
|
627
674
|
bot.catch((err) => {
|
|
628
675
|
logger.error("[Bot] Unhandled error in bot:", err);
|
|
676
|
+
clearAllInteractionState("bot_unhandled_error");
|
|
629
677
|
if (err.ctx) {
|
|
630
678
|
logger.error("[Bot] Error context - update type:", err.ctx.update ? Object.keys(err.ctx.update) : "unknown");
|
|
631
679
|
}
|