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,491 @@
1
+ import * as path from "path";
2
+ import { convert } from "telegram-markdown-v2";
3
+ import { config } from "../config.js";
4
+ import { logger } from "../utils/logger.js";
5
+ import { t } from "../i18n/index.js";
6
+ import { getCurrentProject } from "../settings/manager.js";
7
+ const TELEGRAM_MESSAGE_LIMIT = 4096;
8
+ const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
9
+ function endsWithOddTrailingBackslashes(text, start, end) {
10
+ let backslashCount = 0;
11
+ for (let index = end - 1; index >= start; index--) {
12
+ if (text[index] !== "\\") {
13
+ break;
14
+ }
15
+ backslashCount += 1;
16
+ }
17
+ return backslashCount % 2 === 1;
18
+ }
19
+ function resolveSplitEndIndex(text, currentIndex, maxLength, options) {
20
+ const hardLimit = Math.min(text.length, currentIndex + maxLength);
21
+ if (hardLimit >= text.length) {
22
+ return text.length;
23
+ }
24
+ let endIndex = hardLimit;
25
+ const breakPoint = text.lastIndexOf("\n", endIndex);
26
+ if (breakPoint > currentIndex) {
27
+ endIndex = breakPoint + 1;
28
+ }
29
+ if (!options?.avoidTrailingMarkdownEscape) {
30
+ return endIndex;
31
+ }
32
+ while (endIndex > currentIndex && endsWithOddTrailingBackslashes(text, currentIndex, endIndex)) {
33
+ endIndex -= 1;
34
+ }
35
+ return endIndex > currentIndex ? endIndex : hardLimit;
36
+ }
37
+ function splitText(text, maxLength, options) {
38
+ const parts = [];
39
+ let currentIndex = 0;
40
+ while (currentIndex < text.length) {
41
+ const endIndex = resolveSplitEndIndex(text, currentIndex, maxLength, options);
42
+ if (endIndex <= currentIndex) {
43
+ const fallbackEnd = Math.min(text.length, currentIndex + 1);
44
+ parts.push(text.slice(currentIndex, fallbackEnd));
45
+ currentIndex = fallbackEnd;
46
+ continue;
47
+ }
48
+ parts.push(text.slice(currentIndex, endIndex));
49
+ currentIndex = endIndex;
50
+ }
51
+ return parts;
52
+ }
53
+ function isCodeFenceLine(line) {
54
+ return line.trimStart().startsWith("```");
55
+ }
56
+ function isHorizontalRuleLine(line) {
57
+ const normalized = line.trim();
58
+ if (!normalized) {
59
+ return false;
60
+ }
61
+ return /^([-*_])(?:\s*\1){2,}$/.test(normalized);
62
+ }
63
+ function isHeadingLine(line) {
64
+ return /^\s{0,3}#{1,6}\s+\S/.test(line);
65
+ }
66
+ function normalizeHeadingLine(line) {
67
+ const match = line.match(/^\s{0,3}#{1,6}\s+(.+?)\s*$/);
68
+ if (!match) {
69
+ return line;
70
+ }
71
+ return `**${match[1]}**`;
72
+ }
73
+ function normalizeChecklistLine(line) {
74
+ const match = line.match(/^(\s*)(?:[-+*]|\d+\.)\s+\[( |x|X)\]\s+(.*)$/);
75
+ if (!match) {
76
+ return null;
77
+ }
78
+ const marker = match[2].toLowerCase() === "x" ? "✅" : "🔲";
79
+ return `${match[1]}${marker} ${match[3]}`;
80
+ }
81
+ function preprocessMarkdownForTelegram(text) {
82
+ const lines = text.split("\n");
83
+ const output = [];
84
+ let inCodeFence = false;
85
+ let inQuote = false;
86
+ for (let index = 0; index < lines.length; index++) {
87
+ const line = lines[index];
88
+ if (isCodeFenceLine(line)) {
89
+ inCodeFence = !inCodeFence;
90
+ inQuote = false;
91
+ output.push(line);
92
+ continue;
93
+ }
94
+ if (inCodeFence) {
95
+ output.push(line);
96
+ continue;
97
+ }
98
+ if (!line.trim()) {
99
+ inQuote = false;
100
+ output.push(line);
101
+ continue;
102
+ }
103
+ if (isHeadingLine(line)) {
104
+ output.push(normalizeHeadingLine(line));
105
+ inQuote = false;
106
+ continue;
107
+ }
108
+ if (isHorizontalRuleLine(line)) {
109
+ output.push("──────────");
110
+ inQuote = false;
111
+ continue;
112
+ }
113
+ const trimmedLeft = line.trimStart();
114
+ if (trimmedLeft.startsWith(">")) {
115
+ inQuote = true;
116
+ const quoteContent = trimmedLeft.replace(/^>\s?/, "");
117
+ const normalizedChecklistInQuote = normalizeChecklistLine(quoteContent);
118
+ output.push(normalizedChecklistInQuote ? `> ${normalizedChecklistInQuote.trimStart()}` : trimmedLeft);
119
+ continue;
120
+ }
121
+ const normalizedChecklist = normalizeChecklistLine(line);
122
+ if (normalizedChecklist) {
123
+ output.push(inQuote ? `> ${normalizedChecklist.trimStart()}` : normalizedChecklist);
124
+ continue;
125
+ }
126
+ if (inQuote) {
127
+ output.push(`> ${trimmedLeft}`);
128
+ continue;
129
+ }
130
+ output.push(line);
131
+ }
132
+ return output.join("\n");
133
+ }
134
+ export function normalizePathForDisplay(filePath) {
135
+ const normalizedPath = filePath.replace(/\\/g, "/");
136
+ const project = getCurrentProject();
137
+ if (!project?.worktree) {
138
+ return normalizedPath;
139
+ }
140
+ const normalizedWorktree = project.worktree.replace(/\\/g, "/").replace(/\/+$/, "");
141
+ if (!normalizedWorktree) {
142
+ return normalizedPath;
143
+ }
144
+ const pathForCompare = process.platform === "win32" ? normalizedPath.toLowerCase() : normalizedPath;
145
+ const worktreeForCompare = process.platform === "win32" ? normalizedWorktree.toLowerCase() : normalizedWorktree;
146
+ if (pathForCompare === worktreeForCompare) {
147
+ return ".";
148
+ }
149
+ const worktreePrefix = `${worktreeForCompare}/`;
150
+ if (pathForCompare.startsWith(worktreePrefix)) {
151
+ return normalizedPath.slice(normalizedWorktree.length + 1);
152
+ }
153
+ return normalizedPath;
154
+ }
155
+ export function formatSummary(text) {
156
+ return formatSummaryWithMode(text, config.bot.messageFormatMode);
157
+ }
158
+ export function getAssistantParseMode() {
159
+ if (config.bot.messageFormatMode === "markdown") {
160
+ return "MarkdownV2";
161
+ }
162
+ return undefined;
163
+ }
164
+ export function escapePlainTextForTelegramMarkdownV2(text) {
165
+ return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
166
+ }
167
+ function formatMarkdownForTelegram(text) {
168
+ try {
169
+ const preprocessed = preprocessMarkdownForTelegram(text);
170
+ return escapeMarkdownV2PipesOutsideCode(convert(preprocessed, "keep"));
171
+ }
172
+ catch (error) {
173
+ logger.warn("[Formatter] Failed to convert markdown summary, falling back to raw text", error);
174
+ return text;
175
+ }
176
+ }
177
+ function escapeMarkdownV2PipesOutsideCode(text) {
178
+ let result = "";
179
+ let index = 0;
180
+ let inInlineCode = false;
181
+ let inCodeFence = false;
182
+ while (index < text.length) {
183
+ if (text.startsWith("```", index)) {
184
+ result += "```";
185
+ index += 3;
186
+ inCodeFence = !inCodeFence;
187
+ continue;
188
+ }
189
+ const char = text[index];
190
+ if (!inCodeFence && char === "`") {
191
+ inInlineCode = !inInlineCode;
192
+ result += char;
193
+ index += 1;
194
+ continue;
195
+ }
196
+ if (!inCodeFence && !inInlineCode && char === "|" && text[index - 1] !== "\\") {
197
+ result += "\\|";
198
+ index += 1;
199
+ continue;
200
+ }
201
+ result += char;
202
+ index += 1;
203
+ }
204
+ return result;
205
+ }
206
+ export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_LIMIT) {
207
+ if (!text || text.trim().length === 0) {
208
+ return [];
209
+ }
210
+ const normalizedMaxLength = Math.max(1, Math.floor(maxLength));
211
+ const rawTextLimit = mode === "raw" ? Math.max(1, normalizedMaxLength - "```\n\n```".length) : normalizedMaxLength;
212
+ const parts = splitText(text, rawTextLimit);
213
+ const formattedParts = [];
214
+ for (const part of parts) {
215
+ const trimmed = part.trim();
216
+ if (!trimmed) {
217
+ continue;
218
+ }
219
+ if (mode === "markdown") {
220
+ const converted = formatMarkdownForTelegram(trimmed);
221
+ const convertedParts = splitText(converted, normalizedMaxLength, {
222
+ avoidTrailingMarkdownEscape: true,
223
+ });
224
+ for (const convertedPart of convertedParts) {
225
+ const normalizedPart = convertedPart.trim();
226
+ if (normalizedPart) {
227
+ formattedParts.push(normalizedPart);
228
+ }
229
+ }
230
+ continue;
231
+ }
232
+ if (parts.length > 1) {
233
+ formattedParts.push(`\`\`\`\n${trimmed}\n\`\`\``);
234
+ }
235
+ else {
236
+ formattedParts.push(trimmed);
237
+ }
238
+ }
239
+ return formattedParts;
240
+ }
241
+ function getToolDetails(tool, input) {
242
+ if (!input) {
243
+ return "";
244
+ }
245
+ // First, check fields specific to known tools
246
+ switch (tool) {
247
+ case "read":
248
+ case "edit":
249
+ case "write":
250
+ case "apply_patch":
251
+ const filePath = input.path || input.filePath;
252
+ if (typeof filePath === "string")
253
+ return normalizePathForDisplay(filePath);
254
+ break;
255
+ case "bash":
256
+ if (typeof input.command === "string")
257
+ return input.command;
258
+ break;
259
+ case "grep":
260
+ case "glob":
261
+ if (typeof input.pattern === "string")
262
+ return input.pattern;
263
+ break;
264
+ }
265
+ // Generic search for MCP and other tools
266
+ // Look for common fields: query, url, name, prompt
267
+ const commonFields = ["query", "url", "name", "prompt", "text"];
268
+ for (const field of commonFields) {
269
+ if (typeof input[field] === "string") {
270
+ return input[field];
271
+ }
272
+ }
273
+ // If nothing matched but string fields exist, take the first one (except description)
274
+ for (const [key, value] of Object.entries(input)) {
275
+ if (key !== "description" && typeof value === "string" && value.length > 0) {
276
+ return value;
277
+ }
278
+ }
279
+ return "";
280
+ }
281
+ function getToolIcon(tool) {
282
+ switch (tool) {
283
+ case "read":
284
+ return "📖";
285
+ case "write":
286
+ return "✍️";
287
+ case "edit":
288
+ return "✏️";
289
+ case "apply_patch":
290
+ return "🩹";
291
+ case "bash":
292
+ return "💻";
293
+ case "glob":
294
+ return "📁";
295
+ case "grep":
296
+ return "🔍";
297
+ case "task":
298
+ return "🤖";
299
+ case "question":
300
+ return "❓";
301
+ case "todoread":
302
+ return "📋";
303
+ case "todowrite":
304
+ return "📝";
305
+ case "webfetch":
306
+ return "🌐";
307
+ case "web-search_tavily_search":
308
+ return "🔎";
309
+ case "web-search_tavily_extract":
310
+ return "📄";
311
+ case "skill":
312
+ return "🎓";
313
+ default:
314
+ return "🛠️";
315
+ }
316
+ }
317
+ function formatTodos(todos) {
318
+ const MAX_TODOS = 20;
319
+ const statusToMarker = {
320
+ completed: "✅",
321
+ in_progress: "🔄",
322
+ pending: "🔲",
323
+ };
324
+ const formattedTodos = [];
325
+ for (let i = 0; i < Math.min(todos.length, MAX_TODOS); i++) {
326
+ const todo = todos[i];
327
+ const marker = statusToMarker[todo.status] ?? "🔲";
328
+ formattedTodos.push(`${marker} ${todo.content}`);
329
+ }
330
+ let result = formattedTodos.join("\n");
331
+ if (todos.length > MAX_TODOS) {
332
+ result += `\n${t("tool.todo.overflow", { count: todos.length - MAX_TODOS })}`;
333
+ }
334
+ return result;
335
+ }
336
+ function formatDiffLineInfo(filediff) {
337
+ const parts = [];
338
+ if (filediff.additions && filediff.additions > 0)
339
+ parts.push(`+${filediff.additions}`);
340
+ if (filediff.deletions && filediff.deletions > 0)
341
+ parts.push(`-${filediff.deletions}`);
342
+ return parts.length > 0 ? ` (${parts.join(" ")})` : "";
343
+ }
344
+ function countDiffChangesFromText(text) {
345
+ let additions = 0;
346
+ let deletions = 0;
347
+ for (const line of text.split("\n")) {
348
+ if (line.startsWith("+") && !line.startsWith("+++")) {
349
+ additions++;
350
+ continue;
351
+ }
352
+ if (line.startsWith("-") && !line.startsWith("---")) {
353
+ deletions++;
354
+ }
355
+ }
356
+ return { additions, deletions };
357
+ }
358
+ function extractFirstUpdatedFileFromTitle(title) {
359
+ for (const rawLine of title.split("\n")) {
360
+ const line = rawLine.trim();
361
+ if (line.length >= 3 && line[1] === " " && /[AMDURC]/.test(line[0])) {
362
+ return line.slice(2).trim();
363
+ }
364
+ }
365
+ return "";
366
+ }
367
+ export function formatToolInfo(toolInfo) {
368
+ const { tool, input, title } = toolInfo;
369
+ logger.debug(`[Formatter] formatToolInfo: tool=${tool}, hasMetadata=${!!toolInfo.metadata}, hasFilediff=${!!toolInfo.metadata?.filediff}`);
370
+ if (tool === "todowrite" && toolInfo.metadata?.todos) {
371
+ const todos = toolInfo.metadata.todos;
372
+ const toolIcon = getToolIcon(tool);
373
+ const todosList = formatTodos(todos);
374
+ return `${toolIcon} ${tool} (${todos.length})\n\n${todosList}`;
375
+ }
376
+ let details = title || getToolDetails(tool, input);
377
+ const toolIcon = getToolIcon(tool);
378
+ let description = "";
379
+ if (input && typeof input.description === "string") {
380
+ description = `${input.description}\n`;
381
+ }
382
+ if (tool === "bash" && input && typeof input.command === "string") {
383
+ details = input.command;
384
+ }
385
+ if (tool === "apply_patch") {
386
+ const filediff = toolInfo.metadata && "filediff" in toolInfo.metadata
387
+ ? toolInfo.metadata.filediff
388
+ : undefined;
389
+ if (filediff?.file) {
390
+ details = normalizePathForDisplay(filediff.file);
391
+ }
392
+ else if (title) {
393
+ const fileFromTitle = extractFirstUpdatedFileFromTitle(title);
394
+ if (fileFromTitle) {
395
+ details = normalizePathForDisplay(fileFromTitle);
396
+ }
397
+ }
398
+ }
399
+ const detailsStr = details ? ` ${details}` : "";
400
+ let lineInfo = "";
401
+ if (tool === "write" && input && "content" in input && typeof input.content === "string") {
402
+ const lines = countLines(input.content);
403
+ lineInfo = ` (+${lines})`;
404
+ }
405
+ if ((tool === "edit" || tool === "apply_patch") &&
406
+ toolInfo.metadata &&
407
+ "filediff" in toolInfo.metadata) {
408
+ const filediff = toolInfo.metadata.filediff;
409
+ logger.debug("[Formatter] Diff metadata:", JSON.stringify(toolInfo.metadata, null, 2));
410
+ lineInfo = formatDiffLineInfo(filediff);
411
+ }
412
+ if (tool === "apply_patch" && !lineInfo) {
413
+ const diffText = toolInfo.metadata && typeof toolInfo.metadata.diff === "string"
414
+ ? toolInfo.metadata.diff
415
+ : input && typeof input.patchText === "string"
416
+ ? input.patchText
417
+ : "";
418
+ if (diffText) {
419
+ lineInfo = formatDiffLineInfo(countDiffChangesFromText(diffText));
420
+ }
421
+ }
422
+ return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
423
+ }
424
+ export function formatCompactToolInfo(toolInfo, maxLength = 64, fallback = "-") {
425
+ const formatted = formatToolInfo(toolInfo);
426
+ const normalized = formatted?.replace(/\s*\n+\s*/g, " ").trim() ?? "";
427
+ if (!normalized) {
428
+ return fallback;
429
+ }
430
+ if (normalized.length <= maxLength) {
431
+ return normalized;
432
+ }
433
+ return `${normalized.slice(0, Math.max(0, maxLength - 3)).trimEnd()}...`;
434
+ }
435
+ function countLines(text) {
436
+ return text.split("\n").length;
437
+ }
438
+ function formatDiff(diff) {
439
+ const lines = diff.split("\n");
440
+ const formattedLines = [];
441
+ for (const line of lines) {
442
+ if (line.startsWith("@@")) {
443
+ continue;
444
+ }
445
+ if (line.startsWith("---") || line.startsWith("+++")) {
446
+ continue;
447
+ }
448
+ if (line.startsWith("Index:")) {
449
+ continue;
450
+ }
451
+ if (line.startsWith("===") && line.includes("=")) {
452
+ continue;
453
+ }
454
+ if (line.startsWith("\\ No newline")) {
455
+ continue;
456
+ }
457
+ if (line.startsWith(" ")) {
458
+ formattedLines.push(" " + line.slice(1));
459
+ }
460
+ else if (line.startsWith("+")) {
461
+ formattedLines.push("+ " + line.slice(1));
462
+ }
463
+ else if (line.startsWith("-")) {
464
+ formattedLines.push("- " + line.slice(1));
465
+ }
466
+ else {
467
+ formattedLines.push(line);
468
+ }
469
+ }
470
+ return formattedLines.join("\n");
471
+ }
472
+ export function prepareCodeFile(content, filePath, operation) {
473
+ const displayPath = normalizePathForDisplay(filePath);
474
+ let processedContent = content;
475
+ if (operation === "edit") {
476
+ processedContent = formatDiff(content);
477
+ }
478
+ const sizeKb = Buffer.byteLength(processedContent, "utf8") / 1024;
479
+ if (sizeKb > config.files.maxFileSizeKb) {
480
+ logger.debug(`[Formatter] File too large: ${displayPath} (${sizeKb.toFixed(2)} KB > ${config.files.maxFileSizeKb} KB)`);
481
+ return null;
482
+ }
483
+ const header = operation === "write"
484
+ ? t("tool.file_header.write", { path: displayPath })
485
+ : t("tool.file_header.edit", { path: displayPath });
486
+ const fullContent = header + processedContent;
487
+ const buffer = Buffer.from(fullContent, "utf8");
488
+ const basename = path.basename(filePath);
489
+ const filename = `${operation}_${basename}.txt`;
490
+ return { buffer, filename, caption: "" };
491
+ }
@@ -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
+ }
@@ -0,0 +1,90 @@
1
+ import { logger } from "../utils/logger.js";
2
+ export class ToolMessageBatcher {
3
+ sendText;
4
+ sendFile;
5
+ sessionTasks = new Map();
6
+ generation = 0;
7
+ constructor(options) {
8
+ this.sendText = options.sendText;
9
+ this.sendFile = options.sendFile;
10
+ }
11
+ enqueue(sessionId, message) {
12
+ this.sendTextNow(sessionId, message, "enqueue");
13
+ }
14
+ sendTextNow(sessionId, message, reason) {
15
+ const normalizedMessage = message.trim();
16
+ if (!sessionId || normalizedMessage.length === 0) {
17
+ return;
18
+ }
19
+ const expectedGeneration = this.generation;
20
+ logger.debug(`[ToolBatcher] Sending text message: session=${sessionId}, reason=${reason}`);
21
+ void this.enqueueTask(sessionId, () => this.sendTextSafe(sessionId, normalizedMessage, reason, expectedGeneration));
22
+ }
23
+ enqueueUniqueByPrefix(sessionId, message, prefix) {
24
+ void prefix;
25
+ this.sendTextNow(sessionId, message, "enqueue_unique_by_prefix");
26
+ }
27
+ enqueueFile(sessionId, fileData) {
28
+ if (!sessionId) {
29
+ return;
30
+ }
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));
34
+ }
35
+ async flushSession(sessionId, reason) {
36
+ void reason;
37
+ await (this.sessionTasks.get(sessionId) ?? Promise.resolve());
38
+ }
39
+ async flushAll(reason) {
40
+ void reason;
41
+ for (const task of this.sessionTasks.values()) {
42
+ await task;
43
+ }
44
+ }
45
+ clearSession(sessionId, reason) {
46
+ this.generation++;
47
+ logger.debug(`[ToolBatcher] Cleared session sends: session=${sessionId}, reason=${reason}`);
48
+ }
49
+ clearAll(reason) {
50
+ this.generation++;
51
+ logger.debug(`[ToolBatcher] Cleared all pending tool sends: reason=${reason}`);
52
+ }
53
+ enqueueTask(sessionId, task) {
54
+ const previousTask = this.sessionTasks.get(sessionId) ?? Promise.resolve();
55
+ const nextTask = previousTask
56
+ .catch(() => undefined)
57
+ .then(task)
58
+ .finally(() => {
59
+ if (this.sessionTasks.get(sessionId) === nextTask) {
60
+ this.sessionTasks.delete(sessionId);
61
+ }
62
+ });
63
+ this.sessionTasks.set(sessionId, nextTask);
64
+ return nextTask;
65
+ }
66
+ async sendTextSafe(sessionId, text, reason, expectedGeneration) {
67
+ if (this.generation !== expectedGeneration) {
68
+ logger.debug(`[ToolBatcher] Dropping stale tool text message: session=${sessionId}, reason=${reason}`);
69
+ return;
70
+ }
71
+ try {
72
+ await this.sendText(sessionId, text);
73
+ }
74
+ catch (err) {
75
+ logger.error(`[ToolBatcher] Failed to send tool text message: session=${sessionId}, reason=${reason}`, err);
76
+ }
77
+ }
78
+ async sendFileSafe(sessionId, fileData, reason, expectedGeneration) {
79
+ if (this.generation !== expectedGeneration) {
80
+ logger.debug(`[ToolBatcher] Dropping stale tool file message: session=${sessionId}, reason=${reason}`);
81
+ return;
82
+ }
83
+ try {
84
+ await this.sendFile(sessionId, fileData);
85
+ }
86
+ catch (err) {
87
+ logger.error(`[ToolBatcher] Failed to send tool file message: session=${sessionId}, reason=${reason}`, err);
88
+ }
89
+ }
90
+ }