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.
Files changed (111) hide show
  1. package/.env.example +116 -0
  2. package/LICENSE +21 -0
  3. package/README.md +250 -0
  4. package/dist/agent/manager.js +88 -0
  5. package/dist/agent/types.js +26 -0
  6. package/dist/app/start-bot-app.js +49 -0
  7. package/dist/bot/commands/abort.js +121 -0
  8. package/dist/bot/commands/commands.js +480 -0
  9. package/dist/bot/commands/definitions.js +27 -0
  10. package/dist/bot/commands/help.js +10 -0
  11. package/dist/bot/commands/models.js +38 -0
  12. package/dist/bot/commands/new.js +70 -0
  13. package/dist/bot/commands/opencode-start.js +101 -0
  14. package/dist/bot/commands/opencode-stop.js +44 -0
  15. package/dist/bot/commands/projects.js +223 -0
  16. package/dist/bot/commands/rename.js +139 -0
  17. package/dist/bot/commands/sessions.js +351 -0
  18. package/dist/bot/commands/start.js +43 -0
  19. package/dist/bot/commands/status.js +95 -0
  20. package/dist/bot/commands/task.js +399 -0
  21. package/dist/bot/commands/tasklist.js +220 -0
  22. package/dist/bot/commands/voice.js +145 -0
  23. package/dist/bot/handlers/agent.js +118 -0
  24. package/dist/bot/handlers/context.js +100 -0
  25. package/dist/bot/handlers/document.js +65 -0
  26. package/dist/bot/handlers/inline-menu.js +119 -0
  27. package/dist/bot/handlers/model.js +143 -0
  28. package/dist/bot/handlers/permission.js +235 -0
  29. package/dist/bot/handlers/prompt.js +240 -0
  30. package/dist/bot/handlers/question.js +390 -0
  31. package/dist/bot/handlers/tts.js +89 -0
  32. package/dist/bot/handlers/variant.js +138 -0
  33. package/dist/bot/handlers/voice.js +173 -0
  34. package/dist/bot/index.js +977 -0
  35. package/dist/bot/message-patterns.js +4 -0
  36. package/dist/bot/middleware/auth.js +30 -0
  37. package/dist/bot/middleware/interaction-guard.js +95 -0
  38. package/dist/bot/middleware/unknown-command.js +22 -0
  39. package/dist/bot/streaming/response-streamer.js +286 -0
  40. package/dist/bot/streaming/tool-call-streamer.js +285 -0
  41. package/dist/bot/utils/busy-guard.js +15 -0
  42. package/dist/bot/utils/commands.js +21 -0
  43. package/dist/bot/utils/file-download.js +91 -0
  44. package/dist/bot/utils/finalize-assistant-response.js +52 -0
  45. package/dist/bot/utils/keyboard.js +69 -0
  46. package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
  47. package/dist/bot/utils/telegram-text.js +28 -0
  48. package/dist/bot/utils/thinking-message.js +8 -0
  49. package/dist/cli/args.js +98 -0
  50. package/dist/cli.js +80 -0
  51. package/dist/config.js +97 -0
  52. package/dist/i18n/de.js +357 -0
  53. package/dist/i18n/en.js +357 -0
  54. package/dist/i18n/es.js +357 -0
  55. package/dist/i18n/fr.js +357 -0
  56. package/dist/i18n/index.js +109 -0
  57. package/dist/i18n/ru.js +357 -0
  58. package/dist/i18n/zh.js +357 -0
  59. package/dist/index.js +26 -0
  60. package/dist/interaction/busy.js +8 -0
  61. package/dist/interaction/cleanup.js +32 -0
  62. package/dist/interaction/guard.js +140 -0
  63. package/dist/interaction/manager.js +106 -0
  64. package/dist/interaction/types.js +1 -0
  65. package/dist/keyboard/manager.js +172 -0
  66. package/dist/keyboard/types.js +1 -0
  67. package/dist/model/capabilities.js +62 -0
  68. package/dist/model/context-limit.js +57 -0
  69. package/dist/model/manager.js +259 -0
  70. package/dist/model/types.js +24 -0
  71. package/dist/opencode/client.js +13 -0
  72. package/dist/opencode/events.js +140 -0
  73. package/dist/permission/manager.js +100 -0
  74. package/dist/permission/types.js +1 -0
  75. package/dist/pinned/format.js +29 -0
  76. package/dist/pinned/manager.js +682 -0
  77. package/dist/pinned/types.js +1 -0
  78. package/dist/process/manager.js +273 -0
  79. package/dist/process/types.js +1 -0
  80. package/dist/project/manager.js +88 -0
  81. package/dist/question/manager.js +176 -0
  82. package/dist/question/types.js +1 -0
  83. package/dist/rename/manager.js +53 -0
  84. package/dist/runtime/bootstrap.js +350 -0
  85. package/dist/runtime/mode.js +74 -0
  86. package/dist/runtime/paths.js +37 -0
  87. package/dist/scheduled-task/creation-manager.js +113 -0
  88. package/dist/scheduled-task/display.js +239 -0
  89. package/dist/scheduled-task/executor.js +87 -0
  90. package/dist/scheduled-task/foreground-state.js +32 -0
  91. package/dist/scheduled-task/next-run.js +207 -0
  92. package/dist/scheduled-task/runtime.js +368 -0
  93. package/dist/scheduled-task/schedule-parser.js +169 -0
  94. package/dist/scheduled-task/store.js +65 -0
  95. package/dist/scheduled-task/types.js +19 -0
  96. package/dist/session/cache-manager.js +455 -0
  97. package/dist/session/manager.js +10 -0
  98. package/dist/settings/manager.js +158 -0
  99. package/dist/stt/client.js +97 -0
  100. package/dist/summary/aggregator.js +1136 -0
  101. package/dist/summary/formatter.js +491 -0
  102. package/dist/summary/subagent-formatter.js +63 -0
  103. package/dist/summary/tool-message-batcher.js +90 -0
  104. package/dist/tts/client.js +130 -0
  105. package/dist/utils/error-format.js +29 -0
  106. package/dist/utils/logger.js +127 -0
  107. package/dist/utils/safe-background-task.js +33 -0
  108. package/dist/utils/telegram-rate-limit-retry.js +93 -0
  109. package/dist/variant/manager.js +103 -0
  110. package/dist/variant/types.js +1 -0
  111. package/package.json +79 -0
