zardbot-telegram 1.0.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 +116 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/agent/manager.js +88 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +49 -0
- package/dist/bot/commands/abort.js +121 -0
- package/dist/bot/commands/commands.js +480 -0
- package/dist/bot/commands/definitions.js +27 -0
- package/dist/bot/commands/help.js +10 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +70 -0
- package/dist/bot/commands/opencode-start.js +101 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +223 -0
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +351 -0
- package/dist/bot/commands/start.js +43 -0
- package/dist/bot/commands/status.js +95 -0
- package/dist/bot/commands/task.js +399 -0
- package/dist/bot/commands/tasklist.js +220 -0
- package/dist/bot/commands/voice.js +145 -0
- package/dist/bot/handlers/agent.js +118 -0
- package/dist/bot/handlers/context.js +100 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +143 -0
- package/dist/bot/handlers/permission.js +235 -0
- package/dist/bot/handlers/prompt.js +240 -0
- package/dist/bot/handlers/question.js +390 -0
- package/dist/bot/handlers/tts.js +89 -0
- package/dist/bot/handlers/variant.js +138 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +977 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +95 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/streaming/response-streamer.js +286 -0
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/finalize-assistant-response.js +52 -0
- package/dist/bot/utils/keyboard.js +69 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
- package/dist/bot/utils/telegram-text.js +28 -0
- package/dist/bot/utils/thinking-message.js +8 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +97 -0
- package/dist/i18n/de.js +357 -0
- package/dist/i18n/en.js +357 -0
- package/dist/i18n/es.js +357 -0
- package/dist/i18n/fr.js +357 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/ru.js +357 -0
- package/dist/i18n/zh.js +357 -0
- package/dist/index.js +26 -0
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/cleanup.js +32 -0
- package/dist/interaction/guard.js +140 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +172 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/model/manager.js +259 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +140 -0
- package/dist/permission/manager.js +100 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +682 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +176 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +53 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/scheduled-task/creation-manager.js +113 -0
- package/dist/scheduled-task/display.js +239 -0
- package/dist/scheduled-task/executor.js +87 -0
- package/dist/scheduled-task/foreground-state.js +32 -0
- package/dist/scheduled-task/next-run.js +207 -0
- package/dist/scheduled-task/runtime.js +368 -0
- package/dist/scheduled-task/schedule-parser.js +169 -0
- package/dist/scheduled-task/store.js +65 -0
- package/dist/scheduled-task/types.js +19 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +10 -0
- package/dist/settings/manager.js +158 -0
- package/dist/stt/client.js +97 -0
- package/dist/summary/aggregator.js +1136 -0
- package/dist/summary/formatter.js +491 -0
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +90 -0
- package/dist/tts/client.js +130 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +127 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { InlineKeyboard } from "grammy";
|
|
2
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
3
|
+
import { setCurrentSession } from "../../session/manager.js";
|
|
4
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
5
|
+
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
6
|
+
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
|
+
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
10
|
+
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
11
|
+
import { logger } from "../../utils/logger.js";
|
|
12
|
+
import { safeBackgroundTask } from "../../utils/safe-background-task.js";
|
|
13
|
+
import { config } from "../../config.js";
|
|
14
|
+
import { getDateLocale, t } from "../../i18n/index.js";
|
|
15
|
+
const SESSION_CALLBACK_PREFIX = "session:";
|
|
16
|
+
const SESSION_PAGE_CALLBACK_PREFIX = "session:page:";
|
|
17
|
+
const SESSION_FETCH_EXTRA_COUNT = 1;
|
|
18
|
+
function buildSessionPageCallback(page) {
|
|
19
|
+
return `${SESSION_PAGE_CALLBACK_PREFIX}${page}`;
|
|
20
|
+
}
|
|
21
|
+
function parseSessionPageCallback(data) {
|
|
22
|
+
if (!data.startsWith(SESSION_PAGE_CALLBACK_PREFIX)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
const rawPage = data.slice(SESSION_PAGE_CALLBACK_PREFIX.length);
|
|
26
|
+
const page = Number(rawPage);
|
|
27
|
+
if (!Number.isInteger(page) || page < 0) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return page;
|
|
31
|
+
}
|
|
32
|
+
function parseSessionIdCallback(data) {
|
|
33
|
+
if (!data.startsWith(SESSION_CALLBACK_PREFIX)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
if (data.startsWith(SESSION_PAGE_CALLBACK_PREFIX)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const sessionId = data.slice(SESSION_CALLBACK_PREFIX.length);
|
|
40
|
+
return sessionId.length > 0 ? sessionId : null;
|
|
41
|
+
}
|
|
42
|
+
function formatSessionsSelectText(page) {
|
|
43
|
+
if (page === 0) {
|
|
44
|
+
return t("sessions.select");
|
|
45
|
+
}
|
|
46
|
+
return t("sessions.select_page", { page: page + 1 });
|
|
47
|
+
}
|
|
48
|
+
async function loadSessionPage(directory, page, pageSize) {
|
|
49
|
+
const startIndex = page * pageSize;
|
|
50
|
+
const endExclusive = startIndex + pageSize;
|
|
51
|
+
const { data: sessions, error } = await opencodeClient.session.list({
|
|
52
|
+
directory,
|
|
53
|
+
limit: endExclusive + SESSION_FETCH_EXTRA_COUNT,
|
|
54
|
+
roots: true,
|
|
55
|
+
});
|
|
56
|
+
if (error || !sessions) {
|
|
57
|
+
throw error || new Error("No data received from server");
|
|
58
|
+
}
|
|
59
|
+
const hasNext = sessions.length > endExclusive;
|
|
60
|
+
const pagedSessions = sessions.slice(startIndex, endExclusive);
|
|
61
|
+
logger.debug(`[Sessions] Loaded page=${page + 1}, startIndex=${startIndex}, endExclusive=${endExclusive}, pageSize=${pageSize}, items=${pagedSessions.length}, hasNext=${hasNext}`);
|
|
62
|
+
return {
|
|
63
|
+
sessions: pagedSessions,
|
|
64
|
+
hasNext,
|
|
65
|
+
page,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function buildSessionsKeyboard(pageData, pageSize) {
|
|
69
|
+
const keyboard = new InlineKeyboard();
|
|
70
|
+
const localeForDate = getDateLocale();
|
|
71
|
+
const pageStartIndex = pageData.page * pageSize;
|
|
72
|
+
pageData.sessions.forEach((session, index) => {
|
|
73
|
+
const date = new Date(session.time.created).toLocaleDateString(localeForDate);
|
|
74
|
+
const label = `${pageStartIndex + index + 1}. ${session.title} (${date})`;
|
|
75
|
+
keyboard.text(label, `${SESSION_CALLBACK_PREFIX}${session.id}`).row();
|
|
76
|
+
});
|
|
77
|
+
if (pageData.page > 0) {
|
|
78
|
+
keyboard.text(t("sessions.button.prev_page"), buildSessionPageCallback(pageData.page - 1));
|
|
79
|
+
}
|
|
80
|
+
if (pageData.hasNext) {
|
|
81
|
+
keyboard.text(t("sessions.button.next_page"), buildSessionPageCallback(pageData.page + 1));
|
|
82
|
+
}
|
|
83
|
+
if (pageData.page > 0 || pageData.hasNext) {
|
|
84
|
+
keyboard.row();
|
|
85
|
+
}
|
|
86
|
+
return keyboard;
|
|
87
|
+
}
|
|
88
|
+
export async function sessionsCommand(ctx) {
|
|
89
|
+
try {
|
|
90
|
+
if (isForegroundBusy()) {
|
|
91
|
+
await replyBusyBlocked(ctx);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const pageSize = config.bot.sessionsListLimit;
|
|
95
|
+
const currentProject = getCurrentProject();
|
|
96
|
+
if (!currentProject) {
|
|
97
|
+
await ctx.reply(t("sessions.project_not_selected"));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
logger.debug(`[Sessions] Fetching sessions for directory: ${currentProject.worktree}`);
|
|
101
|
+
const firstPage = await loadSessionPage(currentProject.worktree, 0, pageSize);
|
|
102
|
+
logger.debug(`[Sessions] Found ${firstPage.sessions.length} sessions on page 1`);
|
|
103
|
+
firstPage.sessions.forEach((session) => {
|
|
104
|
+
logger.debug(`[Sessions] Session: ${session.title} | ${session.directory}`);
|
|
105
|
+
});
|
|
106
|
+
if (firstPage.sessions.length === 0) {
|
|
107
|
+
await ctx.reply(t("sessions.empty"));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const keyboard = buildSessionsKeyboard(firstPage, pageSize);
|
|
111
|
+
await replyWithInlineMenu(ctx, {
|
|
112
|
+
menuKind: "session",
|
|
113
|
+
text: formatSessionsSelectText(firstPage.page),
|
|
114
|
+
keyboard,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error("[Sessions] Error fetching sessions:", error);
|
|
119
|
+
await ctx.reply(t("sessions.fetch_error"));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
export async function handleSessionSelect(ctx) {
|
|
123
|
+
const callbackQuery = ctx.callbackQuery;
|
|
124
|
+
if (!callbackQuery?.data || !callbackQuery.data.startsWith(SESSION_CALLBACK_PREFIX)) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
if (isForegroundBusy()) {
|
|
128
|
+
await replyBusyBlocked(ctx);
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
const page = parseSessionPageCallback(callbackQuery.data);
|
|
132
|
+
const sessionId = parseSessionIdCallback(callbackQuery.data);
|
|
133
|
+
const isActiveMenu = await ensureActiveInlineMenu(ctx, "session");
|
|
134
|
+
if (!isActiveMenu) {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const currentProject = getCurrentProject();
|
|
139
|
+
if (!currentProject) {
|
|
140
|
+
clearAllInteractionState("session_select_project_missing");
|
|
141
|
+
await ctx.answerCallbackQuery();
|
|
142
|
+
await ctx.reply(t("sessions.select_project_first"));
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
if (page !== null) {
|
|
146
|
+
try {
|
|
147
|
+
const pageSize = config.bot.sessionsListLimit;
|
|
148
|
+
const pageData = await loadSessionPage(currentProject.worktree, page, pageSize);
|
|
149
|
+
if (pageData.sessions.length === 0) {
|
|
150
|
+
await ctx.answerCallbackQuery({ text: t("sessions.page_empty_callback") });
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
const keyboard = buildSessionsKeyboard(pageData, pageSize);
|
|
154
|
+
appendInlineMenuCancelButton(keyboard, "session");
|
|
155
|
+
await ctx.editMessageText(formatSessionsSelectText(pageData.page), {
|
|
156
|
+
reply_markup: keyboard,
|
|
157
|
+
});
|
|
158
|
+
await ctx.answerCallbackQuery();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
logger.error("[Sessions] Error loading sessions page:", error);
|
|
162
|
+
await ctx.answerCallbackQuery({ text: t("sessions.page_load_error_callback") });
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
if (!sessionId) {
|
|
167
|
+
await ctx.answerCallbackQuery({ text: t("callback.processing_error") });
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
const { data: session, error } = await opencodeClient.session.get({
|
|
171
|
+
sessionID: sessionId,
|
|
172
|
+
directory: currentProject.worktree,
|
|
173
|
+
});
|
|
174
|
+
if (error || !session) {
|
|
175
|
+
throw error || new Error("Failed to get session details");
|
|
176
|
+
}
|
|
177
|
+
logger.info(`[Bot] Session selected: id=${session.id}, title="${session.title}", project=${currentProject.worktree}`);
|
|
178
|
+
const sessionInfo = {
|
|
179
|
+
id: session.id,
|
|
180
|
+
title: session.title,
|
|
181
|
+
directory: currentProject.worktree,
|
|
182
|
+
};
|
|
183
|
+
setCurrentSession(sessionInfo);
|
|
184
|
+
summaryAggregator.clear();
|
|
185
|
+
clearAllInteractionState("session_switched");
|
|
186
|
+
await ctx.answerCallbackQuery();
|
|
187
|
+
let loadingMessageId = null;
|
|
188
|
+
if (ctx.chat) {
|
|
189
|
+
try {
|
|
190
|
+
const loadingMessage = await ctx.api.sendMessage(ctx.chat.id, t("sessions.loading_context"));
|
|
191
|
+
loadingMessageId = loadingMessage.message_id;
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
logger.error("[Sessions] Failed to send loading message:", err);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// Initialize pinned message manager if not already
|
|
198
|
+
if (!pinnedMessageManager.isInitialized() && ctx.chat) {
|
|
199
|
+
pinnedMessageManager.initialize(ctx.api, ctx.chat.id);
|
|
200
|
+
}
|
|
201
|
+
// Initialize keyboard manager if not already
|
|
202
|
+
if (ctx.chat) {
|
|
203
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
// Create new pinned message for this session
|
|
207
|
+
await pinnedMessageManager.onSessionChange(session.id, session.title);
|
|
208
|
+
// Load context from session history (for existing sessions)
|
|
209
|
+
// Wait for it to complete so keyboard has correct context
|
|
210
|
+
await pinnedMessageManager.loadContextFromHistory(session.id, currentProject.worktree);
|
|
211
|
+
}
|
|
212
|
+
catch (err) {
|
|
213
|
+
logger.error("[Bot] Error initializing pinned message:", err);
|
|
214
|
+
}
|
|
215
|
+
if (ctx.chat) {
|
|
216
|
+
const chatId = ctx.chat.id;
|
|
217
|
+
// Update keyboard with loaded context (callback executes async via setImmediate, so update manually)
|
|
218
|
+
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
219
|
+
if (contextInfo) {
|
|
220
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
221
|
+
}
|
|
222
|
+
// Delete loading message
|
|
223
|
+
if (loadingMessageId) {
|
|
224
|
+
try {
|
|
225
|
+
await ctx.api.deleteMessage(chatId, loadingMessageId);
|
|
226
|
+
}
|
|
227
|
+
catch (err) {
|
|
228
|
+
logger.debug("[Sessions] Failed to delete loading message:", err);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// Send session selection confirmation with updated keyboard
|
|
232
|
+
const keyboard = keyboardManager.getKeyboard();
|
|
233
|
+
try {
|
|
234
|
+
await ctx.api.sendMessage(chatId, t("sessions.selected", { title: session.title }), {
|
|
235
|
+
reply_markup: keyboard,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
logger.error("[Sessions] Failed to send selection message:", err);
|
|
240
|
+
}
|
|
241
|
+
// Send preview asynchronously
|
|
242
|
+
safeBackgroundTask({
|
|
243
|
+
taskName: "sessions.sendPreview",
|
|
244
|
+
task: () => sendSessionPreview(ctx.api, chatId, null, session.title, session.id, currentProject.worktree),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
await ctx.deleteMessage();
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
clearAllInteractionState("session_select_error");
|
|
251
|
+
logger.error("[Sessions] Error selecting session:", error);
|
|
252
|
+
await ctx.answerCallbackQuery();
|
|
253
|
+
await ctx.reply(t("sessions.select_error"));
|
|
254
|
+
}
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
const PREVIEW_MESSAGES_LIMIT = 6;
|
|
258
|
+
const PREVIEW_ITEM_MAX_LENGTH = 420;
|
|
259
|
+
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
|
260
|
+
function extractTextParts(parts) {
|
|
261
|
+
const textParts = parts
|
|
262
|
+
.filter((part) => part.type === "text" && typeof part.text === "string")
|
|
263
|
+
.map((part) => part.text);
|
|
264
|
+
if (textParts.length === 0) {
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
const text = textParts.join("").trim();
|
|
268
|
+
return text.length > 0 ? text : null;
|
|
269
|
+
}
|
|
270
|
+
function truncateText(text, maxLength) {
|
|
271
|
+
if (text.length <= maxLength) {
|
|
272
|
+
return text;
|
|
273
|
+
}
|
|
274
|
+
const clipped = text.slice(0, Math.max(0, maxLength - 3)).trimEnd();
|
|
275
|
+
return `${clipped}...`;
|
|
276
|
+
}
|
|
277
|
+
async function loadSessionPreview(sessionId, directory) {
|
|
278
|
+
try {
|
|
279
|
+
const { data: messages, error } = await opencodeClient.session.messages({
|
|
280
|
+
sessionID: sessionId,
|
|
281
|
+
directory,
|
|
282
|
+
limit: PREVIEW_MESSAGES_LIMIT,
|
|
283
|
+
});
|
|
284
|
+
if (error || !messages) {
|
|
285
|
+
logger.warn("[Sessions] Failed to fetch session messages:", error);
|
|
286
|
+
return [];
|
|
287
|
+
}
|
|
288
|
+
const items = messages
|
|
289
|
+
.map(({ info, parts }) => {
|
|
290
|
+
const role = info.role;
|
|
291
|
+
if (role !== "user" && role !== "assistant") {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (role === "assistant" && info.summary) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
const text = extractTextParts(parts);
|
|
298
|
+
if (!text) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const created = info.time?.created ?? 0;
|
|
302
|
+
return {
|
|
303
|
+
role,
|
|
304
|
+
text: truncateText(text, PREVIEW_ITEM_MAX_LENGTH),
|
|
305
|
+
created,
|
|
306
|
+
};
|
|
307
|
+
})
|
|
308
|
+
.filter((item) => Boolean(item));
|
|
309
|
+
return items.sort((a, b) => a.created - b.created);
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
logger.error("[Sessions] Error loading session preview:", err);
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
function formatSessionPreview(_sessionTitle, items) {
|
|
317
|
+
const lines = [];
|
|
318
|
+
if (items.length === 0) {
|
|
319
|
+
lines.push(t("sessions.preview.empty"));
|
|
320
|
+
return lines.join("\n");
|
|
321
|
+
}
|
|
322
|
+
lines.push(t("sessions.preview.title"));
|
|
323
|
+
items.forEach((item, index) => {
|
|
324
|
+
const label = item.role === "user" ? t("sessions.preview.you") : t("sessions.preview.agent");
|
|
325
|
+
lines.push(`${label} ${item.text}`);
|
|
326
|
+
if (index < items.length - 1) {
|
|
327
|
+
lines.push("");
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
const rawMessage = lines.join("\n");
|
|
331
|
+
return truncateText(rawMessage, TELEGRAM_MESSAGE_LIMIT);
|
|
332
|
+
}
|
|
333
|
+
async function sendSessionPreview(api, chatId, messageId, sessionTitle, sessionId, directory) {
|
|
334
|
+
const previewItems = await loadSessionPreview(sessionId, directory);
|
|
335
|
+
const finalText = formatSessionPreview(sessionTitle, previewItems);
|
|
336
|
+
if (messageId) {
|
|
337
|
+
try {
|
|
338
|
+
await api.editMessageText(chatId, messageId, finalText);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
catch (err) {
|
|
342
|
+
logger.warn("[Sessions] Failed to edit preview message, sending new one:", err);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
await api.sendMessage(chatId, finalText);
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
logger.error("[Sessions] Failed to send session preview message:", err);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
2
|
+
import { getStoredAgent } from "../../agent/manager.js";
|
|
3
|
+
import { getStoredModel } from "../../model/manager.js";
|
|
4
|
+
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
6
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
|
+
import { clearSession } from "../../session/manager.js";
|
|
8
|
+
import { clearProject } from "../../settings/manager.js";
|
|
9
|
+
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
10
|
+
import { abortCurrentOperation } from "./abort.js";
|
|
11
|
+
import { t } from "../../i18n/index.js";
|
|
12
|
+
export async function startCommand(ctx) {
|
|
13
|
+
if (ctx.chat) {
|
|
14
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
15
|
+
pinnedMessageManager.initialize(ctx.api, ctx.chat.id);
|
|
16
|
+
}
|
|
17
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
18
|
+
}
|
|
19
|
+
await abortCurrentOperation(ctx, { notifyUser: false });
|
|
20
|
+
foregroundSessionState.clearAll("start_command_reset");
|
|
21
|
+
clearSession();
|
|
22
|
+
clearProject();
|
|
23
|
+
keyboardManager.clearContext();
|
|
24
|
+
await pinnedMessageManager.clear();
|
|
25
|
+
if (pinnedMessageManager.getContextLimit() === 0) {
|
|
26
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
27
|
+
}
|
|
28
|
+
// Get current agent, model, and context
|
|
29
|
+
const currentAgent = getStoredAgent();
|
|
30
|
+
const currentModel = getStoredModel();
|
|
31
|
+
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
32
|
+
const contextInfo = pinnedMessageManager.getContextInfo() ??
|
|
33
|
+
(pinnedMessageManager.getContextLimit() > 0
|
|
34
|
+
? { tokensUsed: 0, tokensLimit: pinnedMessageManager.getContextLimit() }
|
|
35
|
+
: null);
|
|
36
|
+
keyboardManager.updateAgent(currentAgent);
|
|
37
|
+
keyboardManager.updateModel(currentModel);
|
|
38
|
+
if (contextInfo) {
|
|
39
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
40
|
+
}
|
|
41
|
+
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo ?? undefined, variantName);
|
|
42
|
+
await ctx.reply(t("start.welcome"), { reply_markup: keyboard });
|
|
43
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { opencodeClient } from "../../opencode/client.js";
|
|
2
|
+
import { getCurrentSession } from "../../session/manager.js";
|
|
3
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
4
|
+
import { fetchCurrentAgent } from "../../agent/manager.js";
|
|
5
|
+
import { getAgentDisplayName } from "../../agent/types.js";
|
|
6
|
+
import { fetchCurrentModel } from "../../model/manager.js";
|
|
7
|
+
import { processManager } from "../../process/manager.js";
|
|
8
|
+
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
|
+
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
10
|
+
import { logger } from "../../utils/logger.js";
|
|
11
|
+
import { t } from "../../i18n/index.js";
|
|
12
|
+
import { sendBotText } from "../utils/telegram-text.js";
|
|
13
|
+
export async function statusCommand(ctx) {
|
|
14
|
+
try {
|
|
15
|
+
const { data, error } = await opencodeClient.global.health();
|
|
16
|
+
if (error || !data) {
|
|
17
|
+
throw error || new Error("No data received from server");
|
|
18
|
+
}
|
|
19
|
+
let message = `${t("status.header_running")}\n\n`;
|
|
20
|
+
const healthLabel = data.healthy ? t("status.health.healthy") : t("status.health.unhealthy");
|
|
21
|
+
message += `${t("status.line.health", { health: healthLabel })}\n`;
|
|
22
|
+
if (data.version) {
|
|
23
|
+
message += `${t("status.line.version", { version: data.version })}\n`;
|
|
24
|
+
}
|
|
25
|
+
// Add process management information
|
|
26
|
+
if (processManager.isRunning()) {
|
|
27
|
+
const uptime = processManager.getUptime();
|
|
28
|
+
const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
|
|
29
|
+
message += `${t("status.line.managed_yes")}\n`;
|
|
30
|
+
message += `${t("status.line.pid", { pid: processManager.getPID() ?? "-" })}\n`;
|
|
31
|
+
message += `${t("status.line.uptime_sec", { seconds: uptimeStr })}\n`;
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
message += `${t("status.line.managed_no")}\n`;
|
|
35
|
+
}
|
|
36
|
+
// Add agent mode information
|
|
37
|
+
const currentAgent = await fetchCurrentAgent();
|
|
38
|
+
const agentDisplay = currentAgent
|
|
39
|
+
? getAgentDisplayName(currentAgent)
|
|
40
|
+
: t("status.agent_not_set");
|
|
41
|
+
message += `${t("status.line.mode", { mode: agentDisplay })}\n`;
|
|
42
|
+
// Add model information
|
|
43
|
+
const currentModel = fetchCurrentModel();
|
|
44
|
+
const modelDisplay = `🤖 ${currentModel.providerID}/${currentModel.modelID}`;
|
|
45
|
+
message += `${t("status.line.model", { model: modelDisplay })}\n`;
|
|
46
|
+
const currentProject = getCurrentProject();
|
|
47
|
+
if (currentProject) {
|
|
48
|
+
const projectName = currentProject.name || currentProject.worktree;
|
|
49
|
+
message += `\n${t("status.project_selected", { project: projectName })}\n`;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
message += `\n${t("status.project_not_selected")}\n`;
|
|
53
|
+
message += t("status.project_hint");
|
|
54
|
+
}
|
|
55
|
+
const currentSession = getCurrentSession();
|
|
56
|
+
if (currentSession) {
|
|
57
|
+
message += `\n${t("status.session_selected", { title: currentSession.title })}\n`;
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
message += `\n${t("status.session_not_selected")}\n`;
|
|
61
|
+
message += t("status.session_hint");
|
|
62
|
+
}
|
|
63
|
+
if (ctx.chat) {
|
|
64
|
+
if (!pinnedMessageManager.isInitialized()) {
|
|
65
|
+
pinnedMessageManager.initialize(ctx.api, ctx.chat.id);
|
|
66
|
+
}
|
|
67
|
+
// Fetch context limit if not yet loaded (e.g. fresh bot start)
|
|
68
|
+
if (pinnedMessageManager.getContextLimit() === 0) {
|
|
69
|
+
await pinnedMessageManager.refreshContextLimit();
|
|
70
|
+
}
|
|
71
|
+
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
72
|
+
}
|
|
73
|
+
// Sync current context (tokens used + limit) into keyboard state
|
|
74
|
+
const contextInfo = pinnedMessageManager.getContextInfo();
|
|
75
|
+
if (contextInfo) {
|
|
76
|
+
keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
|
|
77
|
+
}
|
|
78
|
+
const keyboard = keyboardManager.getKeyboard();
|
|
79
|
+
if (ctx.chat) {
|
|
80
|
+
await sendBotText({
|
|
81
|
+
api: ctx.api,
|
|
82
|
+
chatId: ctx.chat.id,
|
|
83
|
+
text: message,
|
|
84
|
+
options: { reply_markup: keyboard },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await ctx.reply(message, { reply_markup: keyboard });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
logger.error("[Bot] Error checking server status:", error);
|
|
93
|
+
await ctx.reply(t("status.server_unavailable"));
|
|
94
|
+
}
|
|
95
|
+
}
|