@grinev/opencode-telegram-bot 0.13.1 → 0.14.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.
@@ -0,0 +1,63 @@
1
+ import { formatModelDisplayName } from "../pinned/format.js";
2
+ import { t } from "../i18n/index.js";
3
+ import { formatCompactToolInfo } from "./formatter.js";
4
+ function formatToolStep(subagent) {
5
+ if (!subagent.currentTool) {
6
+ return "";
7
+ }
8
+ const toolInfo = {
9
+ sessionId: subagent.sessionId ?? subagent.parentSessionId,
10
+ messageId: subagent.cardId,
11
+ callId: subagent.cardId,
12
+ tool: subagent.currentTool,
13
+ state: {
14
+ status: "running",
15
+ input: subagent.currentToolInput ?? {},
16
+ title: subagent.currentToolTitle,
17
+ metadata: {},
18
+ time: { start: subagent.updatedAt },
19
+ },
20
+ input: subagent.currentToolInput,
21
+ title: subagent.currentToolTitle,
22
+ metadata: {},
23
+ hasFileAttachment: false,
24
+ };
25
+ const formatted = formatCompactToolInfo(toolInfo, 128, "").trim();
26
+ const firstSpaceIndex = formatted.indexOf(" ");
27
+ if (firstSpaceIndex >= 0 && formatted.slice(firstSpaceIndex + 1) === subagent.currentTool) {
28
+ return "";
29
+ }
30
+ return formatted;
31
+ }
32
+ function formatSubagentActivity(subagent) {
33
+ if (subagent.status === "completed") {
34
+ return `✅ ${t("subagent.completed")}`;
35
+ }
36
+ if (subagent.status === "error") {
37
+ const message = subagent.terminalMessage?.trim() || t("subagent.failed");
38
+ return `❌ ${message}`;
39
+ }
40
+ const toolStep = formatToolStep(subagent);
41
+ if (toolStep) {
42
+ return toolStep;
43
+ }
44
+ return `⚙️ ${t("subagent.working")}`;
45
+ }
46
+ async function formatSubagentCard(subagent) {
47
+ const modelName = formatModelDisplayName(subagent.providerID, subagent.modelID);
48
+ const lines = [
49
+ `🧩 ${t("subagent.line.task", { task: subagent.description })}`,
50
+ t("subagent.line.agent", { agent: subagent.agent }),
51
+ t("pinned.line.model", { model: modelName }),
52
+ "",
53
+ formatSubagentActivity(subagent),
54
+ ];
55
+ return lines.join("\n");
56
+ }
57
+ export async function renderSubagentCards(subagents) {
58
+ if (subagents.length === 0) {
59
+ return "";
60
+ }
61
+ const parts = await Promise.all(subagents.map((subagent) => formatSubagentCard(subagent)));
62
+ return parts.filter(Boolean).join("\n\n");
63
+ }
@@ -1,50 +1,15 @@
1
1
  import { logger } from "../utils/logger.js";
2
- const DEFAULT_INTERVAL_SECONDS = 5;
3
- const TELEGRAM_MESSAGE_MAX_LENGTH = 4096;
4
- function normalizeIntervalSeconds(value) {
5
- if (!Number.isFinite(value)) {
6
- return DEFAULT_INTERVAL_SECONDS;
7
- }
8
- const normalized = Math.floor(value);
9
- if (normalized < 0) {
10
- return DEFAULT_INTERVAL_SECONDS;
11
- }
12
- return normalized;
13
- }
14
2
  export class ToolMessageBatcher {
15
- intervalSeconds;
16
3
  sendText;
17
4
  sendFile;
18
- queues = new Map();
19
- timers = new Map();
20
5
  sessionTasks = new Map();
21
6
  generation = 0;
22
7
  constructor(options) {
23
- this.intervalSeconds = normalizeIntervalSeconds(options.intervalSeconds);
24
8
  this.sendText = options.sendText;
25
9
  this.sendFile = options.sendFile;
26
10
  }
27
- setIntervalSeconds(nextIntervalSeconds) {
28
- const normalized = normalizeIntervalSeconds(nextIntervalSeconds);
29
- if (this.intervalSeconds === normalized) {
30
- return;
31
- }
32
- this.intervalSeconds = normalized;
33
- logger.info(`[ToolBatcher] Interval updated: ${normalized}s`);
34
- if (normalized === 0) {
35
- void this.flushAll("interval_updated");
36
- return;
37
- }
38
- const sessionIds = Array.from(this.queues.keys());
39
- for (const sessionId of sessionIds) {
40
- this.restartTimer(sessionId);
41
- }
42
- }
43
- getIntervalSeconds() {
44
- return this.intervalSeconds;
45
- }
46
11
  enqueue(sessionId, message) {
47
- this.enqueueTextInternal(sessionId, message);
12
+ this.sendTextNow(sessionId, message, "enqueue");
48
13
  }
49
14
  sendTextNow(sessionId, message, reason) {
50
15
  const normalizedMessage = message.trim();
@@ -52,80 +17,38 @@ export class ToolMessageBatcher {
52
17
  return;
53
18
  }
54
19
  const expectedGeneration = this.generation;
55
- logger.debug(`[ToolBatcher] Sending immediate text message outside queue: session=${sessionId}, reason=${reason}`);
20
+ logger.debug(`[ToolBatcher] Sending text message: session=${sessionId}, reason=${reason}`);
56
21
  void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, reason, expectedGeneration));