@@ -0,0 +1,101 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { processManager } from "../../process/manager.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
5
+ import { editBotText } from "../utils/telegram-text.js";
6
+ /**
7
+ * Wait for OpenCode server to become ready by polling health endpoint
8
+ * @param maxWaitMs Maximum time to wait in milliseconds
9
+ * @returns true if server became ready, false if timeout
10
+ */
11
+ async function waitForServerReady(maxWaitMs = 10000) {
12
+ const startTime = Date.now();
13
+ const pollInterval = 500;
14
+ while (Date.now() - startTime < maxWaitMs) {
15
+ try {
16
+ const { data, error } = await opencodeClient.global.health();
17
+ if (!error && data?.healthy) {
18
+ return true;
19
+ }
20
+ }
21
+ catch {
22
+ // Server not ready yet
23
+ }
24
+ await new Promise((resolve) => setTimeout(resolve, pollInterval));
25
+ }
26
+ return false;
27
+ }
28
+ /**
29
+ * Command handler for /opencode-start
30
+ * Starts the OpenCode server process
31
+ */
32
+ export async function opencodeStartCommand(ctx) {
33
+ try {
34
+ // 1. Check if process is already running under our management
35
+ if (processManager.isRunning()) {
36
+ const uptime = processManager.getUptime();
37
+ const uptimeStr = uptime ? Math.floor(uptime / 1000) : 0;
38
+ await ctx.reply(t("opencode_start.already_running_managed", {
39
+ pid: processManager.getPID() ?? "-",
40
+ seconds: uptimeStr,
41
+ }));
42
+ return;
43
+ }
44
+ // 2. Check if server is accessible (external process)
45
+ try {
46
+ const { data, error } = await opencodeClient.global.health();
47
+ if (!error && data?.healthy) {
48
+ await ctx.reply(t("opencode_start.already_running_external", {
49
+ version: data.version || t("common.unknown"),
50
+ }));
51
+ return;
52
+ }
53
+ }
54
+ catch {
55
+ // Server not accessible, continue with start
56
+ }
57
+ // 3. Notify user that we're starting the server
58
+ const statusMessage = await ctx.reply(t("opencode_start.starting"));
59
+ // 4. Start the process
60
+ const { success, error } = await processManager.start();
61
+ if (!success) {
62
+ await editBotText({
63
+ api: ctx.api,
64
+ chatId: ctx.chat.id,
65
+ messageId: statusMessage.message_id,
66
+ text: t("opencode_start.start_error", { error: error || t("common.unknown_error") }),
67
+ });
68
+ return;
69
+ }
70
+ // 5. Wait for server to become ready
71
+ logger.info("[Bot] Waiting for OpenCode server to become ready...");
72
+ const ready = await waitForServerReady(10000);
73
+ if (!ready) {
74
+ await editBotText({
75
+ api: ctx.api,
76
+ chatId: ctx.chat.id,
77
+ messageId: statusMessage.message_id,
78
+ text: t("opencode_start.started_not_ready", {
79
+ pid: processManager.getPID() ?? "-",
80
+ }),
81
+ });
82
+ return;
83
+ }
84
+ // 6. Get server version and send success message
85
+ const { data: health } = await opencodeClient.global.health();
86
+ await editBotText({
87
+ api: ctx.api,
88
+ chatId: ctx.chat.id,
89
+ messageId: statusMessage.message_id,
90
+ text: t("opencode_start.success", {
91
+ pid: processManager.getPID() ?? "-",
92
+ version: health?.version || t("common.unknown"),
93
+ }),
94
+ });
95
+ logger.info(`[Bot] OpenCode server started successfully, PID=${processManager.getPID()}`);
96
+ }
97
+ catch (err) {
98
+ logger.error("[Bot] Error in /opencode-start command:", err);
99
+ await ctx.reply(t("opencode_start.error"));
100
+ }
101
+ }
@@ -0,0 +1,44 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { processManager } from "../../process/manager.js";
3
+ import { logger } from "../../utils/logger.js";
4
+ import { t } from "../../i18n/index.js";
5
+ /**
6
+ * Command handler for /opencode-stop
7
+ * Stops the OpenCode server process
8
+ */
9
+ export async function opencodeStopCommand(ctx) {
10
+ try {
11
+ // 1. Check if process is running under our management
12
+ if (!processManager.isRunning()) {
13
+ // Check if there's an external server running
14
+ try {
15
+ const { data, error } = await opencodeClient.global.health();
16
+ if (!error && data?.healthy) {
17
+ await ctx.reply(t("opencode_stop.external_running"));
18
+ return;
19
+ }
20
+ }
21
+ catch {
22
+ // Server not accessible
23
+ }
24
+ await ctx.reply(t("opencode_stop.not_running"));
25
+ return;
26
+ }
27
+ // 2. Notify user that we're stopping the server
28
+ const pid = processManager.getPID();
29
+ const statusMessage = await ctx.reply(t("opencode_stop.stopping", { pid: pid ?? "-" }));
30
+ // 3. Stop the process
31
+ const { success, error } = await processManager.stop(5000);
32
+ if (!success) {
33
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_stop.stop_error", { error: error || t("common.unknown_error") }));
34
+ return;
35
+ }
36
+ // 4. Success - process has been stopped
37
+ await ctx.api.editMessageText(ctx.chat.id, statusMessage.message_id, t("opencode_stop.success"));
38
+ logger.info("[Bot] OpenCode server stopped successfully");
39
+ }
40
+ catch (err) {
41
+ logger.error("[Bot] Error in /opencode-stop command:", err);
42
+ await ctx.reply(t("opencode_stop.error"));
43
+ }
44
+ }
@@ -0,0 +1,223 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { setCurrentProject, getCurrentProject } from "../../settings/manager.js";
3
+ import { getProjects } from "../../project/manager.js";
4
+ import { syncSessionDirectoryCache } from "../../session/cache-manager.js";
5
+ import { clearSession } from "../../session/manager.js";
6
+ import { summaryAggregator } from "../../summary/aggregator.js";
7
+ import { pinnedMessageManager } from "../../pinned/manager.js";
8
+ import { keyboardManager } from "../../keyboard/manager.js";
9
+ import { getStoredAgent } from "../../agent/manager.js";
10
+ import { getStoredModel } from "../../model/manager.js";
11
+ import { formatVariantForButton } from "../../variant/manager.js";
12
+ import { clearAllInteractionState } from "../../interaction/cleanup.js";
13
+ import { createMainKeyboard } from "../utils/keyboard.js";
14
+ import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
15
+ import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
16
+ import { logger } from "../../utils/logger.js";
17
+ import { t } from "../../i18n/index.js";
18
+ import { config } from "../../config.js";
19
+ const MAX_INLINE_BUTTON_LABEL_LENGTH = 64;
20
+ const PROJECT_PAGE_CALLBACK_PREFIX = "projects:page:";
21
+ function formatProjectButtonLabel(label, isActive) {
22
+ const prefix = isActive ? "✅ " : "";
23
+ const availableLength = MAX_INLINE_BUTTON_LABEL_LENGTH - prefix.length;
24
+ if (label.length <= availableLength) {
25
+ return `${prefix}${label}`;
26
+ }
27
+ return `${prefix}${label.slice(0, Math.max(0, availableLength - 3))}...`;
28
+ }
29
+ export function getProjectFolderName(worktree) {
30
+ const normalized = worktree.replace(/[\\/]+$/g, "");
31
+ if (!normalized) {
32
+ return worktree;
33
+ }
34
+ const segments = normalized.split(/[\\/]/).filter(Boolean);
35
+ return segments.at(-1) ?? normalized;
36
+ }
37
+ export function buildProjectButtonLabel(index, worktree) {
38
+ const folderName = getProjectFolderName(worktree);
39
+ return `${index + 1}. ${folderName} [${worktree}]`;
40
+ }
41
+ export function parseProjectPageCallback(data) {
42
+ if (!data.startsWith(PROJECT_PAGE_CALLBACK_PREFIX)) {
43
+ return null;
44
+ }
45
+ const rawPage = data.slice(PROJECT_PAGE_CALLBACK_PREFIX.length);
46
+ if (!/^\d+$/.test(rawPage)) {
47
+ return null;
48
+ }
49
+ return Number.parseInt(rawPage, 10);
50
+ }
51
+ export function calculateProjectsPaginationRange(totalProjects, page, pageSize) {
52
+ const safePageSize = Math.max(1, pageSize);
53
+ const totalPages = Math.max(1, Math.ceil(totalProjects / safePageSize));
54
+ const normalizedPage = Math.min(Math.max(0, page), totalPages - 1);
55
+ const startIndex = normalizedPage * safePageSize;
56
+ const endIndex = Math.min(startIndex + safePageSize, totalProjects);
57
+ return {
58
+ page: normalizedPage,
59
+ totalPages,
60
+ startIndex,
61
+ endIndex,
62
+ };
63
+ }
64
+ function buildProjectsMenuText(currentProjectName, page, totalPages) {
65
+ const baseText = currentProjectName
66
+ ? t("projects.select_with_current", {
67
+ project: currentProjectName,
68
+ })
69
+ : t("projects.select");
70
+ if (totalPages <= 1) {
71
+ return baseText;
72
+ }
73
+ return `${baseText}\n\n${t("projects.page_indicator", {
74
+ current: String(page + 1),
75
+ total: String(totalPages),
76
+ })}`;
77
+ }
78
+ function buildProjectsKeyboard(projects, page) {
79
+ const keyboard = new InlineKeyboard();
80
+ const currentProject = getCurrentProject();
81
+ const pageSize = config.bot.projectsListLimit;
82
+ const { page: normalizedPage, totalPages, startIndex, endIndex, } = calculateProjectsPaginationRange(projects.length, page, pageSize);
83
+ projects.slice(startIndex, endIndex).forEach((project, index) => {
84
+ const isActive = currentProject &&
85
+ (project.id === currentProject.id || project.worktree === currentProject.worktree);
86
+ const label = buildProjectButtonLabel(startIndex + index, project.worktree);
87
+ const labelWithCheck = formatProjectButtonLabel(label, Boolean(isActive));
88
+ keyboard.text(labelWithCheck, `project:${project.id}`).row();
89
+ });
90
+ if (totalPages > 1) {
91
+ if (normalizedPage > 0) {
92
+ keyboard.text(t("projects.prev_page"), `${PROJECT_PAGE_CALLBACK_PREFIX}${normalizedPage - 1}`);
93
+ }
94
+ if (normalizedPage < totalPages - 1) {
95
+ keyboard.text(t("projects.next_page"), `${PROJECT_PAGE_CALLBACK_PREFIX}${normalizedPage + 1}`);
96
+ }
97
+ }
98
+ return keyboard;
99
+ }
100
+ function buildProjectsMenuView(projects, page) {
101
+ const currentProject = getCurrentProject();
102
+ const pageSize = config.bot.projectsListLimit;
103
+ const { page: normalizedPage, totalPages } = calculateProjectsPaginationRange(projects.length, page, pageSize);
104
+ const currentProjectName = currentProject?.name || currentProject?.worktree || null;
105
+ return {
106
+ text: buildProjectsMenuText(currentProjectName, normalizedPage, totalPages),
107
+ keyboard: buildProjectsKeyboard(projects, normalizedPage),
108
+ };
109
+ }
110
+ export async function projectsCommand(ctx) {
111
+ try {
112
+ if (isForegroundBusy()) {
113
+ await replyBusyBlocked(ctx);
114
+ return;
115
+ }
116
+ await syncSessionDirectoryCache();
117
+ const projects = await getProjects();
118
+ if (projects.length === 0) {
119
+ await ctx.reply(t("projects.empty"));
120
+ return;
121
+ }
122
+ const { text, keyboard } = buildProjectsMenuView(projects, 0);
123
+ await replyWithInlineMenu(ctx, {
124
+ menuKind: "project",
125
+ text,
126
+ keyboard,
127
+ });
128
+ }
129
+ catch (error) {
130
+ logger.error("[Bot] Error fetching projects:", error);
131
+ await ctx.reply(t("projects.fetch_error"));
132
+ }
133
+ }
134
+ export async function handleProjectSelect(ctx) {
135
+ const callbackQuery = ctx.callbackQuery;
136
+ if (!callbackQuery?.data) {
137
+ return false;
138
+ }
139
+ if (isForegroundBusy()) {
140
+ await replyBusyBlocked(ctx);
141
+ return true;
142
+ }
143
+ const page = parseProjectPageCallback(callbackQuery.data);
144
+ if (page !== null) {
145
+ const isActiveMenu = await ensureActiveInlineMenu(ctx, "project");
146
+ if (!isActiveMenu) {
147
+ return true;
148
+ }
149
+ try {
150
+ const projects = await getProjects();
151
+ if (projects.length === 0) {
152
+ await ctx.answerCallbackQuery();
153
+ await ctx.reply(t("projects.empty"));
154
+ return true;
155
+ }
156
+ const { text, keyboard } = buildProjectsMenuView(projects, page);
157
+ await ctx.answerCallbackQuery();
158
+ await ctx.editMessageText(text, {
159
+ reply_markup: appendInlineMenuCancelButton(keyboard, "project"),
160
+ });
161
+ }
162
+ catch (error) {
163
+ logger.error("[Bot] Error switching projects page:", error);
164
+ await ctx.answerCallbackQuery({ text: t("projects.page_load_error") });
165
+ }
166
+ return true;
167
+ }
168
+ if (!callbackQuery.data.startsWith("project:")) {
169
+ return false;
170
+ }
171
+ const projectId = callbackQuery.data.replace("project:", "");
172
+ const isActiveMenu = await ensureActiveInlineMenu(ctx, "project");
173
+ if (!isActiveMenu) {
174
+ return true;
175
+ }
176
+ try {
177
+ const projects = await getProjects();
178
+ const selectedProject = projects.find((p) => p.id === projectId);
179
+ if (!selectedProject) {
180
+ throw new Error(`Project with id ${projectId} not found`);
181
+ }
182
+ logger.info(`[Bot] Project selected: ${selectedProject.name || selectedProject.worktree} (id: ${projectId})`);
183
+ setCurrentProject(selectedProject);
184
+ clearSession();
185
+ summaryAggregator.clear();
186
+ clearAllInteractionState("project_switched");
187
+ // Clear pinned message when switching projects
188
+ try {
189
+ await pinnedMessageManager.clear();
190
+ }
191
+ catch (err) {
192
+ logger.error("[Bot] Error clearing pinned message:", err);
193
+ }
194
+ // Initialize keyboard manager if not already
195
+ if (ctx.chat) {
196
+ keyboardManager.initialize(ctx.api, ctx.chat.id);
197
+ }
198
+ // Refresh context limit for current model
199
+ await pinnedMessageManager.refreshContextLimit();
200
+ const contextLimit = pinnedMessageManager.getContextLimit();
201
+ // Reset context to 0 (no session selected) with current model's limit
202
+ keyboardManager.updateContext(0, contextLimit);
203
+ // Get current state for keyboard (with context = 0)
204
+ const currentAgent = getStoredAgent();
205
+ const currentModel = getStoredModel();
206
+ const contextInfo = { tokensUsed: 0, tokensLimit: contextLimit };
207
+ const variantName = formatVariantForButton(currentModel.variant || "default");
208
+ const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo, variantName);
209
+ const projectName = selectedProject.name || selectedProject.worktree;
210
+ await ctx.answerCallbackQuery();
211
+ await ctx.reply(t("projects.selected", { project: projectName }), {
212
+ reply_markup: keyboard,
213
+ });
214
+ await ctx.deleteMessage();
215
+ }
216
+ catch (error) {
217
+ clearAllInteractionState("project_select_error");
218
+ logger.error("[Bot] Error selecting project:", error);
219
+ await ctx.answerCallbackQuery();
220
+ await ctx.reply(t("projects.select_error"));
221
+ }
222
+ return true;
223
+ }
@@ -0,0 +1,139 @@
1
+ import { InlineKeyboard } from "grammy";
2
+ import { opencodeClient } from "../../opencode/client.js";
3
+ import { getCurrentSession, setCurrentSession } from "../../session/manager.js";
4
+ import { renameManager } from "../../rename/manager.js";
5
+ import { interactionManager } from "../../interaction/manager.js";
6
+ import { pinnedMessageManager } from "../../pinned/manager.js";
7
+ import { logger } from "../../utils/logger.js";
8
+ import { t } from "../../i18n/index.js";
9
+ function getCallbackMessageId(ctx) {
10
+ const message = ctx.callbackQuery?.message;
11
+ if (!message || !("message_id" in message)) {
12
+ return null;
13
+ }
14
+ const messageId = message.message_id;
15
+ return typeof messageId === "number" ? messageId : null;
16
+ }
17
+ function clearRenameInteraction(reason) {
18
+ const state = interactionManager.getSnapshot();
19
+ if (state?.kind === "rename") {
20
+ interactionManager.clear(reason);
21
+ }
22
+ }
23
+ export async function renameCommand(ctx) {
24
+ try {
25
+ const currentSession = getCurrentSession();
26
+ if (!currentSession) {
27
+ await ctx.reply(t("rename.no_session"));
28
+ return;
29
+ }
30
+ const keyboard = new InlineKeyboard().text(t("rename.button.cancel"), "rename:cancel");
31
+ const message = await ctx.reply(t("rename.prompt", { title: currentSession.title }), {
32
+ reply_markup: keyboard,
33
+ });
34
+ renameManager.startWaiting(currentSession.id, currentSession.directory, currentSession.title);
35
+ renameManager.setMessageId(message.message_id);
36
+ interactionManager.start({
37
+ kind: "rename",
38
+ expectedInput: "text",
39
+ metadata: {
40
+ sessionId: currentSession.id,
41
+ messageId: message.message_id,
42
+ },
43
+ });
44
+ logger.info(`[RenameCommand] Waiting for new title for session: ${currentSession.id}`);
45
+ }
46
+ catch (error) {
47
+ logger.error("[RenameCommand] Error starting rename flow:", error);
48
+ await ctx.reply(t("rename.error"));
49
+ }
50
+ }
51
+ export async function handleRenameCancel(ctx) {
52
+ const data = ctx.callbackQuery?.data;
53
+ if (!data || data !== "rename:cancel") {
54
+ return false;
55
+ }
56
+ logger.debug("[RenameHandler] Cancel callback received");
57
+ if (!renameManager.isWaitingForName()) {
58
+ clearRenameInteraction("rename_cancel_inactive");
59
+ await ctx.answerCallbackQuery({ text: t("rename.inactive_callback"), show_alert: true });
60
+ return true;
61
+ }
62
+ const interactionState = interactionManager.getSnapshot();
63
+ if (interactionState?.kind !== "rename") {
64
+ renameManager.clear();
65
+ await ctx.answerCallbackQuery({ text: t("rename.inactive_callback"), show_alert: true });
66
+ return true;
67
+ }
68
+ const callbackMessageId = getCallbackMessageId(ctx);
69
+ if (!renameManager.isActiveMessage(callbackMessageId)) {
70
+ await ctx.answerCallbackQuery({ text: t("rename.inactive_callback"), show_alert: true });
71
+ return true;
72
+ }
73
+ renameManager.clear();
74
+ clearRenameInteraction("rename_cancelled");
75
+ await ctx.answerCallbackQuery();
76
+ await ctx.editMessageText(t("rename.cancelled")).catch(() => { });
77
+ return true;
78
+ }
79
+ export async function handleRenameTextAnswer(ctx) {
80
+ if (!renameManager.isWaitingForName()) {
81
+ return false;
82
+ }
83
+ const text = ctx.message?.text;
84
+ if (!text) {
85
+ return false;
86
+ }
87
+ if (text.startsWith("/")) {
88
+ return false;
89
+ }
90
+ const interactionState = interactionManager.getSnapshot();
91
+ if (interactionState?.kind !== "rename") {
92
+ renameManager.clear();
93
+ await ctx.reply(t("rename.inactive"));
94
+ return true;
95
+ }
96
+ const sessionInfo = renameManager.getSessionInfo();
97
+ if (!sessionInfo) {
98
+ renameManager.clear();
99
+ clearRenameInteraction("rename_missing_session_info");
100
+ return false;
101
+ }
102
+ const newTitle = text.trim();
103
+ if (!newTitle) {
104
+ await ctx.reply(t("rename.empty_title"));
105
+ return true;
106
+ }
107
+ logger.info(`[RenameHandler] Renaming session ${sessionInfo.sessionId} to: ${newTitle}`);
108
+ try {
109
+ const { data: updatedSession, error } = await opencodeClient.session.update({
110
+ sessionID: sessionInfo.sessionId,
111
+ directory: sessionInfo.directory,
112
+ title: newTitle,
113
+ });
114
+ if (error || !updatedSession) {
115
+ throw error || new Error("Failed to update session");
116
+ }
117
+ setCurrentSession({
118
+ id: sessionInfo.sessionId,
119
+ title: newTitle,
120
+ directory: sessionInfo.directory,
121
+ });
122
+ if (pinnedMessageManager.isInitialized()) {
123
+ await pinnedMessageManager.onSessionChange(sessionInfo.sessionId, newTitle);
124
+ }
125
+ const messageId = renameManager.getMessageId();
126
+ if (messageId && ctx.chat) {
127
+ await ctx.api.deleteMessage(ctx.chat.id, messageId).catch(() => { });
128
+ }
129
+ await ctx.reply(t("rename.success", { title: newTitle }));
130
+ logger.info(`[RenameHandler] Session renamed successfully: ${newTitle}`);
131
+ }
132
+ catch (error) {
133
+ logger.error("[RenameHandler] Error renaming session:", error);
134
+ await ctx.reply(t("rename.error"));
135
+ }
136
+ renameManager.clear();
137
+ clearRenameInteraction("rename_completed");
138
+ return true;
139
+ }