@grinev/opencode-telegram-bot 0.5.0 → 0.6.1-rc.1

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.
@@ -266,7 +266,7 @@ class PinnedMessageManager {
266
266
  logger.debug(`[PinnedManager] loadDiffsFromMessages: ${messagesData.length} messages`);
267
267
  const filesMap = new Map();
268
268
  let toolCount = 0;
269
- let editWriteCount = 0;
269
+ let fileToolCount = 0;
270
270
  for (const { parts } of messagesData) {
271
271
  for (const part of parts) {
272
272
  if (part.type !== "tool")
@@ -275,10 +275,12 @@ class PinnedMessageManager {
275
275
  const toolPart = part;
276
276
  if (toolPart.state.status !== "completed")
277
277
  continue;
278
- if (toolPart.tool === "edit" || toolPart.tool === "write") {
279
- editWriteCount++;
278
+ if (toolPart.tool === "edit" ||
279
+ toolPart.tool === "write" ||
280
+ toolPart.tool === "apply_patch") {
281
+ fileToolCount++;
280
282
  }
281
- if (toolPart.tool === "edit" &&
283
+ if ((toolPart.tool === "edit" || toolPart.tool === "apply_patch") &&
282
284
  toolPart.state.metadata &&
283
285
  "filediff" in toolPart.state.metadata) {
284
286
  const filediff = toolPart.state.metadata.filediff;
@@ -318,7 +320,7 @@ class PinnedMessageManager {
318
320
  }
319
321
  }
320
322
  }
321
- logger.debug(`[PinnedManager] loadDiffsFromMessages: found ${toolCount} tool parts, ${editWriteCount} edit/write`);
323
+ logger.debug(`[PinnedManager] loadDiffsFromMessages: found ${toolCount} tool parts, ${fileToolCount} file tools`);
322
324
  if (filesMap.size > 0) {
323
325
  this.state.changedFiles = Array.from(filesMap.values());
324
326
  logger.info(`[PinnedManager] Loaded ${this.state.changedFiles.length} file diffs from messages`);
@@ -119,5 +119,10 @@ export function __resetSettingsForTests() {
119
119
  settingsWriteQueue = Promise.resolve();
120
120
  }
121
121
  export async function loadSettings() {
122
- currentSettings = await readSettingsFile();
122
+ const loadedSettings = (await readSettingsFile());
123
+ if ("toolMessagesIntervalSec" in loadedSettings) {
124
+ delete loadedSettings.toolMessagesIntervalSec;
125
+ void writeSettingsFile(loadedSettings);
126
+ }
127
+ currentSettings = loadedSettings;
123
128
  }
@@ -1,6 +1,29 @@
1
- import { prepareCodeFile } from "./formatter.js";
1
+ import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { getCurrentProject } from "../settings/manager.js";
4
+ function extractFirstUpdatedFileFromTitle(title) {
5
+ for (const rawLine of title.split("\n")) {
6
+ const line = rawLine.trim();
7
+ if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
8
+ return line.slice(2).trim();
9
+ }
10
+ }
11
+ return "";
12
+ }
13
+ function countDiffChangesFromText(text) {
14
+ let additions = 0;
15
+ let deletions = 0;
16
+ for (const line of text.split("\n")) {
17
+ if (line.startsWith("+") && !line.startsWith("+++")) {
18
+ additions++;
19
+ continue;
20
+ }
21
+ if (line.startsWith("-") && !line.startsWith("---")) {
22
+ deletions++;
23
+ }
24
+ }
25
+ return { additions, deletions };
26
+ }
4
27
  class SummaryAggregator {
5
28
  currentSessionId = null;
6
29
  currentMessageParts = new Map();
@@ -19,6 +42,7 @@ class SummaryAggregator {
19
42
  onPermissionCallback = null;
20
43
  onSessionDiffCallback = null;
21
44
  onFileChangeCallback = null;
45
+ onClearedCallback = null;
22
46
  processedToolStates = new Set();
23
47
  bot = null;
24
48
  chatId = null;
@@ -61,6 +85,9 @@ class SummaryAggregator {
61
85
  setOnFileChange(callback) {
62
86
  this.onFileChangeCallback = callback;
63
87
  }
88
+ setOnCleared(callback) {
89
+ this.onClearedCallback = callback;
90
+ }
64
91
  startTypingIndicator() {
65
92
  if (this.typingTimer) {
66
93
  return;
@@ -145,6 +172,14 @@ class SummaryAggregator {
145
172
  this.processedToolStates.clear();
146
173
  this.messageCount = 0;
147
174
  this.lastUpdated = 0;
175
+ if (this.onClearedCallback) {
176
+ try {
177
+ this.onClearedCallback();
178
+ }
179
+ catch (err) {
180
+ logger.error("[Aggregator] Error in clear callback:", err);
181
+ }
182
+ }
148
183
  }
149
184
  handleMessageUpdated(event) {
150
185
  const { info } = event.properties;
@@ -160,8 +195,11 @@ class SummaryAggregator {
160
195
  this.startTypingIndicator();
161
196
  // Notify that agent started thinking
162
197
  if (this.onThinkingCallback) {
198
+ const callback = this.onThinkingCallback;
163
199
  setImmediate(() => {
164
- this.onThinkingCallback();
200
+ if (typeof callback === "function") {
201
+ callback(info.sessionID);
202
+ }
165
203
  });
166
204
  }
167
205
  }
@@ -260,10 +298,12 @@ class SummaryAggregator {
260
298
  }
261
299
  if ("status" in state && state.status === "completed") {
262
300
  logger.debug(`[Aggregator] Tool completed: callID=${part.callID}, tool=${part.tool}`, JSON.stringify(state, null, 2));
263
- const notifiedKey = `notified-${part.callID}`;
264
- if (!this.processedToolStates.has(notifiedKey)) {
265
- this.processedToolStates.add(notifiedKey);
301
+ const completedKey = `completed-${part.callID}`;
302
+ if (!this.processedToolStates.has(completedKey)) {
303
+ this.processedToolStates.add(completedKey);
304
+ const preparedFileContext = this.prepareToolFileContext(part.tool, input, title, state.metadata);
266
305
  const toolData = {
306
+ sessionId: part.sessionID,
267
307
  messageId: messageID,
268
308
  callId: part.callID,
269
309
  tool: part.tool,
@@ -271,61 +311,105 @@ class SummaryAggregator {
271
311
  input,
272
312
  title,
273
313
  metadata: state.metadata,
314
+ hasFileAttachment: !!preparedFileContext.fileData,
274
315
  };
275
316
  logger.debug(`[Aggregator] Sending tool notification to Telegram: tool=${part.tool}, title=${title || "N/A"}`);
276
317
  if (this.onToolCallback) {
277
318
  this.onToolCallback(toolData);
278
319
  }
279
- }
280
- }
281
- if ("status" in state && state.status === "completed") {
282
- const fileKey = `file-${part.callID}`;
283
- if (!this.processedToolStates.has(fileKey)) {
284
- this.processedToolStates.add(fileKey);
285
- if (part.tool === "write" && input && "content" in input && "filePath" in input) {
286
- const fileData = prepareCodeFile(input.content, input.filePath, "write");
287
- if (fileData && this.onToolFileCallback) {
288
- logger.debug(`[Aggregator] Sending write file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
289
- this.onToolFileCallback(fileData);
290
- }
291
- // Notify about file change for pinned message
292
- if (this.onFileChangeCallback) {
293
- const lines = input.content.split("\n").length;
294
- this.onFileChangeCallback({
295
- file: input.filePath,
296
- additions: lines,
297
- deletions: 0,
298
- });
299
- }
320
+ if (preparedFileContext.fileData && this.onToolFileCallback) {
321
+ logger.debug(`[Aggregator] Sending ${part.tool} file: ${preparedFileContext.fileData.filename} (${preparedFileContext.fileData.buffer.length} bytes)`);
322
+ this.onToolFileCallback({
323
+ ...toolData,
324
+ hasFileAttachment: true,
325
+ fileData: preparedFileContext.fileData,
326
+ });
300
327
  }
301
- else if (part.tool === "edit" &&
302
- state.metadata &&
303
- "diff" in state.metadata &&
304
- "filediff" in state.metadata) {
305
- const filediff = state.metadata.filediff;
306
- const filePath = filediff.file;
307
- const diff = state.metadata.diff;
308
- if (filePath && diff) {
309
- const fileData = prepareCodeFile(diff, filePath, "edit");
310
- if (fileData && this.onToolFileCallback) {
311
- logger.debug(`[Aggregator] Sending edit file: ${fileData.filename} (${fileData.buffer.length} bytes)`);
312
- this.onToolFileCallback(fileData);
313
- }
314
- // Notify about file change for pinned message
315
- if (this.onFileChangeCallback) {
316
- this.onFileChangeCallback({
317
- file: filePath,
318
- additions: filediff.additions || 0,
319
- deletions: filediff.deletions || 0,
320
- });
321
- }
322
- }
328
+ if (preparedFileContext.fileChange && this.onFileChangeCallback) {
329
+ this.onFileChangeCallback(preparedFileContext.fileChange);
323
330
  }
324
331
  }
325
332
  }
326
333
  }
327
334
  this.lastUpdated = Date.now();
328
335
  }
336
+ prepareToolFileContext(tool, input, title, metadata) {
337
+ if (tool === "write" && input) {
338
+ const filePath = typeof input.filePath === "string" ? normalizePathForDisplay(input.filePath) : "";
339
+ const hasContent = typeof input.content === "string";
340
+ const content = hasContent ? input.content : "";
341
+ if (!filePath || !hasContent) {
342
+ return { fileData: null, fileChange: null };
343
+ }
344
+ return {
345
+ fileData: prepareCodeFile(content, filePath, "write"),
346
+ fileChange: {
347
+ file: filePath,
348
+ additions: content.split("\n").length,
349
+ deletions: 0,
350
+ },
351
+ };
352
+ }
353
+ if (tool === "edit" && metadata) {
354
+ const editMetadata = metadata;
355
+ const filePath = editMetadata.filediff?.file
356
+ ? normalizePathForDisplay(editMetadata.filediff.file)
357
+ : "";
358
+ const diffText = typeof editMetadata.diff === "string" ? editMetadata.diff : "";
359
+ if (!filePath || !diffText) {
360
+ return { fileData: null, fileChange: null };
361
+ }
362
+ return {
363
+ fileData: prepareCodeFile(diffText, filePath, "edit"),
364
+ fileChange: {
365
+ file: filePath,
366
+ additions: editMetadata.filediff?.additions || 0,
367
+ deletions: editMetadata.filediff?.deletions || 0,
368
+ },
369
+ };
370
+ }
371
+ if (tool === "apply_patch") {
372
+ const patchMetadata = metadata;
373
+ const filePathFromInput = input && typeof input.filePath === "string"
374
+ ? normalizePathForDisplay(input.filePath)
375
+ : input && typeof input.path === "string"
376
+ ? normalizePathForDisplay(input.path)
377
+ : "";
378
+ const filePathFromTitle = title ? extractFirstUpdatedFileFromTitle(title) : "";
379
+ const filePath = (patchMetadata?.filediff?.file && normalizePathForDisplay(patchMetadata.filediff.file)) ||
380
+ filePathFromInput ||
381
+ normalizePathForDisplay(filePathFromTitle);
382
+ const diffText = typeof patchMetadata?.diff === "string"
383
+ ? patchMetadata.diff
384
+ : input && typeof input.patchText === "string"
385
+ ? input.patchText
386
+ : "";
387
+ if (!filePath) {
388
+ return { fileData: null, fileChange: null };
389
+ }
390
+ const fileChange = patchMetadata?.filediff
391
+ ? {
392
+ file: filePath,
393
+ additions: patchMetadata.filediff.additions || 0,
394
+ deletions: patchMetadata.filediff.deletions || 0,
395
+ }
396
+ : diffText
397
+ ? (() => {
398
+ const changes = countDiffChangesFromText(diffText);
399
+ return {
400
+ file: filePath,
401
+ additions: changes.additions,
402
+ deletions: changes.deletions,
403
+ };
404
+ })()
405
+ : null;
406
+ return {
407
+ fileData: diffText ? prepareCodeFile(diffText, filePath, "edit") : null,
408
+ fileChange,
409
+ };
410
+ }
411
+ return { fileData: null, fileChange: null };
412
+ }
329
413
  hashString(str) {
330
414
  let hash = 0;
331
415
  for (let i = 0; i < str.length; i++) {
@@ -2,6 +2,7 @@ import * as path from "path";
2
2
  import { config } from "../config.js";
3
3
  import { logger } from "../utils/logger.js";
4
4
  import { t } from "../i18n/index.js";
5
+ import { getCurrentProject } from "../settings/manager.js";
5
6
  const TELEGRAM_MESSAGE_LIMIT = 4096;
6
7
  function splitText(text, maxLength) {
7
8
  const parts = [];
@@ -21,6 +22,27 @@ function splitText(text, maxLength) {
21
22
  }
22
23
  return parts;
23
24
  }
25
+ export function normalizePathForDisplay(filePath) {
26
+ const normalizedPath = filePath.replace(/\\/g, "/");
27
+ const project = getCurrentProject();
28
+ if (!project?.worktree) {
29
+ return normalizedPath;
30
+ }
31
+ const normalizedWorktree = project.worktree.replace(/\\/g, "/").replace(/\/+$/, "");
32
+ if (!normalizedWorktree) {
33
+ return normalizedPath;
34
+ }
35
+ const pathForCompare = process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
36
+ const worktreeForCompare = process.platform === "win32" ? normalizedWorktree.toLowerCase() : normalizedWorktree;
37
+ if (pathForCompare === worktreeForCompare) {
38
+ return ".";
39
+ }
40
+ const worktreePrefix = `${worktreeForCompare}/`;
41
+ if (pathForCompare.startsWith(worktreePrefix)) {
42
+ return normalizedPath.slice(normalizedWorktree.length + 1);
43
+ }
44
+ return normalizedPath;
45
+ }
24
46
  export function formatSummary(text) {
25
47
  if (!text || text.trim().length === 0) {
26
48
  return [];
@@ -50,9 +72,10 @@ function getToolDetails(tool, input) {
50
72
  case "read":
51
73
  case "edit":
52
74
  case "write":
53
- const path = input.path || input.filePath;
54
- if (typeof path === "string")
55
- return path;
75
+ case "apply_patch":
76
+ const filePath = input.path || input.filePath;
77
+ if (typeof filePath === "string")
78
+ return normalizePathForDisplay(filePath);
56
79
  break;
57
80
  case "bash":
58
81
  if (typeof input.command === "string")
@@ -88,6 +111,8 @@ function getToolIcon(tool) {
88
111
  return "✍️";
89
112
  case "edit":
90
113
  return "✏️";
114
+ case "apply_patch":
115
+ return "🩹";
91
116
  case "bash":
92
117
  return "💻";
93
118
  case "glob":
@@ -133,6 +158,37 @@ function formatTodos(todos) {
133
158
  }
134
159
  return result;
135
160
  }
161
+ function formatDiffLineInfo(filediff) {
162
+ const parts = [];
163
+ if (filediff.additions && filediff.additions > 0)
164
+ parts.push(`+${filediff.additions}`);
165
+ if (filediff.deletions && filediff.deletions > 0)
166
+ parts.push(`-${filediff.deletions}`);
167
+ return parts.length > 0 ? ` (${parts.join(" ")})` : "";
168
+ }
169
+ function countDiffChangesFromText(text) {
170
+ let additions = 0;
171
+ let deletions = 0;
172
+ for (const line of text.split("\n")) {
173
+ if (line.startsWith("+") && !line.startsWith("+++")) {
174
+ additions++;
175
+ continue;
176
+ }
177
+ if (line.startsWith("-") && !line.startsWith("---")) {
178
+ deletions++;
179
+ }
180
+ }
181
+ return { additions, deletions };
182
+ }
183
+ function extractFirstUpdatedFileFromTitle(title) {
184
+ for (const rawLine of title.split("\n")) {
185
+ const line = rawLine.trim();
186
+ if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
187
+ return line.slice(2).trim();
188
+ }
189
+ }
190
+ return "";
191
+ }
136
192
  export function formatToolInfo(toolInfo) {
137
193
  const { tool, input, title } = toolInfo;
138
194
  logger.debug(`[Formatter] formatToolInfo: tool=${tool}, hasMetadata=${!!toolInfo.metadata}, hasFilediff=${!!toolInfo.metadata?.filediff}`);
@@ -151,22 +207,41 @@ export function formatToolInfo(toolInfo) {
151
207
  if (tool === "bash" && input && typeof input.command === "string") {
152
208
  details = input.command;
153
209
  }
210
+ if (tool === "apply_patch") {
211
+ const filediff = toolInfo.metadata && "filediff" in toolInfo.metadata
212
+ ? toolInfo.metadata.filediff
213
+ : undefined;
214
+ if (filediff?.file) {
215
+ details = normalizePathForDisplay(filediff.file);
216
+ }
217
+ else if (title) {
218
+ const fileFromTitle = extractFirstUpdatedFileFromTitle(title);
219
+ if (fileFromTitle) {
220
+ details = normalizePathForDisplay(fileFromTitle);
221
+ }
222
+ }
223
+ }
154
224
  const detailsStr = details ? ` ${details}` : "";
155
225
  let lineInfo = "";
156
226
  if (tool === "write" && input && "content" in input && typeof input.content === "string") {
157
227
  const lines = countLines(input.content);
158
228
  lineInfo = ` (+${lines})`;
159
229
  }
160
- if (tool === "edit" && toolInfo.metadata && "filediff" in toolInfo.metadata) {
230
+ if ((tool === "edit" || tool === "apply_patch") &&
231
+ toolInfo.metadata &&
232
+ "filediff" in toolInfo.metadata) {
161
233
  const filediff = toolInfo.metadata.filediff;
162
- logger.debug("[Formatter] Edit metadata:", JSON.stringify(toolInfo.metadata, null, 2));
163
- const parts = [];
164
- if (filediff.additions && filediff.additions > 0)
165
- parts.push(`+${filediff.additions}`);
166
- if (filediff.deletions && filediff.deletions > 0)
167
- parts.push(`-${filediff.deletions}`);
168
- if (parts.length > 0) {
169
- lineInfo = ` (${parts.join(" ")})`;
234
+ logger.debug("[Formatter] Diff metadata:", JSON.stringify(toolInfo.metadata, null, 2));
235
+ lineInfo = formatDiffLineInfo(filediff);
236
+ }
237
+ if (tool === "apply_patch" && !lineInfo) {
238
+ const diffText = toolInfo.metadata && typeof toolInfo.metadata.diff === "string"
239
+ ? toolInfo.metadata.diff
240
+ : input && typeof input.patchText === "string"
241
+ ? input.patchText
242
+ : "";
243
+ if (diffText) {
244
+ lineInfo = formatDiffLineInfo(countDiffChangesFromText(diffText));
170
245
  }
171
246
  }
172
247
  return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
@@ -209,18 +284,19 @@ function formatDiff(diff) {
209
284
  return formattedLines.join("\n");
210
285
  }
211
286
  export function prepareCodeFile(content, filePath, operation) {
287
+ const displayPath = normalizePathForDisplay(filePath);
212
288
  let processedContent = content;
213
289
  if (operation === "edit") {
214
290
  processedContent = formatDiff(content);
215
291
  }
216
292
  const sizeKb = Buffer.byteLength(processedContent, "utf8") / 1024;
217
293
  if (sizeKb > config.files.maxFileSizeKb) {
218
- logger.debug(`[Formatter] File too large: ${filePath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
294
+ logger.debug(`[Formatter] File too large: ${displayPath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
219
295
  return null;
220
296
  }
221
297
  const header = operation === "write"
222
- ? t("tool.file_header.write", { path: filePath })
223
- : t("tool.file_header.edit", { path: filePath });
298
+ ? t("tool.file_header.write", { path: displayPath })
299
+ : t("tool.file_header.edit", { path: displayPath });
224
300
  const fullContent = header + processedContent;
225
301
  const buffer = Buffer.from(fullContent, "utf8");
226
302
  const basename = path.basename(filePath);