57
22
  }
58
23
  enqueueUniqueByPrefix(sessionId, message, prefix) {
59
- this.enqueueTextInternal(sessionId, message, prefix);
24
+ void prefix;
25
+ this.sendTextNow(sessionId, message, "enqueue_unique_by_prefix");
60
26
  }
61
27
  enqueueFile(sessionId, fileData) {
62
28
  if (!sessionId) {
63
29
  return;
64
30
  }
65
- if (this.intervalSeconds === 0) {
66
- const expectedGeneration = this.generation;
67
- logger.debug(`[ToolBatcher] Sending immediate file message: session=${sessionId}`);
68
- void this.enqueueTask(sessionId, () => this.sendFileSafe(sessionId, fileData, "immediate", expectedGeneration));
69
- return;
70
- }
71
- const queue = this.queues.get(sessionId) ?? [];
72
- queue.push({ kind: "file", fileData });
73
- this.queues.set(sessionId, queue);
74
- logger.debug(`[ToolBatcher] Queued file message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
75
- this.ensureTimer(sessionId);
31
+ const expectedGeneration = this.generation;
32
+ logger.debug(`[ToolBatcher] Sending file message: session=${sessionId}`);
33
+ void this.enqueueTask(sessionId, () => this.sendFileSafe(sessionId, fileData, "enqueue_file", expectedGeneration));
76
34
  }
77
35
  async flushSession(sessionId, reason) {
78
- await this.enqueueTask(sessionId, () => this.flushSessionInternal(sessionId, reason));
36
+ void reason;
37
+ await (this.sessionTasks.get(sessionId) ?? Promise.resolve());
79
38
  }
80
39
  async flushAll(reason) {
81
- for (const sessionId of Array.from(this.timers.keys())) {
82
- this.clearTimer(sessionId);
83
- }
84
- const sessionIds = Array.from(this.queues.keys());
85
- for (const sessionId of sessionIds) {
86
- await this.flushSession(sessionId, reason);
40
+ void reason;
41
+ for (const task of this.sessionTasks.values()) {
42
+ await task;
87
43
  }
88
44
  }
89
45
  clearSession(sessionId, reason) {
90
46
  this.generation++;
91
- this.clearTimer(sessionId);
92
- if (this.queues.delete(sessionId)) {
93
- logger.debug(`[ToolBatcher] Cleared session queue: session=${sessionId}, reason=${reason}`);
94
- }
47
+ logger.debug(`[ToolBatcher] Cleared session sends: session=${sessionId}, reason=${reason}`);
95
48
  }
96
49
  clearAll(reason) {
97
50
  this.generation++;
98
- for (const timer of this.timers.values()) {
99
- clearTimeout(timer);
100
- }
101
- const queuedSessions = this.queues.size;
102
- this.timers.clear();
103
- this.queues.clear();
104
- if (queuedSessions > 0) {
105
- logger.debug(`[ToolBatcher] Cleared all queued tool messages: sessions=${queuedSessions}, reason=${reason}`);
106
- }
107
- }
108
- clearTimer(sessionId) {
109
- const timer = this.timers.get(sessionId);
110
- if (!timer) {
111
- return;
112
- }
113
- clearTimeout(timer);
114
- this.timers.delete(sessionId);
115
- }
116
- ensureTimer(sessionId) {
117
- if (this.timers.has(sessionId)) {
118
- return;
119
- }
120
- this.restartTimer(sessionId);
121
- }
122
- restartTimer(sessionId) {
123
- this.clearTimer(sessionId);
124
- const timer = setTimeout(() => {
125
- this.timers.delete(sessionId);
126
- void this.flushSession(sessionId, "interval_elapsed");
127
- }, this.intervalSeconds * 1000);
128
- this.timers.set(sessionId, timer);
51
+ logger.debug(`[ToolBatcher] Cleared all pending tool sends: reason=${reason}`);
129
52
  }
130
53
  enqueueTask(sessionId, task) {
131
54
  const previousTask = this.sessionTasks.get(sessionId) ?? Promise.resolve();
@@ -140,53 +63,6 @@ export class ToolMessageBatcher {
140
63
  this.sessionTasks.set(sessionId, nextTask);
141
64
  return nextTask;
142
65
  }
143
- enqueueTextInternal(sessionId, message, uniquePrefix) {
144
- const normalizedMessage = message.trim();
145
- if (!sessionId || normalizedMessage.length === 0) {
146
- return;
147
- }
148
- if (this.intervalSeconds === 0) {
149
- const expectedGeneration = this.generation;
150
- logger.debug(`[ToolBatcher] Sending immediate text message: session=${sessionId}`);
151
- void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, "immediate", expectedGeneration));
152
- return;
153
- }
154
- const normalizedPrefix = uniquePrefix?.trim();
155
- const queue = this.queues.get(sessionId) ?? [];
156
- if (normalizedPrefix) {
157
- const existingUniqueMessage = queue.find((item) => item.kind === "text" && item.text.startsWith(normalizedPrefix));
158
- if (existingUniqueMessage) {
159
- existingUniqueMessage.text = normalizedMessage;
160
- this.queues.set(sessionId, queue);
161
- logger.debug(`[ToolBatcher] Updated queued unique text message: session=${sessionId}, prefix=${normalizedPrefix}, interval=${this.intervalSeconds}s`);
162
- this.ensureTimer(sessionId);
163
- return;
164
- }
165
- }
166
- queue.push({ kind: "text", text: normalizedMessage });
167
- this.queues.set(sessionId, queue);
168
- logger.debug(`[ToolBatcher] Queued text message: session=${sessionId}, queueSize=${queue.length}, interval=${this.intervalSeconds}s`);
169
- this.ensureTimer(sessionId);
170
- }
171
- async flushSessionInternal(sessionId, reason) {
172
- const expectedGeneration = this.generation;
173
- this.clearTimer(sessionId);
174
- const queuedItems = this.queues.get(sessionId);
175
- if (!queuedItems || queuedItems.length === 0) {
176
- return;
177
- }
178
- this.queues.delete(sessionId);
179
- const flushItems = this.buildFlushItems(queuedItems);
180
- logger.debug(`[ToolBatcher] Flushing ${queuedItems.length} queued items as ${flushItems.length} Telegram sends (session=${sessionId}, reason=${reason})`);
181
- for (const item of flushItems) {
182
- if (item.kind === "text") {
183
- await this.sendTextSafe(sessionId, item.text, reason, expectedGeneration);
184
- }
185
- else {
186
- await this.sendFileSafe(sessionId, item.fileData, reason, expectedGeneration);
187
- }
188
- }
189
- }
190
66
  async sendTextSafe(sessionId, text, reason, expectedGeneration) {
191
67
  if (this.generation !== expectedGeneration) {
192
68
  logger.debug(`[ToolBatcher] Dropping stale tool text message: session=${sessionId}, reason=${reason}`);
@@ -211,75 +87,4 @@ export class ToolMessageBatcher {
211
87
  logger.error(`[ToolBatcher] Failed to send tool file message: session=${sessionId}, reason=${reason}`, err);
212
88
  }
213
89
  }
214
- buildFlushItems(entries) {
215
- const result = [];
216
- const textBuffer = [];
217
- const flushTextBuffer = () => {
218
- if (textBuffer.length === 0) {
219
- return;
220
- }
221
- const packedTextMessages = this.packMessages(textBuffer);
222
- for (const text of packedTextMessages) {
223
- result.push({ kind: "text", text });
224
- }
225
- textBuffer.length = 0;
226
- };
227
- for (const entry of entries) {
228
- if (entry.kind === "text") {
229
- textBuffer.push(entry.text);
230
- }
231
- else {
232
- flushTextBuffer();
233
- result.push({ kind: "file", fileData: entry.fileData });
234
- }
235
- }
236
- flushTextBuffer();
237
- return result;
238
- }
239
- packMessages(messages) {
240
- const normalizedEntries = messages
241
- .flatMap((message) => this.splitLongText(message, TELEGRAM_MESSAGE_MAX_LENGTH))
242
- .filter((entry) => entry.length > 0);
243
- if (normalizedEntries.length === 0) {
244
- return [];
245
- }
246
- const result = [];
247
- let current = "";
248
- for (const entry of normalizedEntries) {
249
- if (!current) {
250
- current = entry;
251
- continue;
252
- }
253
- const candidate = `${current}\n\n${entry}`;
254
- if (candidate.length <= TELEGRAM_MESSAGE_MAX_LENGTH) {
255
- current = candidate;
256
- continue;
257
- }
258
- result.push(current);
259
- current = entry;
260
- }
261
- if (current) {
262
- result.push(current);
263
- }
264
- return result;
265
- }
266
- splitLongText(text, limit) {
267
- if (text.length <= limit) {
268
- return [text];
269
- }
270
- const chunks = [];
271
- let remaining = text;
272
- while (remaining.length > limit) {
273
- let splitIndex = remaining.lastIndexOf("\n", limit);
274
- if (splitIndex <= 0 || splitIndex < Math.floor(limit * 0.5)) {
275
- splitIndex = limit;
276
- }
277
- chunks.push(remaining.slice(0, splitIndex));
278
- remaining = remaining.slice(splitIndex).replace(/^\n+/, "");
279
- }
280
- if (remaining.length > 0) {
281
- chunks.push(remaining);
282
- }
283
- return chunks;
284
- }
285
90
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grinev/opencode-telegram-bot",
3
- "version": "0.13.1",
3
+ "version": "0.14.0",
4
4
  "description": "Telegram bot client for OpenCode to run and monitor coding tasks from chat.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",