@grinev/opencode-telegram-bot 0.17.0 → 0.19.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 +22 -1
- package/README.md +35 -6
- package/dist/app/start-bot-app.js +4 -0
- package/dist/attach/manager.js +66 -0
- package/dist/attach/service.js +171 -0
- package/dist/bot/commands/abort.js +0 -4
- package/dist/bot/commands/commands.js +13 -3
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/mcps.js +394 -0
- package/dist/bot/commands/new.js +10 -18
- package/dist/bot/commands/opencode-start.js +2 -10
- package/dist/bot/commands/sessions.js +19 -20
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/handlers/prompt.js +26 -31
- package/dist/bot/index.js +82 -10
- package/dist/bot/middleware/interaction-guard.js +1 -1
- package/dist/bot/utils/busy-guard.js +3 -2
- package/dist/bot/utils/external-user-input.js +46 -0
- package/dist/bot/utils/switch-project.js +2 -0
- package/dist/config.js +25 -6
- package/dist/external-input/suppression.js +54 -0
- package/dist/i18n/de.js +34 -0
- package/dist/i18n/en.js +34 -0
- package/dist/i18n/es.js +34 -0
- package/dist/i18n/fr.js +34 -0
- package/dist/i18n/ru.js +34 -0
- package/dist/i18n/zh.js +34 -0
- package/dist/interaction/guard.js +2 -1
- package/dist/opencode/auto-restart.js +92 -0
- package/dist/opencode/events.js +13 -1
- package/dist/opencode/process.js +18 -1
- package/dist/pinned/manager.js +39 -0
- package/dist/summary/aggregator.js +89 -9
- package/dist/summary/formatter.js +2 -2
- package/dist/summary/markdown-to-telegram-v2.js +99 -0
- package/dist/summary/subagent-formatter.js +23 -2
- package/dist/telegram/render/inline-renderer.js +18 -0
- package/dist/tts/client.js +94 -19
- package/package.json +2 -2
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
3
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
4
|
+
import { interactionManager } from "../../interaction/manager.js";
|
|
5
|
+
import { logger } from "../../utils/logger.js";
|
|
6
|
+
import { t } from "../../i18n/index.js";
|
|
7
|
+
const MCPS_CALLBACK_PREFIX = "mcps:";
|
|
8
|
+
const MCPS_CALLBACK_SELECT_PREFIX = `${MCPS_CALLBACK_PREFIX}select:`;
|
|
9
|
+
const MCPS_CALLBACK_TOGGLE = `${MCPS_CALLBACK_PREFIX}toggle`;
|
|
10
|
+
const MCPS_CALLBACK_BACK = `${MCPS_CALLBACK_PREFIX}back`;
|
|
11
|
+
const MCPS_CALLBACK_CANCEL = `${MCPS_CALLBACK_PREFIX}cancel`;
|
|
12
|
+
const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
|
|
13
|
+
function normalizeDirectoryForApi(directory) {
|
|
14
|
+
return directory.replace(/\\/g, "/");
|
|
15
|
+
}
|
|
16
|
+
function getCallbackMessageId(ctx) {
|
|
17
|
+
const message = ctx.callbackQuery?.message;
|
|
18
|
+
if (!message || !("message_id" in message)) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
const messageId = message.message_id;
|
|
22
|
+
return typeof messageId === "number" ? messageId : null;
|
|
23
|
+
}
|
|
24
|
+
function getStatusLabel(status) {
|
|
25
|
+
switch (status.status) {
|
|
26
|
+
case "connected":
|
|
27
|
+
return t("mcps.status.connected");
|
|
28
|
+
case "disabled":
|
|
29
|
+
return t("mcps.status.disabled");
|
|
30
|
+
case "failed":
|
|
31
|
+
return t("mcps.status.failed");
|
|
32
|
+
case "needs_auth":
|
|
33
|
+
return t("mcps.status.needs_auth");
|
|
34
|
+
case "needs_client_registration":
|
|
35
|
+
return t("mcps.status.needs_client_registration");
|
|
36
|
+
default:
|
|
37
|
+
return t("common.unknown");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function getStatusEmoji(status) {
|
|
41
|
+
switch (status.status) {
|
|
42
|
+
case "connected":
|
|
43
|
+
return "🟢";
|
|
44
|
+
case "disabled":
|
|
45
|
+
return "🔴";
|
|
46
|
+
case "failed":
|
|
47
|
+
return "⚠️";
|
|
48
|
+
case "needs_auth":
|
|
49
|
+
return "🔒";
|
|
50
|
+
case "needs_client_registration":
|
|
51
|
+
return "🔒";
|
|
52
|
+
default:
|
|
53
|
+
return "❓";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function formatMcpButtonLabel(server) {
|
|
57
|
+
const rawLabel = `${getStatusEmoji(server.status)} ${server.name}`;
|
|
58
|
+
if (rawLabel.length <= MAX_INLINE_BUTTON_LABEL_LENGTH) {
|
|
59
|
+
return rawLabel;
|
|
60
|
+
}
|
|
61
|
+
return `${rawLabel.slice(0, MAX_INLINE_BUTTON_LABEL_LENGTH - 3)}...`;
|
|
62
|
+
}
|
|
63
|
+
function buildMcpsListKeyboard(servers) {
|
|
64
|
+
const keyboard = new InlineKeyboard();
|
|
65
|
+
servers.forEach((server, index) => {
|
|
66
|
+
keyboard
|
|
67
|
+
.text(formatMcpButtonLabel(server), `${MCPS_CALLBACK_SELECT_PREFIX}${index}`)
|
|
68
|
+
.row();
|
|
69
|
+
});
|
|
70
|
+
keyboard.text(t("inline.button.cancel"), MCPS_CALLBACK_CANCEL);
|
|
71
|
+
return keyboard;
|
|
72
|
+
}
|
|
73
|
+
function buildMcpsDetailKeyboard(server) {
|
|
74
|
+
const keyboard = new InlineKeyboard();
|
|
75
|
+
let hasToggleButton = false;
|
|
76
|
+
if (server.status.status === "connected") {
|
|
77
|
+
keyboard.text(t("mcps.button.disable"), MCPS_CALLBACK_TOGGLE);
|
|
78
|
+
hasToggleButton = true;
|
|
79
|
+
}
|
|
80
|
+
else if (server.status.status === "disabled" || server.status.status === "failed") {
|
|
81
|
+
keyboard.text(t("mcps.button.enable"), MCPS_CALLBACK_TOGGLE);
|
|
82
|
+
hasToggleButton = true;
|
|
83
|
+
}
|
|
84
|
+
if (hasToggleButton) {
|
|
85
|
+
keyboard.row();
|
|
86
|
+
}
|
|
87
|
+
keyboard.text(t("mcps.button.back"), MCPS_CALLBACK_BACK);
|
|
88
|
+
keyboard.text(t("inline.button.cancel"), MCPS_CALLBACK_CANCEL);
|
|
89
|
+
return keyboard;
|
|
90
|
+
}
|
|
91
|
+
function buildMcpsDetailText(server) {
|
|
92
|
+
const lines = [];
|
|
93
|
+
lines.push(t("mcps.detail.title", { name: server.name }));
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(t("mcps.detail.status", { status: getStatusLabel(server.status) }));
|
|
96
|
+
if (server.status.status === "failed" || server.status.status === "needs_client_registration") {
|
|
97
|
+
lines.push(t("mcps.detail.error", { error: server.status.error }));
|
|
98
|
+
}
|
|
99
|
+
if (server.status.status === "needs_auth" || server.status.status === "needs_client_registration") {
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push(t("mcps.auth_required"));
|
|
102
|
+
}
|
|
103
|
+
return lines.join("\n");
|
|
104
|
+
}
|
|
105
|
+
function parseMcpsServers(value) {
|
|
106
|
+
if (!value || typeof value !== "object") {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
if (Array.isArray(value)) {
|
|
110
|
+
const servers = [];
|
|
111
|
+
for (const item of value) {
|
|
112
|
+
if (!item || typeof item !== "object") {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const name = item.name;
|
|
116
|
+
const status = item.status;
|
|
117
|
+
if (typeof name !== "string" || !status || typeof status !== "object") {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
const s = status;
|
|
121
|
+
if (typeof s.status !== "string") {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
const mcpStatus = { status: s.status };
|
|
125
|
+
if ("error" in s && typeof s.error === "string") {
|
|
126
|
+
mcpStatus.error = s.error;
|
|
127
|
+
}
|
|
128
|
+
servers.push({ name, status: mcpStatus });
|
|
129
|
+
}
|
|
130
|
+
return servers;
|
|
131
|
+
}
|
|
132
|
+
const entries = Object.entries(value);
|
|
133
|
+
const servers = [];
|
|
134
|
+
for (const [name, status] of entries) {
|
|
135
|
+
if (!status || typeof status !== "object") {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
const s = status;
|
|
139
|
+
if (typeof s.status !== "string") {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const mcpStatus = { status: s.status };
|
|
143
|
+
if ("error" in s && typeof s.error === "string") {
|
|
144
|
+
mcpStatus.error = s.error;
|
|
145
|
+
}
|
|
146
|
+
servers.push({ name, status: mcpStatus });
|
|
147
|
+
}
|
|
148
|
+
return servers;
|
|
149
|
+
}
|
|
150
|
+
function parseMcpsMetadata(state) {
|
|
151
|
+
if (!state || state.kind !== "custom") {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const flow = state.metadata.flow;
|
|
155
|
+
const stage = state.metadata.stage;
|
|
156
|
+
const messageId = state.metadata.messageId;
|
|
157
|
+
const projectDirectory = state.metadata.projectDirectory;
|
|
158
|
+
if (flow !== "mcps" || typeof messageId !== "number" || typeof projectDirectory !== "string") {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
const servers = parseMcpsServers(state.metadata.servers);
|
|
162
|
+
if (!servers) {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
if (stage === "list") {
|
|
166
|
+
return {
|
|
167
|
+
flow,
|
|
168
|
+
stage,
|
|
169
|
+
messageId,
|
|
170
|
+
projectDirectory,
|
|
171
|
+
servers,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
if (stage === "detail") {
|
|
175
|
+
const serverName = state.metadata.serverName;
|
|
176
|
+
if (typeof serverName !== "string" || !serverName.trim()) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
flow,
|
|
181
|
+
stage,
|
|
182
|
+
messageId,
|
|
183
|
+
projectDirectory,
|
|
184
|
+
serverName,
|
|
185
|
+
servers,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
function clearMcpsInteraction(reason) {
|
|
191
|
+
const metadata = parseMcpsMetadata(interactionManager.getSnapshot());
|
|
192
|
+
if (metadata) {
|
|
193
|
+
interactionManager.clear(reason);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function parseSelectIndex(data) {
|
|
197
|
+
if (!data.startsWith(MCPS_CALLBACK_SELECT_PREFIX)) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const index = Number(data.slice(MCPS_CALLBACK_SELECT_PREFIX.length));
|
|
201
|
+
if (!Number.isInteger(index) || index < 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
return index;
|
|
205
|
+
}
|
|
206
|
+
async function getMcpServerList(projectDirectory) {
|
|
207
|
+
const { data, error } = await opencodeClient.mcp.status({
|
|
208
|
+
directory: normalizeDirectoryForApi(projectDirectory),
|
|
209
|
+
});
|
|
210
|
+
if (error || !data) {
|
|
211
|
+
throw error || new Error("No MCP status data received");
|
|
212
|
+
}
|
|
213
|
+
const servers = parseMcpsServers(data);
|
|
214
|
+
if (!servers) {
|
|
215
|
+
throw new Error("Invalid MCP status data format");
|
|
216
|
+
}
|
|
217
|
+
return servers;
|
|
218
|
+
}
|
|
219
|
+
async function toggleMcpServer(projectDirectory, serverName, enable) {
|
|
220
|
+
const params = {
|
|
221
|
+
name: serverName,
|
|
222
|
+
directory: normalizeDirectoryForApi(projectDirectory),
|
|
223
|
+
};
|
|
224
|
+
if (enable) {
|
|
225
|
+
const { error } = await opencodeClient.mcp.connect(params);
|
|
226
|
+
if (error) {
|
|
227
|
+
throw error;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
const { error } = await opencodeClient.mcp.disconnect(params);
|
|
232
|
+
if (error) {
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export async function mcpsCommand(ctx) {
|
|
238
|
+
try {
|
|
239
|
+
const currentProject = getCurrentProject();
|
|
240
|
+
if (!currentProject) {
|
|
241
|
+
await ctx.reply(t("bot.project_not_selected"));
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const servers = await getMcpServerList(currentProject.worktree);
|
|
245
|
+
if (servers.length === 0) {
|
|
246
|
+
await ctx.reply(t("mcps.empty"));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const keyboard = buildMcpsListKeyboard(servers);
|
|
250
|
+
const message = await ctx.reply(t("mcps.select"), {
|
|
251
|
+
reply_markup: keyboard,
|
|
252
|
+
});
|
|
253
|
+
interactionManager.start({
|
|
254
|
+
kind: "custom",
|
|
255
|
+
expectedInput: "callback",
|
|
256
|
+
metadata: {
|
|
257
|
+
flow: "mcps",
|
|
258
|
+
stage: "list",
|
|
259
|
+
messageId: message.message_id,
|
|
260
|
+
projectDirectory: currentProject.worktree,
|
|
261
|
+
servers,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
catch (error) {
|
|
266
|
+
logger.error("[Mcps] Error fetching MCP servers list:", error);
|
|
267
|
+
await ctx.reply(t("mcps.fetch_error"));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
export async function handleMcpsCallback(ctx) {
|
|
271
|
+
const data = ctx.callbackQuery?.data;
|
|
272
|
+
if (!data || !data.startsWith(MCPS_CALLBACK_PREFIX)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const metadata = parseMcpsMetadata(interactionManager.getSnapshot());
|
|
276
|
+
const callbackMessageId = getCallbackMessageId(ctx);
|
|
277
|
+
if (!metadata || callbackMessageId === null || metadata.messageId !== callbackMessageId) {
|
|
278
|
+
await ctx.answerCallbackQuery({ text: t("inline.inactive_callback"), show_alert: true });
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
if (data === MCPS_CALLBACK_CANCEL) {
|
|
283
|
+
clearMcpsInteraction("mcps_cancelled");
|
|
284
|
+
await ctx.answerCallbackQuery({ text: t("inline.cancelled_callback") });
|
|
285
|
+
await ctx.deleteMessage().catch(() => { });
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
if (data === MCPS_CALLBACK_BACK) {
|
|
289
|
+
if (metadata.stage !== "detail") {
|
|
290
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
const servers = await getMcpServerList(metadata.projectDirectory);
|
|
294
|
+
const keyboard = buildMcpsListKeyboard(servers);
|
|
295
|
+
await ctx.editMessageText(t("mcps.select"), { reply_markup: keyboard });
|
|
296
|
+
await ctx.answerCallbackQuery();
|
|
297
|
+
interactionManager.transition({
|
|
298
|
+
expectedInput: "callback",
|
|
299
|
+
metadata: {
|
|
300
|
+
flow: "mcps",
|
|
301
|
+
stage: "list",
|
|
302
|
+
messageId: metadata.messageId,
|
|
303
|
+
projectDirectory: metadata.projectDirectory,
|
|
304
|
+
servers,
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (data === MCPS_CALLBACK_TOGGLE) {
|
|
310
|
+
if (metadata.stage !== "detail") {
|
|
311
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
const serverName = metadata.serverName;
|
|
315
|
+
const server = metadata.servers.find((s) => s.name === serverName);
|
|
316
|
+
if (!server) {
|
|
317
|
+
await ctx.answerCallbackQuery({ text: t("inline.inactive_callback"), show_alert: true });
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
const enable = server.status.status !== "connected";
|
|
321
|
+
await ctx.answerCallbackQuery({ text: enable ? t("mcps.enabling") : t("mcps.disabling") });
|
|
322
|
+
await toggleMcpServer(metadata.projectDirectory, serverName, enable);
|
|
323
|
+
const updatedServers = await getMcpServerList(metadata.projectDirectory);
|
|
324
|
+
const updatedServer = updatedServers.find((s) => s.name === serverName);
|
|
325
|
+
if (!updatedServer) {
|
|
326
|
+
await ctx.editMessageText(t("mcps.select"), {
|
|
327
|
+
reply_markup: buildMcpsListKeyboard(updatedServers),
|
|
328
|
+
});
|
|
329
|
+
interactionManager.transition({
|
|
330
|
+
expectedInput: "callback",
|
|
331
|
+
metadata: {
|
|
332
|
+
flow: "mcps",
|
|
333
|
+
stage: "list",
|
|
334
|
+
messageId: metadata.messageId,
|
|
335
|
+
projectDirectory: metadata.projectDirectory,
|
|
336
|
+
servers: updatedServers,
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
await ctx.editMessageText(buildMcpsDetailText(updatedServer), {
|
|
342
|
+
reply_markup: buildMcpsDetailKeyboard(updatedServer),
|
|
343
|
+
});
|
|
344
|
+
interactionManager.transition({
|
|
345
|
+
expectedInput: "callback",
|
|
346
|
+
metadata: {
|
|
347
|
+
flow: "mcps",
|
|
348
|
+
stage: "detail",
|
|
349
|
+
messageId: metadata.messageId,
|
|
350
|
+
projectDirectory: metadata.projectDirectory,
|
|
351
|
+
serverName: updatedServer.name,
|
|
352
|
+
servers: updatedServers,
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
return true;
|
|
356
|
+
}
|
|
357
|
+
if (data.startsWith(MCPS_CALLBACK_SELECT_PREFIX)) {
|
|
358
|
+
if (metadata.stage !== "list") {
|
|
359
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error"), show_alert: true });
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
const serverIndex = parseSelectIndex(data);
|
|
363
|
+
const server = serverIndex === null ? undefined : metadata.servers[serverIndex];
|
|
364
|
+
if (!server) {
|
|
365
|
+
await ctx.answerCallbackQuery({ text: t("inline.inactive_callback"), show_alert: true });
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
await ctx.answerCallbackQuery();
|
|
369
|
+
await ctx.editMessageText(buildMcpsDetailText(server), {
|
|
370
|
+
reply_markup: buildMcpsDetailKeyboard(server),
|
|
371
|
+
});
|
|
372
|
+
interactionManager.transition({
|
|
373
|
+
expectedInput: "callback",
|
|
374
|
+
metadata: {
|
|
375
|
+
flow: "mcps",
|
|
376
|
+
stage: "detail",
|
|
377
|
+
messageId: metadata.messageId,
|
|
378
|
+
projectDirectory: metadata.projectDirectory,
|
|
379
|
+
serverName: server.name,
|
|
380
|
+
servers: metadata.servers,
|
|
381
|
+
},
|
|
382
|
+
});
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
catch (error) {
|
|
389
|
+
logger.error("[Mcps] Error handling MCP callback:", error);
|
|
390
|
+
clearMcpsInteraction("mcps_callback_error");
|
|
391
|
+
await ctx.answerCallbackQuery({ text: t("mcps.toggle_error") }).catch(() => { });
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
}
|
package/dist/bot/commands/new.js
CHANGED
|
@@ -3,8 +3,6 @@ import { setCurrentSession } from "../../session/manager.js";
|
|
|
3
3
|
import { ingestSessionInfoForCache } from "../../session/cache-manager.js";
|
|
4
4
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
5
|
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
6
|
-
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
|
-
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
6
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
7
|
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
10
8
|
import { getStoredModel } from "../../model/manager.js";
|
|
@@ -13,7 +11,8 @@ import { createMainKeyboard } from "../utils/keyboard.js";
|
|
|
13
11
|
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
14
12
|
import { logger } from "../../utils/logger.js";
|
|
15
13
|
import { t } from "../../i18n/index.js";
|
|
16
|
-
|
|
14
|
+
import { attachToSession } from "../../attach/service.js";
|
|
15
|
+
export async function newCommand(ctx, deps) {
|
|
17
16
|
try {
|
|
18
17
|
if (isForegroundBusy()) {
|
|
19
18
|
await replyBusyBlocked(ctx);
|
|
@@ -38,27 +37,20 @@ export async function newCommand(ctx) {
|
|
|
38
37
|
directory: currentProject.worktree,
|
|
39
38
|
};
|
|
40
39
|
setCurrentSession(sessionInfo);
|
|
41
|
-
summaryAggregator.clear();
|
|
42
40
|
clearAllInteractionState("session_created");
|
|
43
41
|
await ingestSessionInfoForCache(session);
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
try {
|
|
51
|
-
await pinnedMessageManager.onSessionChange(session.id, session.title);
|
|
52
|
-
}
|
|
53
|
-
catch (err) {
|
|
54
|
-
logger.error("[Bot] Error creating pinned message:", err);
|
|
55
|
-
}
|
|
42
|
+
await attachToSession({
|
|
43
|
+
bot: deps.bot,
|
|
44
|
+
chatId: ctx.chat.id,
|
|
45
|
+
session: sessionInfo,
|
|
46
|
+
ensureEventSubscription: deps.ensureEventSubscription,
|
|
47
|
+
});
|
|
56
48
|
// Get current state for keyboard
|
|
57
49
|
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
58
50
|
const currentModel = getStoredModel();
|
|
59
|
-
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
60
|
-
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
61
51
|
keyboardManager.updateAgent(currentAgent);
|
|
52
|
+
const contextInfo = keyboardManager.getContextInfo();
|
|
53
|
+
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
62
54
|
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
|
|
63
55
|
await ctx.reply(t("new.created", { title: session.title }), {
|
|
64
56
|
reply_markup: keyboard,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { config } from "../../config.js";
|
|
3
2
|
import { opencodeClient } from "../../opencode/client.js";
|
|
4
|
-
import { resolveLocalOpencodeTarget } from "../../opencode/process.js";
|
|
3
|
+
import { resolveLocalOpencodeTarget, startLocalOpencodeServer } from "../../opencode/process.js";
|
|
5
4
|
import { logger } from "../../utils/logger.js";
|
|
6
5
|
import { t } from "../../i18n/index.js";
|
|
7
6
|
import { editBotText } from "../utils/telegram-text.js";
|
|
@@ -50,14 +49,7 @@ export async function opencodeStartCommand(ctx) {
|
|
|
50
49
|
// Server not accessible, continue with start.
|
|
51
50
|
}
|
|
52
51
|
const statusMessage = await ctx.reply(t("opencode_start.starting"));
|
|
53
|
-
const
|
|
54
|
-
const command = isWindows ? "cmd.exe" : "opencode";
|
|
55
|
-
const args = isWindows ? ["/c", "opencode", "serve"] : ["serve"];
|
|
56
|
-
const childProcess = spawn(command, args, {
|
|
57
|
-
detached: true,
|
|
58
|
-
stdio: "ignore",
|
|
59
|
-
windowsHide: isWindows,
|
|
60
|
-
});
|
|
52
|
+
const childProcess = startLocalOpencodeServer(localTarget);
|
|
61
53
|
childProcess.once("error", (error) => {
|
|
62
54
|
logger.error("[Bot] OpenCode server process failed to start", error);
|
|
63
55
|
});
|
|
@@ -4,8 +4,6 @@ import { resolveProjectAgent } from "../../agent/manager.js";
|
|
|
4
4
|
import { setCurrentSession } from "../../session/manager.js";
|
|
5
5
|
import { getCurrentProject } from "../../settings/manager.js";
|
|
6
6
|
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
7
|
-
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
8
|
-
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
9
7
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
10
8
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
11
9
|
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
@@ -13,6 +11,7 @@ import { logger } from "../../utils/logger.js";
|
|
|
13
11
|
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
14
12
|
import { config } from "../../config.js";
|
|
15
13
|
import { getDateLocale, t } from "../../i18n/index.js";
|
|
14
|
+
import { attachToSession } from "../../attach/service.js";
|
|
16
15
|
const SESSION_CALLBACK_PREFIX = "session:";
|
|
17
16
|
const SESSION_PAGE_CALLBACK_PREFIX = "session:page:";
|
|
18
17
|
const SESSION_FETCH_EXTRA_COUNT = 1;
|
|
@@ -120,7 +119,7 @@ export async function sessionsCommand(ctx) {
|
|
|
120
119
|
await ctx.reply(t("sessions.fetch_error"));
|
|
121
120
|
}
|
|
122
121
|
}
|
|
123
|
-
export async function handleSessionSelect(ctx) {
|
|
122
|
+
export async function handleSessionSelect(ctx, deps) {
|
|
124
123
|
const callbackQuery = ctx.callbackQuery;
|
|
125
124
|
if (!callbackQuery?.data || !callbackQuery.data.startsWith(SESSION_CALLBACK_PREFIX)) {
|
|
126
125
|
return false;
|
|
@@ -182,7 +181,6 @@ export async function handleSessionSelect(ctx) {
|
|
|
182
181
|
directory: currentProject.worktree,
|
|
183
182
|
};
|
|
184
183
|
setCurrentSession(sessionInfo);
|
|
185
|
-
summaryAggregator.clear();
|
|
186
184
|
clearAllInteractionState("session_switched");
|
|
187
185
|
await ctx.answerCallbackQuery();
|
|
188
186
|
let loadingMessageId = null;
|
|
@@ -195,30 +193,31 @@ export async function handleSessionSelect(ctx) {
|
|
|
195
193
|
logger.error("[Sessions] Failed to send loading message:", err);
|
|
196
194
|
}
|
|
197
195
|
}
|
|
198
|
-
// Initialize pinned message manager if not already
|
|
199
|
-
if (!pinnedMessageManager.isInitialized() && ctx.chat) {
|
|
200
|
-
pinnedMessageManager.initialize(ctx.api, ctx.chat.id);
|
|
201
|
-
}
|
|
202
|
-
// Initialize keyboard manager if not already
|
|
203
|
-
if (ctx.chat) {
|
|
204
|
-
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
205
|
-
}
|
|
206
196
|
try {
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
197
|
+
await attachToSession({
|
|
198
|
+
bot: deps.bot,
|
|
199
|
+
chatId: ctx.chat.id,
|
|
200
|
+
session: sessionInfo,
|
|
201
|
+
ensureEventSubscription: deps.ensureEventSubscription,
|
|
202
|
+
});
|
|
212
203
|
}
|
|
213
204
|
catch (err) {
|
|
214
|
-
|
|
205
|
+
if (loadingMessageId) {
|
|
206
|
+
try {
|
|
207
|
+
await ctx.api.deleteMessage(ctx.chat.id, loadingMessageId);
|
|
208
|
+
}
|
|
209
|
+
catch (deleteError) {
|
|
210
|
+
logger.debug("[Sessions] Failed to delete loading message after follow error:", deleteError);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
logger.error("[Sessions] Error following selected session:", err);
|
|
214
|
+
throw err;
|
|
215
215
|
}
|
|
216
216
|
if (ctx.chat) {
|
|
217
217
|
const chatId = ctx.chat.id;
|
|
218
218
|
const currentAgent = await resolveProjectAgent();
|
|
219
219
|
keyboardManager.updateAgent(currentAgent);
|
|
220
|
-
|
|
221
|
-
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
220
|
+
const contextInfo = keyboardManager.getContextInfo();
|
|
222
221
|
if (contextInfo) {
|
|
223
222
|
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
224
223
|
}
|
|
@@ -10,6 +10,7 @@ import { foregroundSessionState } from "../../scheduled-task/foreground-state.js
|
|
|
10
10
|
import { abortCurrentOperation } from "./abort.js";
|
|
11
11
|
import { t } from "../../i18n/index.js";
|
|
12
12
|
import { assistantRunState } from "../assistant-run-state.js";
|
|
13
|
+
import { detachAttachedSession } from "../../attach/service.js";
|
|
13
14
|
export async function startCommand(ctx) {
|
|
14
15
|
if (ctx.chat) {
|
|
15
16
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -18,6 +19,7 @@ export async function startCommand(ctx) {
|
|
|
18
19
|
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
19
20
|
}
|
|
20
21
|
await abortCurrentOperation(ctx, { notifyUser: false });
|
|
22
|
+
detachAttachedSession("start_command_reset");
|
|
21
23
|
foregroundSessionState.clearAll("start_command_reset");
|
|
22
24
|
assistantRunState.clearAll("start_command_reset");
|
|
23
25
|
clearSession();
|