@grinev/opencode-telegram-bot 0.20.6 → 0.21.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 +5 -2
- package/README.md +10 -0
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/messages.js +537 -0
- package/dist/bot/handlers/model.js +205 -42
- package/dist/bot/index.js +16 -3
- package/dist/bot/streaming/response-streamer.js +8 -0
- package/dist/bot/utils/assistant-run-footer.js +15 -4
- package/dist/bot/utils/busy-reconciliation.js +8 -0
- package/dist/config.js +2 -1
- package/dist/i18n/de.js +27 -0
- package/dist/i18n/en.js +27 -0
- package/dist/i18n/es.js +27 -0
- package/dist/i18n/fr.js +27 -0
- package/dist/i18n/ru.js +27 -0
- package/dist/i18n/zh.js +27 -0
- package/dist/model/manager.js +38 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import { selectModel, fetchCurrentModel, getModelSelectionLists } from "../../model/manager.js";
|
|
2
|
+
import { selectModel, fetchCurrentModel, getModelSelectionLists, searchModels, } from "../../model/manager.js";
|
|
3
3
|
import { formatModelForDisplay } from "../../model/types.js";
|
|
4
4
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
5
|
import { logger } from "../../utils/logger.js";
|
|
@@ -8,7 +8,11 @@ import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
|
8
8
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
9
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
10
|
import { clearActiveInlineMenu, ensureActiveInlineMenu, replyWithInlineMenu, } from "./inline-menu.js";
|
|
11
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
11
12
|
import { t } from "../../i18n/index.js";
|
|
13
|
+
const MODEL_SEARCH_CALLBACK = "model:search";
|
|
14
|
+
const MODEL_SEARCH_AGAIN_CALLBACK = "model:search:again";
|
|
15
|
+
const MODEL_SEARCH_CANCEL_CALLBACK = "model:search:cancel";
|
|
12
16
|
function buildModelSelectionMenuText(modelLists) {
|
|
13
17
|
const lines = [t("model.menu.select"), t("model.menu.favorites_title")];
|
|
14
18
|
if (modelLists.favorites.length === 0) {
|
|
@@ -20,9 +24,51 @@ function buildModelSelectionMenuText(modelLists) {
|
|
|
20
24
|
}
|
|
21
25
|
return lines.join("\n");
|
|
22
26
|
}
|
|
27
|
+
function parseModelSearchMetadata() {
|
|
28
|
+
const state = interactionManager.getSnapshot();
|
|
29
|
+
if (!state || state.kind !== "custom") {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const flow = state.metadata.flow;
|
|
33
|
+
const stage = state.metadata.stage;
|
|
34
|
+
if (flow !== "model-search" || typeof stage !== "string") {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const messageId = typeof state.metadata.messageId === "number" ? state.metadata.messageId : undefined;
|
|
38
|
+
return { flow, stage, messageId };
|
|
39
|
+
}
|
|
23
40
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
41
|
+
* Shared logic for applying a model selection and updating UI.
|
|
42
|
+
* Used by both the regular inline menu flow and the search results flow.
|
|
43
|
+
*/
|
|
44
|
+
async function applyModelSelectionAndNotify(ctx, modelInfo) {
|
|
45
|
+
if (ctx.chat) {
|
|
46
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
47
|
+
}
|
|
48
|
+
selectModel(modelInfo);
|
|
49
|
+
keyboardManager.updateModel(modelInfo);
|
|
50
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
51
|
+
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
52
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
53
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
54
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
55
|
+
: null);
|
|
56
|
+
keyboardManager.updateAgent(currentAgent);
|
|
57
|
+
if (contextInfo) {
|
|
58
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
59
|
+
}
|
|
60
|
+
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
61
|
+
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
62
|
+
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
63
|
+
await ctx.answerCallbackQuery({ text: t("model.changed_callback", { name: displayName }) });
|
|
64
|
+
await ctx.reply(t("model.changed_message", { name: displayName }), {
|
|
65
|
+
reply_markup: keyboard,
|
|
66
|
+
});
|
|
67
|
+
await ctx.deleteMessage().catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Handle model selection callback from the inline menu.
|
|
71
|
+
* Skips search-related callbacks (handled separately).
|
|
26
72
|
* @returns true if handled, false otherwise
|
|
27
73
|
*/
|
|
28
74
|
export async function handleModelSelect(ctx) {
|
|
@@ -30,15 +76,18 @@ export async function handleModelSelect(ctx) {
|
|
|
30
76
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith("model:")) {
|
|
31
77
|
return false;
|
|
32
78
|
}
|
|
79
|
+
// Skip search callbacks — handled by handleModelSearchCallback / handleModelSearchResults
|
|
80
|
+
if (callbackQuery.data === MODEL_SEARCH_CALLBACK ||
|
|
81
|
+
callbackQuery.data === MODEL_SEARCH_AGAIN_CALLBACK ||
|
|
82
|
+
callbackQuery.data === MODEL_SEARCH_CANCEL_CALLBACK) {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
33
85
|
const isActiveMenu = await ensureActiveInlineMenu(ctx, "model");
|
|
34
86
|
if (!isActiveMenu) {
|
|
35
87
|
return true;
|
|
36
88
|
}
|
|
37
89
|
logger.debug(`[ModelHandler] Received callback: ${callbackQuery.data}`);
|
|
38
90
|
try {
|
|
39
|
-
if (ctx.chat) {
|
|
40
|
-
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
41
|
-
}
|
|
42
91
|
// Parse callback data: "model:providerID:modelID"
|
|
43
92
|
const parts = callbackQuery.data.split(":");
|
|
44
93
|
if (parts.length < 3) {
|
|
@@ -52,35 +101,10 @@ export async function handleModelSelect(ctx) {
|
|
|
52
101
|
const modelInfo = {
|
|
53
102
|
providerID,
|
|
54
103
|
modelID,
|
|
55
|
-
variant: "default",
|
|
104
|
+
variant: "default",
|
|
56
105
|
};
|
|
57
|
-
// Select model and persist
|
|
58
|
-
selectModel(modelInfo);
|
|
59
|
-
// Update keyboard manager state (may not be initialized if no session selected)
|
|
60
|
-
keyboardManager.updateModel(modelInfo);
|
|
61
|
-
// Refresh context limit for new model
|
|
62
|
-
await pinnedMessageManager.refreshContextLimit();
|
|
63
|
-
// Update Reply Keyboard with new model and context
|
|
64
|
-
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
65
|
-
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
66
|
-
(pinnedMessageManager.getContextLimit() > 0
|
|
67
|
-
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
68
|
-
: null);
|
|
69
|
-
keyboardManager.updateAgent(currentAgent);
|
|
70
|
-
if (contextInfo) {
|
|
71
|
-
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
72
|
-
}
|
|
73
|
-
const variantName = formatVariantForButton(modelInfo.variant || "default");
|
|
74
|
-
const keyboard = createMainKeyboard(currentAgent, modelInfo, contextInfo ?? undefined, variantName);
|
|
75
|
-
const displayName = formatModelForDisplay(modelInfo.providerID, modelInfo.modelID);
|
|
76
106
|
clearActiveInlineMenu("model_selected");
|
|
77
|
-
|
|
78
|
-
await ctx.answerCallbackQuery({ text: t("model.changed_callback", { name: displayName }) });
|
|
79
|
-
await ctx.reply(t("model.changed_message", { name: displayName }), {
|
|
80
|
-
reply_markup: keyboard,
|
|
81
|
-
});
|
|
82
|
-
// Delete the inline menu message
|
|
83
|
-
await ctx.deleteMessage().catch(() => { });
|
|
107
|
+
await applyModelSelectionAndNotify(ctx, modelInfo);
|
|
84
108
|
return true;
|
|
85
109
|
}
|
|
86
110
|
catch (err) {
|
|
@@ -91,15 +115,15 @@ export async function handleModelSelect(ctx) {
|
|
|
91
115
|
}
|
|
92
116
|
}
|
|
93
117
|
/**
|
|
94
|
-
* Build inline keyboard with favorite and recent models
|
|
95
|
-
* @param currentModel Current model for highlighting
|
|
96
|
-
* @returns InlineKeyboard with model selection buttons
|
|
118
|
+
* Build inline keyboard with favorite and recent models, plus a search button at the top.
|
|
97
119
|
*/
|
|
98
120
|
export async function buildModelSelectionMenu(currentModel, modelLists) {
|
|
99
121
|
const keyboard = new InlineKeyboard();
|
|
100
122
|
const lists = modelLists ?? (await getModelSelectionLists());
|
|
101
123
|
const favorites = lists.favorites;
|
|
102
124
|
const recent = lists.recent;
|
|
125
|
+
// Search button — always present as first row
|
|
126
|
+
keyboard.text(t("model.search.button"), MODEL_SEARCH_CALLBACK).row();
|
|
103
127
|
if (favorites.length === 0 && recent.length === 0) {
|
|
104
128
|
logger.warn("[ModelHandler] No model choices found in favorites/recent");
|
|
105
129
|
return keyboard;
|
|
@@ -108,7 +132,6 @@ export async function buildModelSelectionMenu(currentModel, modelLists) {
|
|
|
108
132
|
const isActive = currentModel &&
|
|
109
133
|
model.providerID === currentModel.providerID &&
|
|
110
134
|
model.modelID === currentModel.modelID;
|
|
111
|
-
// Inline buttons use full model ID without truncation
|
|
112
135
|
const label = `${prefix} ${model.providerID}/${model.modelID}`;
|
|
113
136
|
const labelWithCheck = isActive ? `✅ ${label}` : label;
|
|
114
137
|
keyboard.text(labelWithCheck, `model:${model.providerID}:${model.modelID}`).row();
|
|
@@ -119,17 +142,13 @@ export async function buildModelSelectionMenu(currentModel, modelLists) {
|
|
|
119
142
|
}
|
|
120
143
|
/**
|
|
121
144
|
* Show model selection menu
|
|
122
|
-
* @param ctx grammY context
|
|
123
145
|
*/
|
|
124
146
|
export async function showModelSelectionMenu(ctx) {
|
|
125
147
|
try {
|
|
126
148
|
const currentModel = fetchCurrentModel();
|
|
127
149
|
const modelLists = await getModelSelectionLists();
|
|
128
150
|
const keyboard = await buildModelSelectionMenu(currentModel, modelLists);
|
|
129
|
-
|
|
130
|
-
await ctx.reply(t("model.menu.empty"));
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
151
|
+
// keyboard always has at least the search button, so length > 0
|
|
133
152
|
const text = buildModelSelectionMenuText(modelLists);
|
|
134
153
|
await replyWithInlineMenu(ctx, {
|
|
135
154
|
menuKind: "model",
|
|
@@ -142,3 +161,147 @@ export async function showModelSelectionMenu(ctx) {
|
|
|
142
161
|
await ctx.reply(t("model.menu.error"));
|
|
143
162
|
}
|
|
144
163
|
}
|
|
164
|
+
// ─── Model search handlers ────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Handle the search button callback (model:search) from the inline menu.
|
|
167
|
+
* Transitions the interaction to text-input mode and prompts the user.
|
|
168
|
+
*/
|
|
169
|
+
export async function handleModelSearchCallback(ctx) {
|
|
170
|
+
const data = ctx.callbackQuery?.data;
|
|
171
|
+
if (!data) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
if (data !== MODEL_SEARCH_CALLBACK) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
const isActive = await ensureActiveInlineMenu(ctx, "model");
|
|
178
|
+
if (!isActive) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
await ctx.answerCallbackQuery().catch(() => { });
|
|
182
|
+
await ctx.deleteMessage().catch(() => { });
|
|
183
|
+
// Start a new interaction for search text input
|
|
184
|
+
// interactionManager.start() clears any existing interaction automatically
|
|
185
|
+
interactionManager.start({
|
|
186
|
+
kind: "custom",
|
|
187
|
+
expectedInput: "text",
|
|
188
|
+
metadata: {
|
|
189
|
+
flow: "model-search",
|
|
190
|
+
stage: "input",
|
|
191
|
+
},
|
|
192
|
+
});
|
|
193
|
+
await ctx.reply(t("model.search.prompt"));
|
|
194
|
+
logger.debug("[ModelHandler] Model search prompt shown");
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Handle text input for model search.
|
|
199
|
+
* Searches the full provider catalog and shows results (or "not found").
|
|
200
|
+
*/
|
|
201
|
+
export async function handleModelSearchTextInput(ctx) {
|
|
202
|
+
const meta = parseModelSearchMetadata();
|
|
203
|
+
if (!meta || meta.stage !== "input") {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const text = ctx.message?.text;
|
|
207
|
+
if (!text) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
logger.debug(`[ModelHandler] Model search query: "${text}"`);
|
|
211
|
+
try {
|
|
212
|
+
const results = await searchModels(text);
|
|
213
|
+
const keyboard = new InlineKeyboard();
|
|
214
|
+
for (const model of results) {
|
|
215
|
+
const label = `${model.providerID}/${model.modelID}`;
|
|
216
|
+
keyboard.text(label, `model:${model.providerID}:${model.modelID}`).row();
|
|
217
|
+
}
|
|
218
|
+
keyboard.row();
|
|
219
|
+
keyboard.text(t("model.search.search_again"), MODEL_SEARCH_AGAIN_CALLBACK);
|
|
220
|
+
keyboard.text(t("inline.button.cancel"), MODEL_SEARCH_CANCEL_CALLBACK);
|
|
221
|
+
const replyText = results.length === 0
|
|
222
|
+
? t("model.search.no_results", { query: text })
|
|
223
|
+
: t("model.search.results_title", { query: text });
|
|
224
|
+
const sent = await ctx.reply(replyText, { reply_markup: keyboard });
|
|
225
|
+
// Transition to results stage (callback-only)
|
|
226
|
+
interactionManager.transition({
|
|
227
|
+
expectedInput: "callback",
|
|
228
|
+
metadata: {
|
|
229
|
+
flow: "model-search",
|
|
230
|
+
stage: "results",
|
|
231
|
+
messageId: sent.message_id,
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
logger.error("[ModelHandler] Model search error:", err);
|
|
238
|
+
await ctx.reply(t("model.search.error"));
|
|
239
|
+
interactionManager.clear("model_search_error");
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Handle callbacks from the search results menu:
|
|
245
|
+
* - model:search:cancel — clears interaction, deletes message
|
|
246
|
+
* - model:search:again — delegates to handleModelSearchCallback
|
|
247
|
+
* - model:provider:model — selects the model from search results
|
|
248
|
+
*/
|
|
249
|
+
export async function handleModelSearchResults(ctx) {
|
|
250
|
+
const data = ctx.callbackQuery?.data;
|
|
251
|
+
if (!data) {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
const meta = parseModelSearchMetadata();
|
|
255
|
+
if (!meta || meta.stage !== "results") {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
// Verify message ID matches to reject stale callbacks
|
|
259
|
+
const callbackMessageId = ctx.callbackQuery?.message?.message_id;
|
|
260
|
+
if (meta.messageId !== undefined && callbackMessageId !== meta.messageId) {
|
|
261
|
+
await ctx
|
|
262
|
+
.answerCallbackQuery({ text: t("inline.inactive_callback"), show_alert: true })
|
|
263
|
+
.catch(() => { });
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
// Cancel
|
|
267
|
+
if (data === MODEL_SEARCH_CANCEL_CALLBACK) {
|
|
268
|
+
interactionManager.clear("model_search_cancelled");
|
|
269
|
+
await ctx.answerCallbackQuery({ text: t("inline.cancelled_callback") }).catch(() => { });
|
|
270
|
+
await ctx.deleteMessage().catch(() => { });
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
// Search again — inline implementation
|
|
274
|
+
if (data === MODEL_SEARCH_AGAIN_CALLBACK) {
|
|
275
|
+
await ctx.answerCallbackQuery().catch(() => { });
|
|
276
|
+
await ctx.deleteMessage().catch(() => { });
|
|
277
|
+
interactionManager.start({
|
|
278
|
+
kind: "custom",
|
|
279
|
+
expectedInput: "text",
|
|
280
|
+
metadata: {
|
|
281
|
+
flow: "model-search",
|
|
282
|
+
stage: "input",
|
|
283
|
+
},
|
|
284
|
+
});
|
|
285
|
+
await ctx.reply(t("model.search.prompt"));
|
|
286
|
+
logger.debug("[ModelHandler] Model search prompt shown (search again)");
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
// Model selection from search results
|
|
290
|
+
if (data.startsWith("model:")) {
|
|
291
|
+
const parts = data.split(":");
|
|
292
|
+
if (parts.length < 3) {
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
const providerID = parts[1];
|
|
296
|
+
const modelID = parts.slice(2).join(":");
|
|
297
|
+
const modelInfo = {
|
|
298
|
+
providerID,
|
|
299
|
+
modelID,
|
|
300
|
+
variant: "default",
|
|
301
|
+
};
|
|
302
|
+
interactionManager.clear("model_search_selected");
|
|
303
|
+
await applyModelSelectionAndNotify(ctx, modelInfo);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
return false;
|
|
307
|
+
}
|
package/dist/bot/index.js
CHANGED
|
@@ -25,13 +25,14 @@ import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./com
|
|
|
25
25
|
import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
|
|
26
26
|
import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
|
|
27
27
|
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
28
|
+
import { handleMessagesCallback, messagesCommand } from "./commands/messages.js";
|
|
28
29
|
import { skillsCommand, handleSkillsCallback, handleSkillTextArguments, } from "./commands/skills.js";
|
|
29
30
|
import { mcpsCommand, handleMcpsCallback } from "./commands/mcps.js";
|
|
30
31
|
import { ttsCommand } from "./commands/tts.js";
|
|
31
32
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
32
33
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
33
34
|
import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
|
|
34
|
-
import { handleModelSelect, showModelSelectionMenu } from "./handlers/model.js";
|
|
35
|
+
import { handleModelSelect, handleModelSearchCallback, handleModelSearchResults, handleModelSearchTextInput, showModelSelectionMenu, } from "./handlers/model.js";
|
|
35
36
|
import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
|
|
36
37
|
import { handleContextButtonPress, handleCompactConfirm } from "./handlers/context.js";
|
|
37
38
|
import { handleInlineMenuCancel } from "./handlers/inline-menu.js";
|
|
@@ -59,7 +60,7 @@ import { handleVoiceMessage } from "./handlers/voice.js";
|
|
|
59
60
|
import { handleDocumentMessage } from "./handlers/document.js";
|
|
60
61
|
import { createMediaGroupAttachmentMiddleware } from "./handlers/media-group.js";
|
|
61
62
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
62
|
-
import { reconcileBusyState } from "./utils/busy-reconciliation.js";
|
|
63
|
+
import { reconcileBusyState, setResponseStreamerForReconciliation } from "./utils/busy-reconciliation.js";
|
|
63
64
|
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
64
65
|
import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
|
|
65
66
|
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
@@ -218,6 +219,7 @@ const responseStreamer = new ResponseStreamer({
|
|
|
218
219
|
});
|
|
219
220
|
},
|
|
220
221
|
});
|
|
222
|
+
setResponseStreamerForReconciliation(responseStreamer);
|
|
221
223
|
const toolCallStreamer = new ToolCallStreamer({
|
|
222
224
|
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
223
225
|
sendText: async (sessionId, text) => {
|
|
@@ -928,6 +930,7 @@ export function createBot() {
|
|
|
928
930
|
bot.command("open", openCommand);
|
|
929
931
|
bot.command("ls", lsCommand);
|
|
930
932
|
bot.command("sessions", sessionsCommand);
|
|
933
|
+
bot.command("messages", messagesCommand);
|
|
931
934
|
bot.command("new", (ctx) => newCommand(ctx, { bot, ensureEventSubscription }));
|
|
932
935
|
bot.command("abort", abortCommand);
|
|
933
936
|
bot.command("detach", detachCommand);
|
|
@@ -964,6 +967,8 @@ export function createBot() {
|
|
|
964
967
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
965
968
|
const handledPermission = await handlePermissionCallback(ctx);
|
|
966
969
|
const handledAgent = await handleAgentSelect(ctx);
|
|
970
|
+
const handledModelSearch = await handleModelSearchCallback(ctx);
|
|
971
|
+
const handledModelSearchResults = await handleModelSearchResults(ctx);
|
|
967
972
|
const handledModel = await handleModelSelect(ctx);
|
|
968
973
|
const handledVariant = await handleVariantSelect(ctx);
|
|
969
974
|
const handledCompactConfirm = await handleCompactConfirm(ctx);
|
|
@@ -971,9 +976,10 @@ export function createBot() {
|
|
|
971
976
|
const handledTaskList = await handleTaskListCallback(ctx);
|
|
972
977
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
973
978
|
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
979
|
+
const handledMessages = await handleMessagesCallback(ctx, { bot, ensureEventSubscription });
|
|
974
980
|
const handledSkills = await handleSkillsCallback(ctx, { bot, ensureEventSubscription });
|
|
975
981
|
const handledMcps = await handleMcpsCallback(ctx);
|
|
976
|
-
logger.debug(`[Bot] Callback handled: backgroundSession=${handledBackgroundSession}, inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, ls=${handledLs}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, skills=${handledSkills}, mcps=${handledMcps}`);
|
|
982
|
+
logger.debug(`[Bot] Callback handled: backgroundSession=${handledBackgroundSession}, inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, ls=${handledLs}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, modelSearch=${handledModelSearch}, modelSearchResults=${handledModelSearchResults}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, messages=${handledMessages}, skills=${handledSkills}, mcps=${handledMcps}`);
|
|
977
983
|
if (!handledBackgroundSession &&
|
|
978
984
|
!handledInlineCancel &&
|
|
979
985
|
!handledSession &&
|
|
@@ -984,6 +990,8 @@ export function createBot() {
|
|
|
984
990
|
!handledQuestion &&
|
|
985
991
|
!handledPermission &&
|
|
986
992
|
!handledAgent &&
|
|
993
|
+
!handledModelSearch &&
|
|
994
|
+
!handledModelSearchResults &&
|
|
987
995
|
!handledModel &&
|
|
988
996
|
!handledVariant &&
|
|
989
997
|
!handledCompactConfirm &&
|
|
@@ -991,6 +999,7 @@ export function createBot() {
|
|
|
991
999
|
!handledTaskList &&
|
|
992
1000
|
!handledRenameCancel &&
|
|
993
1001
|
!handledCommands &&
|
|
1002
|
+
!handledMessages &&
|
|
994
1003
|
!handledSkills &&
|
|
995
1004
|
!handledMcps) {
|
|
996
1005
|
logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
|
|
@@ -1183,6 +1192,10 @@ export function createBot() {
|
|
|
1183
1192
|
if (handledTask) {
|
|
1184
1193
|
return;
|
|
1185
1194
|
}
|
|
1195
|
+
const handledModelSearchText = await handleModelSearchTextInput(ctx);
|
|
1196
|
+
if (handledModelSearchText) {
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1186
1199
|
const handledRename = await handleRenameTextAnswer(ctx);
|
|
1187
1200
|
if (handledRename) {
|
|
1188
1201
|
return;
|
|
@@ -144,6 +144,14 @@ export class ResponseStreamer {
|
|
|
144
144
|
logger.debug(`[ResponseStreamer] Cleared all streams: count=${count}, reason=${reason}`);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
hasActiveStream(sessionId) {
|
|
148
|
+
for (const state of this.states.values()) {
|
|
149
|
+
if (state.sessionId === sessionId && !state.cancelled) {
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
147
155
|
getOrCreateState(sessionId, messageId) {
|
|
148
156
|
const key = buildStateKey(sessionId, messageId);
|
|
149
157
|
const existing = this.states.get(key);
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { getAgentDisplayName } from "../../agent/types.js";
|
|
2
|
-
function
|
|
3
|
-
const safeElapsedMs = Math.max(0, elapsedMs);
|
|
4
|
-
|
|
2
|
+
function formatDuration(elapsedMs) {
|
|
3
|
+
const safeElapsedMs = Math.max(0, Math.round(elapsedMs));
|
|
4
|
+
const totalSeconds = Math.floor(safeElapsedMs / 1000);
|
|
5
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
6
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
7
|
+
const seconds = totalSeconds % 60;
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (hours > 0)
|
|
10
|
+
parts.push(`${hours}h`);
|
|
11
|
+
if (minutes > 0)
|
|
12
|
+
parts.push(`${minutes}m`);
|
|
13
|
+
if (seconds > 0 || parts.length === 0)
|
|
14
|
+
parts.push(`${seconds}s`);
|
|
15
|
+
return parts.join(" ");
|
|
5
16
|
}
|
|
6
17
|
export function formatAssistantRunFooter({ agent, providerID, modelID, elapsedMs, }) {
|
|
7
18
|
const agentDisplay = getAgentDisplayName(agent);
|
|
8
|
-
return `${agentDisplay} · 🤖 ${providerID}/${modelID} · 🕒 ${
|
|
19
|
+
return `${agentDisplay} · 🤖 ${providerID}/${modelID} · 🕒 ${formatDuration(elapsedMs)}`;
|
|
9
20
|
}
|
|
@@ -10,6 +10,10 @@ const RECONCILE_MIN_INTERVAL_MS = 10_000;
|
|
|
10
10
|
const FOREGROUND_BUSY_RECONCILE_GRACE_MS = 2_000;
|
|
11
11
|
const inFlightDirectories = new Set();
|
|
12
12
|
const lastReconcileAtByDirectory = new Map();
|
|
13
|
+
let responseStreamerInstance = null;
|
|
14
|
+
export function setResponseStreamerForReconciliation(streamer) {
|
|
15
|
+
responseStreamerInstance = streamer;
|
|
16
|
+
}
|
|
13
17
|
function getReconciliationTargets(directory) {
|
|
14
18
|
const foregroundBusySessions = foregroundSessionState
|
|
15
19
|
.getBusySessions()
|
|
@@ -72,6 +76,10 @@ export async function reconcileBusyStateNow(directory, now = Date.now()) {
|
|
|
72
76
|
logger.debug(`[BusyReconciliation] Skipping fresh foreground busy state: session=${session.sessionId}, directory=${session.directory}, status=${status?.type ?? "not-found"}`);
|
|
73
77
|
continue;
|
|
74
78
|
}
|
|
79
|
+
if (responseStreamerInstance?.hasActiveStream(session.sessionId)) {
|
|
80
|
+
logger.debug(`[BusyReconciliation] Skipping clear, responseStreamer still active: session=${session.sessionId}`);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
75
83
|
logger.info(`[BusyReconciliation] Clearing stale foreground busy state: session=${session.sessionId}, directory=${session.directory}, status=${status?.type ?? "not-found"}`);
|
|
76
84
|
if (attachedSessionForDirectory?.sessionId !== session.sessionId) {
|
|
77
85
|
await markAttachedSessionIdle(session.sessionId);
|
package/dist/config.js
CHANGED
|
@@ -105,11 +105,12 @@ export const config = {
|
|
|
105
105
|
},
|
|
106
106
|
bot: {
|
|
107
107
|
sessionsListLimit: getOptionalPositiveIntEnvVar("SESSIONS_LIST_LIMIT", 10),
|
|
108
|
+
messagesListLimit: getOptionalPositiveIntEnvVar("MESSAGES_LIST_LIMIT", 10),
|
|
108
109
|
projectsListLimit: getOptionalPositiveIntEnvVar("PROJECTS_LIST_LIMIT", 10),
|
|
109
110
|
commandsListLimit: getOptionalPositiveIntEnvVar("COMMANDS_LIST_LIMIT", 10),
|
|
110
111
|
taskLimit: getOptionalPositiveIntEnvVar("TASK_LIMIT", 10),
|
|
111
112
|
scheduledTaskExecutionTimeoutMinutes: getOptionalPositiveIntEnvVar("SCHEDULED_TASK_EXECUTION_TIMEOUT_MINUTES", 120),
|
|
112
|
-
responseStreamThrottleMs: getOptionalPositiveIntEnvVar("RESPONSE_STREAM_THROTTLE_MS",
|
|
113
|
+
responseStreamThrottleMs: getOptionalPositiveIntEnvVar("RESPONSE_STREAM_THROTTLE_MS", 1000),
|
|
113
114
|
bashToolDisplayMaxLength: getOptionalPositiveIntEnvVar("BASH_TOOL_DISPLAY_MAX_LENGTH", 128),
|
|
114
115
|
locale: getOptionalLocaleEnvVar("BOT_LOCALE", "en"),
|
|
115
116
|
hideThinkingMessages: getOptionalBooleanEnvVar("HIDE_THINKING_MESSAGES", false),
|
package/dist/i18n/de.js
CHANGED
|
@@ -4,6 +4,7 @@ export const de = {
|
|
|
4
4
|
"cmd.description.stop": "Aktuelle Aktion stoppen",
|
|
5
5
|
"cmd.description.detach": "Von aktueller Sitzung trennen",
|
|
6
6
|
"cmd.description.sessions": "Sitzungen auflisten",
|
|
7
|
+
"cmd.description.messages": "Sitzungsnachrichten durchsuchen",
|
|
7
8
|
"cmd.description.tts": "Audioantworten umschalten",
|
|
8
9
|
"cmd.description.projects": "Projekte auflisten",
|
|
9
10
|
"cmd.description.worktree": "Git-Worktrees wechseln",
|
|
@@ -126,6 +127,26 @@ export const de = {
|
|
|
126
127
|
"sessions.preview.title": "Letzte Nachrichten:",
|
|
127
128
|
"sessions.preview.you": "Du:",
|
|
128
129
|
"sessions.preview.agent": "Agent:",
|
|
130
|
+
"messages.project_not_selected": "🏗 Kein Projekt ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.",
|
|
131
|
+
"messages.session_not_selected": "💬 Keine Sitzung ausgewählt.\n\nWähle zuerst eine Sitzung mit /sessions oder erstelle eine mit /new.",
|
|
132
|
+
"messages.session_project_mismatch": "⚠️ Die ausgewählte Sitzung passt nicht zum aktuellen Projekt. Wähle die Sitzung erneut über /sessions.",
|
|
133
|
+
"messages.empty": "📭 Keine Benutzernachrichten in der aktuellen Sitzung.",
|
|
134
|
+
"messages.select": "Wähle eine Nachricht:",
|
|
135
|
+
"messages.select_page": "Wähle eine Nachricht (Seite {page}):",
|
|
136
|
+
"messages.fetch_error": "🔴 OpenCode Server ist nicht erreichbar oder beim Laden der Nachrichten ist ein Fehler aufgetreten.",
|
|
137
|
+
"messages.inactive_callback": "Dieses Nachrichtenmenü ist nicht mehr aktiv",
|
|
138
|
+
"messages.cancelled_callback": "Abgebrochen",
|
|
139
|
+
"messages.page_empty_callback": "Keine Nachrichten auf dieser Seite",
|
|
140
|
+
"messages.button.prev_page": "⬅️ Zurück",
|
|
141
|
+
"messages.button.next_page": "Weiter ➡️",
|
|
142
|
+
"messages.button.revert": "↩️ Revert",
|
|
143
|
+
"messages.button.fork": "🔀 Fork",
|
|
144
|
+
"messages.button.back": "⬅️ Zurück",
|
|
145
|
+
"messages.button.cancel": "❌ Abbrechen",
|
|
146
|
+
"messages.revert_success": "✅ Zurück zur Nachricht:\n\n{text}",
|
|
147
|
+
"messages.revert_error": "❌ Nachricht konnte nicht zurückgesetzt werden. Bitte versuche es erneut.",
|
|
148
|
+
"messages.fork_success": "🔀 Fork erstellt von Nachricht:\n\n{text}",
|
|
149
|
+
"messages.fork_error": "❌ Fork konnte nicht erstellt werden. Bitte versuche es erneut.",
|
|
129
150
|
"attach.project_not_selected": "🏗 Projekt ist nicht ausgewählt.\n\nWähle zuerst ein Projekt mit /projects.",
|
|
130
151
|
"attach.session_not_selected": "💬 Keine Sitzung ausgewählt.\n\nWähle zuerst mit /sessions eine Sitzung aus.",
|
|
131
152
|
"attach.session_project_mismatch": "⚠️ Die ausgewählte Sitzung passt nicht zum aktuellen Projekt. Wähle sie über /sessions erneut aus.",
|
|
@@ -190,6 +211,12 @@ export const de = {
|
|
|
190
211
|
"model.menu.recent_empty": "— Leer.",
|
|
191
212
|
"model.menu.favorites_hint": "ℹ️ Füge Modelle in OpenCode CLI zu den Favoriten hinzu, damit sie oben angezeigt werden.",
|
|
192
213
|
"model.menu.error": "🔴 Modellliste konnte nicht geladen werden",
|
|
214
|
+
"model.search.button": "🔍 Suche",
|
|
215
|
+
"model.search.prompt": "🔍 Modellnamen zum Suchen eingeben:",
|
|
216
|
+
"model.search.results_title": "Suchergebnisse für \"{query}\":",
|
|
217
|
+
"model.search.no_results": "Keine Modelle gefunden für \"{query}\"",
|
|
218
|
+
"model.search.search_again": "↩ Erneut suchen",
|
|
219
|
+
"model.search.error": "Suche fehlgeschlagen",
|
|
193
220
|
"variant.model_not_selected_callback": "Fehler: Modell ist nicht ausgewählt",
|
|
194
221
|
"variant.changed_callback": "Variante geändert: {name}",
|
|
195
222
|
"variant.changed_message": "✅ Variante geändert zu: {name}",
|
package/dist/i18n/en.js
CHANGED
|
@@ -4,6 +4,7 @@ export const en = {
|
|
|
4
4
|
"cmd.description.stop": "Stop current action",
|
|
5
5
|
"cmd.description.detach": "Detach from current session",
|
|
6
6
|
"cmd.description.sessions": "List sessions",
|
|
7
|
+
"cmd.description.messages": "Browse session messages",
|
|
7
8
|
"cmd.description.tts": "Toggle audio replies",
|
|
8
9
|
"cmd.description.projects": "List projects",
|
|
9
10
|
"cmd.description.worktree": "Switch git worktrees",
|
|
@@ -126,6 +127,26 @@ export const en = {
|
|
|
126
127
|
"sessions.preview.title": "Recent messages:",
|
|
127
128
|
"sessions.preview.you": "You:",
|
|
128
129
|
"sessions.preview.agent": "Agent:",
|
|
130
|
+
"messages.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
|
|
131
|
+
"messages.session_not_selected": "💬 Session is not selected.\n\nFirst choose a session with /sessions or create one with /new.",
|
|
132
|
+
"messages.session_project_mismatch": "⚠️ The selected session does not match the current project. Choose the session again via /sessions.",
|
|
133
|
+
"messages.empty": "📭 No user messages in the current session.",
|
|
134
|
+
"messages.select": "Choose a message:",
|
|
135
|
+
"messages.select_page": "Choose a message (page {page}):",
|
|
136
|
+
"messages.fetch_error": "🔴 OpenCode Server is unavailable or an error occurred while loading messages.",
|
|
137
|
+
"messages.inactive_callback": "This messages menu is inactive",
|
|
138
|
+
"messages.cancelled_callback": "Cancelled",
|
|
139
|
+
"messages.page_empty_callback": "No messages on this page",
|
|
140
|
+
"messages.button.prev_page": "⬅️ Prev",
|
|
141
|
+
"messages.button.next_page": "Next ➡️",
|
|
142
|
+
"messages.button.revert": "↩️ Revert",
|
|
143
|
+
"messages.button.fork": "🔀 Fork",
|
|
144
|
+
"messages.button.back": "⬅️ Back",
|
|
145
|
+
"messages.button.cancel": "❌ Cancel",
|
|
146
|
+
"messages.revert_success": "✅ Reverted to message:\n\n{text}",
|
|
147
|
+
"messages.revert_error": "❌ Failed to revert message. Please try again.",
|
|
148
|
+
"messages.fork_success": "🔀 Fork created from message:\n\n{text}",
|
|
149
|
+
"messages.fork_error": "❌ Failed to create fork. Please try again.",
|
|
129
150
|
"attach.project_not_selected": "🏗 Project is not selected.\n\nFirst select a project with /projects.",
|
|
130
151
|
"attach.session_not_selected": "💬 Session is not selected.\n\nFirst choose a session with /sessions.",
|
|
131
152
|
"attach.session_project_mismatch": "⚠️ The selected session does not match the current project. Choose the session again via /sessions.",
|
|
@@ -190,6 +211,12 @@ export const en = {
|
|
|
190
211
|
"model.menu.recent_empty": "— Empty.",
|
|
191
212
|
"model.menu.favorites_hint": "ℹ️ Add models to favorites in OpenCode CLI to keep them at the top.",
|
|
192
213
|
"model.menu.error": "🔴 Failed to get models list",
|
|
214
|
+
"model.search.button": "🔍 Search",
|
|
215
|
+
"model.search.prompt": "🔍 Enter model name to search:",
|
|
216
|
+
"model.search.results_title": "Search results for \"{query}\":",
|
|
217
|
+
"model.search.no_results": "No models found for \"{query}\"",
|
|
218
|
+
"model.search.search_again": "↩ Search again",
|
|
219
|
+
"model.search.error": "Search failed",
|
|
193
220
|
"variant.model_not_selected_callback": "Error: model is not selected",
|
|
194
221
|
"variant.changed_callback": "Variant changed: {name}",
|
|
195
222
|
"variant.changed_message": "✅ Variant changed to: {name}",
|
package/dist/i18n/es.js
CHANGED
|
@@ -4,6 +4,7 @@ export const es = {
|
|
|
4
4
|
"cmd.description.stop": "Detener la acción actual",
|
|
5
5
|
"cmd.description.detach": "Desconectar de la sesión actual",
|
|
6
6
|
"cmd.description.sessions": "Listar sesiones",
|
|
7
|
+
"cmd.description.messages": "Ver mensajes de la sesión",
|
|
7
8
|
"cmd.description.tts": "Alternar respuestas de audio",
|
|
8
9
|
"cmd.description.projects": "Listar proyectos",
|
|
9
10
|
"cmd.description.worktree": "Cambiar worktrees de git",
|
|
@@ -126,6 +127,26 @@ export const es = {
|
|
|
126
127
|
"sessions.preview.title": "Mensajes recientes:",
|
|
127
128
|
"sessions.preview.you": "Tú:",
|
|
128
129
|
"sessions.preview.agent": "Agente:",
|
|
130
|
+
"messages.project_not_selected": "🏗 No hay ningún proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
|
|
131
|
+
"messages.session_not_selected": "💬 No hay ninguna sesión seleccionada.\n\nPrimero elige una sesión con /sessions o crea una con /new.",
|
|
132
|
+
"messages.session_project_mismatch": "⚠️ La sesión seleccionada no coincide con el proyecto actual. Vuelve a elegir la sesión con /sessions.",
|
|
133
|
+
"messages.empty": "📭 No hay mensajes de usuario en la sesión actual.",
|
|
134
|
+
"messages.select": "Elige un mensaje:",
|
|
135
|
+
"messages.select_page": "Elige un mensaje (página {page}):",
|
|
136
|
+
"messages.fetch_error": "🔴 OpenCode Server no está disponible o se produjo un error al cargar los mensajes.",
|
|
137
|
+
"messages.inactive_callback": "Este menú de mensajes ya no está activo",
|
|
138
|
+
"messages.cancelled_callback": "Cancelado",
|
|
139
|
+
"messages.page_empty_callback": "No hay mensajes en esta página",
|
|
140
|
+
"messages.button.prev_page": "⬅️ Anterior",
|
|
141
|
+
"messages.button.next_page": "Siguiente ➡️",
|
|
142
|
+
"messages.button.revert": "↩️ Revert",
|
|
143
|
+
"messages.button.fork": "🔀 Fork",
|
|
144
|
+
"messages.button.back": "⬅️ Volver",
|
|
145
|
+
"messages.button.cancel": "❌ Cancelar",
|
|
146
|
+
"messages.revert_success": "✅ Revertido al mensaje:\n\n{text}",
|
|
147
|
+
"messages.revert_error": "❌ No se pudo revertir el mensaje. Inténtalo de nuevo.",
|
|
148
|
+
"messages.fork_success": "🔀 Fork creado desde el mensaje:\n\n{text}",
|
|
149
|
+
"messages.fork_error": "❌ No se pudo crear el fork. Inténtalo de nuevo.",
|
|
129
150
|
"attach.project_not_selected": "🏗 No hay un proyecto seleccionado.\n\nPrimero selecciona un proyecto con /projects.",
|
|
130
151
|
"attach.session_not_selected": "💬 No hay una sesión seleccionada.\n\nPrimero selecciona una sesión con /sessions.",
|
|
131
152
|
"attach.session_project_mismatch": "⚠️ La sesión seleccionada no coincide con el proyecto actual. Vuelve a seleccionarla con /sessions.",
|
|
@@ -190,6 +211,12 @@ export const es = {
|
|
|
190
211
|
"model.menu.recent_empty": "— Vacío.",
|
|
191
212
|
"model.menu.favorites_hint": "ℹ️ Agrega modelos a favoritos en OpenCode CLI para mantenerlos arriba de la lista.",
|
|
192
213
|
"model.menu.error": "🔴 No se pudo obtener la lista de modelos",
|
|
214
|
+
"model.search.button": "🔍 Buscar",
|
|
215
|
+
"model.search.prompt": "🔍 Ingrese el nombre del modelo para buscar:",
|
|
216
|
+
"model.search.results_title": "Resultados de búsqueda para \"{query}\":",
|
|
217
|
+
"model.search.no_results": "No se encontraron modelos para \"{query}\"",
|
|
218
|
+
"model.search.search_again": "↩ Buscar de nuevo",
|
|
219
|
+
"model.search.error": "Búsqueda fallida",
|
|
193
220
|
"variant.model_not_selected_callback": "Error: no hay un modelo seleccionado",
|
|
194
221
|
"variant.changed_callback": "Variante cambiada: {name}",
|
|
195
222
|
"variant.changed_message": "✅ Variante cambiada a: {name}",
|