@grinev/opencode-telegram-bot 0.13.2 → 0.14.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.
@@ -39,6 +39,7 @@ class SummaryAggregator {
39
39
  onThinkingCallback = null;
40
40
  onTokensCallback = null;
41
41
  onCostCallback = null;
42
+ onSubagentCallback = null;
42
43
  onSessionCompactedCallback = null;
43
44
  onSessionErrorCallback = null;
44
45
  onSessionRetryCallback = null;
@@ -54,6 +55,13 @@ class SummaryAggregator {
54
55
  typingTimer = null;
55
56
  typingIndicatorEnabled = true;
56
57
  partHashes = new Map();
58
+ trackedSessionParents = new Map();
59
+ subagentStates = new Map();
60
+ subagentOrder = [];
61
+ subagentCardIdBySessionId = new Map();
62
+ pendingSubagentCardIdsByParent = new Map();
63
+ pendingChildSessionIdsByParent = new Map();
64
+ fallbackSubagentCardIdsByParent = new Map();
57
65
  setBotAndChatId(bot, chatId) {
58
66
  this.bot = bot;
59
67
  this.chatId = chatId;
@@ -85,6 +93,9 @@ class SummaryAggregator {
85
93
  setOnCost(callback) {
86
94
  this.onCostCallback = callback;
87
95
  }
96
+ setOnSubagent(callback) {
97
+ this.onSubagentCallback = callback;
98
+ }
88
99
  setOnSessionCompacted(callback) {
89
100
  this.onSessionCompactedCallback = callback;
90
101
  }
@@ -150,6 +161,10 @@ class SummaryAggregator {
150
161
  logger.debug(`[Aggregator] Session event: ${event.type}`, JSON.stringify(event.properties, null, 2));
151
162
  }
152
163
  switch (event.type) {
164
+ case "session.created":
165
+ case "session.updated":
166
+ this.handleSessionCreatedOrUpdated(event);
167
+ break;
153
168
  case "message.updated":
154
169
  this.handleMessageUpdated(event);
155
170
  break;
@@ -195,6 +210,7 @@ class SummaryAggregator {
195
210
  if (this.currentSessionId !== sessionId) {
196
211
  this.clear();
197
212
  this.currentSessionId = sessionId;
213
+ this.trackedSessionParents.set(sessionId, null);
198
214
  }
199
215
  }
200
216
  clear() {
@@ -206,6 +222,13 @@ class SummaryAggregator {
206
222
  this.knownTextPartIds.clear();
207
223
  this.processedToolStates.clear();
208
224
  this.thinkingFiredForMessages.clear();
225
+ this.trackedSessionParents.clear();
226
+ this.subagentStates.clear();
227
+ this.subagentOrder = [];
228
+ this.subagentCardIdBySessionId.clear();
229
+ this.pendingSubagentCardIdsByParent.clear();
230
+ this.pendingChildSessionIdsByParent.clear();
231
+ this.fallbackSubagentCardIdsByParent.clear();
209
232
  this.messageCount = 0;
210
233
  this.lastUpdated = 0;
211
234
  if (this.onClearedCallback) {
@@ -217,8 +240,362 @@ class SummaryAggregator {
217
240
  }
218
241
  }
219
242
  }
243
+ isTrackedChildSession(sessionId) {
244
+ return this.trackedSessionParents.has(sessionId) && sessionId !== this.currentSessionId;
245
+ }
246
+ getQueue(map, parentSessionId) {
247
+ const existing = map.get(parentSessionId);
248
+ if (existing) {
249
+ return existing;
250
+ }
251
+ const queue = [];
252
+ map.set(parentSessionId, queue);
253
+ return queue;
254
+ }
255
+ dequeue(map, parentSessionId) {
256
+ const queue = map.get(parentSessionId);
257
+ if (!queue || queue.length === 0) {
258
+ return undefined;
259
+ }
260
+ const value = queue.shift();
261
+ if (queue.length === 0) {
262
+ map.delete(parentSessionId);
263
+ }
264
+ return value;
265
+ }
266
+ removeFromQueue(map, parentSessionId, value) {
267
+ const queue = map.get(parentSessionId);
268
+ if (!queue) {
269
+ return;
270
+ }
271
+ const index = queue.indexOf(value);
272
+ if (index >= 0) {
273
+ queue.splice(index, 1);
274
+ }
275
+ if (queue.length === 0) {
276
+ map.delete(parentSessionId);
277
+ }
278
+ }
279
+ emitSubagentState() {
280
+ if (!this.currentSessionId || !this.onSubagentCallback || this.subagentOrder.length === 0) {
281
+ return;
282
+ }
283
+ const subagents = this.subagentOrder
284
+ .map((cardId) => this.subagentStates.get(cardId))
285
+ .filter((state) => Boolean(state))
286
+ .map((state) => ({
287
+ cardId: state.cardId,
288
+ sessionId: state.sessionId,
289
+ parentSessionId: state.parentSessionId,
290
+ agent: state.agent,
291
+ description: state.description,
292
+ prompt: state.prompt,
293
+ command: state.command,
294
+ status: state.status,
295
+ providerID: state.providerID,
296
+ modelID: state.modelID,
297
+ tokens: { ...state.tokens },
298
+ cost: state.cost,
299
+ currentTool: state.currentTool,
300
+ currentToolInput: state.currentToolInput ? { ...state.currentToolInput } : undefined,
301
+ currentToolTitle: state.currentToolTitle,
302
+ terminalMessage: state.terminalMessage,
303
+ updatedAt: state.updatedAt,
304
+ }));
305
+ this.onSubagentCallback(this.currentSessionId, subagents);
306
+ }
307
+ createSubagentState(parentSessionId, sessionId, cardId = `subagent-${parentSessionId}-${Date.now()}-${this.subagentOrder.length}`) {
308
+ const state = {
309
+ cardId,
310
+ sessionId,
311
+ parentSessionId,
312
+ agent: "",
313
+ description: "",
314
+ prompt: "",
315
+ status: "pending",
316
+ tokens: {
317
+ input: 0,
318
+ output: 0,
319
+ reasoning: 0,
320
+ cacheRead: 0,
321
+ cacheWrite: 0,
322
+ },
323
+ cost: 0,
324
+ terminalMessage: undefined,
325
+ updatedAt: Date.now(),
326
+ hasSubtaskMetadata: false,
327
+ hasTaskToolMetadata: false,
328
+ hasSessionTitleMetadata: false,
329
+ createdAt: Date.now(),
330
+ };
331
+ this.subagentStates.set(cardId, state);
332
+ this.subagentOrder.push(cardId);
333
+ if (sessionId) {
334
+ this.subagentCardIdBySessionId.set(sessionId, cardId);
335
+ }
336
+ return state;
337
+ }
338
+ enrichSubagentFromSubtask(state, details) {
339
+ state.agent = details.agent || state.agent;
340
+ state.description = details.description || details.prompt || state.description;
341
+ state.prompt = details.prompt;
342
+ state.command = details.command;
343
+ state.hasSubtaskMetadata = true;
344
+ state.updatedAt = Date.now();
345
+ }
346
+ enrichSubagentFromTaskTool(state, details) {
347
+ const nextDescription = details.description?.trim() || details.prompt?.trim();
348
+ if (details.agent?.trim()) {
349
+ state.agent = details.agent.trim();
350
+ }
351
+ if (nextDescription) {
352
+ state.description = nextDescription;
353
+ }
354
+ if (details.prompt?.trim()) {
355
+ state.prompt = details.prompt.trim();
356
+ }
357
+ if (details.command?.trim()) {
358
+ state.command = details.command.trim();
359
+ }
360
+ state.hasTaskToolMetadata = true;
361
+ state.updatedAt = Date.now();
362
+ }
363
+ enrichSubagentFromSessionTitle(state, title) {
364
+ const trimmedTitle = title?.trim();
365
+ if (!trimmedTitle) {
366
+ return;
367
+ }
368
+ const match = trimmedTitle.match(/^(.*?)(?:\s+\(@([^\s)]+)\s+subagent\))?$/i);
369
+ const rawDescription = match?.[1]?.trim() || trimmedTitle;
370
+ const rawAgent = match?.[2]?.trim();
371
+ if (rawDescription) {
372
+ state.description = rawDescription;
373
+ }
374
+ if (rawAgent) {
375
+ state.agent = rawAgent.replace(/^@/, "");
376
+ }
377
+ state.hasSessionTitleMetadata = true;
378
+ state.updatedAt = Date.now();
379
+ }
380
+ attachSessionToSubagent(cardId, sessionId) {
381
+ const state = this.subagentStates.get(cardId);
382
+ if (!state) {
383
+ return;
384
+ }
385
+ state.sessionId = sessionId;
386
+ state.updatedAt = Date.now();
387
+ this.subagentCardIdBySessionId.set(sessionId, cardId);
388
+ this.removeFromQueue(this.pendingSubagentCardIdsByParent, state.parentSessionId, cardId);
389
+ }
390
+ findPendingSubagentWithoutSession() {
391
+ for (const cardId of this.subagentOrder) {
392
+ const state = this.subagentStates.get(cardId);
393
+ if (state && !state.sessionId) {
394
+ return state;
395
+ }
396
+ }
397
+ return null;
398
+ }
399
+ attachUnknownSessionToPendingSubagent(sessionId) {
400
+ const pendingState = this.findPendingSubagentWithoutSession();
401
+ if (!pendingState) {
402
+ return false;
403
+ }
404
+ this.trackedSessionParents.set(sessionId, pendingState.parentSessionId);
405
+ this.attachSessionToSubagent(pendingState.cardId, sessionId);
406
+ this.removeFromQueue(this.pendingChildSessionIdsByParent, pendingState.parentSessionId, sessionId);
407
+ this.emitSubagentState();
408
+ return true;
409
+ }
410
+ findNextSubagentForTaskTool(parentSessionId) {
411
+ for (const cardId of this.subagentOrder) {
412
+ const state = this.subagentStates.get(cardId);
413
+ if (state && state.parentSessionId === parentSessionId && !state.hasTaskToolMetadata) {
414
+ return state;
415
+ }
416
+ }
417
+ return null;
418
+ }
419
+ updateSubagentFromTaskTool(parentSessionId, input) {
420
+ const subagent = this.findNextSubagentForTaskTool(parentSessionId);
421
+ if (!subagent || !input) {
422
+ return;
423
+ }
424
+ const description = typeof input.description === "string" ? input.description : undefined;
425
+ const prompt = typeof input.prompt === "string" ? input.prompt : undefined;
426
+ const agent = typeof input.subagent_type === "string" ? input.subagent_type : undefined;
427
+ const command = typeof input.command === "string" ? input.command : undefined;
428
+ if (!description && !prompt && !agent && !command) {
429
+ return;
430
+ }
431
+ this.enrichSubagentFromTaskTool(subagent, { agent, description, prompt, command });
432
+ this.emitSubagentState();
433
+ }
434
+ getOrCreateSubagentForSession(sessionId) {
435
+ const existingCardId = this.subagentCardIdBySessionId.get(sessionId);
436
+ if (existingCardId) {
437
+ return this.subagentStates.get(existingCardId);
438
+ }
439
+ const parentSessionId = this.trackedSessionParents.get(sessionId) ?? this.currentSessionId ?? sessionId;
440
+ this.removeFromQueue(this.pendingChildSessionIdsByParent, parentSessionId, sessionId);
441
+ const state = this.createSubagentState(parentSessionId, sessionId);
442
+ this.getQueue(this.fallbackSubagentCardIdsByParent, parentSessionId).push(state.cardId);
443
+ return state;
444
+ }
445
+ registerSubtaskPart(parentSessionId, partId, agent, description, prompt, command) {
446
+ const fallbackCardId = this.dequeue(this.fallbackSubagentCardIdsByParent, parentSessionId);
447
+ if (fallbackCardId) {
448
+ const fallbackState = this.subagentStates.get(fallbackCardId);
449
+ if (fallbackState) {
450
+ this.enrichSubagentFromSubtask(fallbackState, { agent, description, prompt, command });
451
+ this.emitSubagentState();
452
+ return;
453
+ }
454
+ }
455
+ const state = this.createSubagentState(parentSessionId, null, `subtask-${parentSessionId}-${partId}`);
456
+ this.enrichSubagentFromSubtask(state, { agent, description, prompt, command });
457
+ const pendingChildSessionId = this.dequeue(this.pendingChildSessionIdsByParent, parentSessionId);
458
+ if (pendingChildSessionId) {
459
+ this.attachSessionToSubagent(state.cardId, pendingChildSessionId);
460
+ }
461
+ else {
462
+ this.getQueue(this.pendingSubagentCardIdsByParent, parentSessionId).push(state.cardId);
463
+ }
464
+ this.emitSubagentState();
465
+ }
466
+ trackChildSession(sessionId, parentSessionId) {
467
+ this.trackedSessionParents.set(sessionId, parentSessionId);
468
+ const pendingCardId = this.dequeue(this.pendingSubagentCardIdsByParent, parentSessionId);
469
+ if (pendingCardId) {
470
+ this.attachSessionToSubagent(pendingCardId, sessionId);
471
+ this.emitSubagentState();
472
+ return;
473
+ }
474
+ this.getQueue(this.pendingChildSessionIdsByParent, parentSessionId).push(sessionId);
475
+ }
476
+ handleSessionCreatedOrUpdated(event) {
477
+ if (!this.currentSessionId) {
478
+ return;
479
+ }
480
+ const { info } = event.properties;
481
+ if (!info.parentID) {
482
+ return;
483
+ }
484
+ if (!this.trackedSessionParents.has(info.parentID)) {
485
+ return;
486
+ }
487
+ if (info.id === this.currentSessionId) {
488
+ return;
489
+ }
490
+ if (!this.trackedSessionParents.has(info.id)) {
491
+ this.trackChildSession(info.id, info.parentID);
492
+ }
493
+ const subagent = this.getOrCreateSubagentForSession(info.id);
494
+ this.enrichSubagentFromSessionTitle(subagent, info.title);
495
+ this.emitSubagentState();
496
+ }
497
+ updateSubagentFromAssistantMessage(info) {
498
+ const subagent = this.getOrCreateSubagentForSession(info.sessionID);
499
+ if (info.agent) {
500
+ subagent.agent = info.agent;
501
+ }
502
+ if (info.providerID) {
503
+ subagent.providerID = info.providerID;
504
+ }
505
+ if (info.modelID) {
506
+ subagent.modelID = info.modelID;
507
+ }
508
+ if (info.tokens) {
509
+ subagent.tokens = {
510
+ input: info.tokens.input,
511
+ output: info.tokens.output,
512
+ reasoning: info.tokens.reasoning,
513
+ cacheRead: info.tokens.cache?.read || 0,
514
+ cacheWrite: info.tokens.cache?.write || 0,
515
+ };
516
+ }
517
+ if (typeof info.cost === "number") {
518
+ subagent.cost = info.cost;
519
+ }
520
+ subagent.updatedAt = Date.now();
521
+ this.emitSubagentState();
522
+ }
523
+ updateSubagentToolState(sessionId, state, tool, input, title) {
524
+ const subagent = this.getOrCreateSubagentForSession(sessionId);
525
+ const status = "status" in state ? state.status : undefined;
526
+ if (status === "running") {
527
+ subagent.status = "running";
528
+ subagent.terminalMessage = undefined;
529
+ }
530
+ if (status === "pending" && subagent.status === "pending") {
531
+ subagent.status = "pending";
532
+ subagent.terminalMessage = undefined;
533
+ }
534
+ subagent.currentTool = tool;
535
+ subagent.currentToolInput = input ? { ...input } : undefined;
536
+ subagent.currentToolTitle = title;
537
+ subagent.updatedAt = Date.now();
538
+ this.emitSubagentState();
539
+ }
540
+ updateSubagentStepStart(sessionId, snapshot) {
541
+ const subagent = this.getOrCreateSubagentForSession(sessionId);
542
+ subagent.status = "running";
543
+ subagent.terminalMessage = undefined;
544
+ subagent.currentTool = undefined;
545
+ subagent.currentToolInput = undefined;
546
+ subagent.currentToolTitle = snapshot?.trim() || subagent.currentToolTitle;
547
+ subagent.updatedAt = Date.now();
548
+ this.emitSubagentState();
549
+ }
550
+ updateSubagentStepFinish(sessionId, tokens, cost, snapshot) {
551
+ const subagent = this.getOrCreateSubagentForSession(sessionId);
552
+ subagent.status = "running";
553
+ subagent.terminalMessage = undefined;
554
+ subagent.tokens = {
555
+ input: tokens.input,
556
+ output: tokens.output,
557
+ reasoning: tokens.reasoning,
558
+ cacheRead: tokens.cache.read,
559
+ cacheWrite: tokens.cache.write,
560
+ };
561
+ subagent.cost += cost;
562
+ if (snapshot?.trim()) {
563
+ subagent.currentToolTitle = snapshot.trim();
564
+ }
565
+ subagent.updatedAt = Date.now();
566
+ this.emitSubagentState();
567
+ }
568
+ setSubagentTerminalStatus(sessionId, status, terminalMessage) {
569
+ const cardId = this.subagentCardIdBySessionId.get(sessionId);
570
+ if (!cardId) {
571
+ return;
572
+ }
573
+ const subagent = this.subagentStates.get(cardId);
574
+ if (!subagent) {
575
+ return;
576
+ }
577
+ subagent.status = status;
578
+ subagent.currentTool = undefined;
579
+ subagent.currentToolInput = undefined;
580
+ subagent.currentToolTitle = undefined;
581
+ subagent.terminalMessage = terminalMessage?.trim() || undefined;
582
+ subagent.updatedAt = Date.now();
583
+ this.emitSubagentState();
584
+ }
220
585
  handleMessageUpdated(event) {
221
586
  const { info } = event.properties;
587
+ if (info.sessionID !== this.currentSessionId &&
588
+ !this.trackedSessionParents.has(info.sessionID) &&
589
+ info.role === "assistant") {
590
+ this.attachUnknownSessionToPendingSubagent(info.sessionID);
591
+ }
592
+ if (this.isTrackedChildSession(info.sessionID)) {
593
+ if (info.role === "assistant") {
594
+ const assistantInfo = info;
595
+ this.updateSubagentFromAssistantMessage(assistantInfo);
596
+ }
597
+ return;
598
+ }
222
599
  if (info.sessionID !== this.currentSessionId) {
223
600
  return;
224
601
  }
@@ -242,23 +619,24 @@ class SummaryAggregator {
242
619
  if (!isCompleted && textState.optimisticUpdateCount === 1) {
243
620
  this.emitPartialText(info.sessionID, messageID, messageText);
244
621
  }
622
+ // Extract and report tokens for EVERY message.updated with token data
623
+ // (both intermediate and completed). This keeps keyboard context in sync.
624
+ const assistantInfo = info;
625
+ if (this.onTokensCallback && assistantInfo.tokens) {
626
+ const tokens = {
627
+ input: assistantInfo.tokens.input,
628
+ output: assistantInfo.tokens.output,
629
+ reasoning: assistantInfo.tokens.reasoning,
630
+ cacheRead: assistantInfo.tokens.cache?.read || 0,
631
+ cacheWrite: assistantInfo.tokens.cache?.write || 0,
632
+ };
633
+ logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}, cacheRead=${tokens.cacheRead}, cacheWrite=${tokens.cacheWrite}, completed=${isCompleted}`);
634
+ // Call synchronously so keyboardManager is updated before onComplete sends the reply
635
+ this.onTokensCallback(tokens, isCompleted);
636
+ }
245
637
  if (isCompleted) {
246
638
  const finalText = messageText;
247
639
  logger.debug(`[Aggregator] Message part completed: messageId=${messageID}, textLength=${finalText.length}, totalParts=${textState.orderedPartIds.length}, session=${this.currentSessionId}`);
248
- // Extract and report tokens BEFORE onComplete so keyboard context is updated
249
- const assistantInfo = info;
250
- if (this.onTokensCallback && assistantInfo.tokens) {
251
- const tokens = {
252
- input: assistantInfo.tokens.input,
253
- output: assistantInfo.tokens.output,
254
- reasoning: assistantInfo.tokens.reasoning,
255
- cacheRead: assistantInfo.tokens.cache?.read || 0,
256
- cacheWrite: assistantInfo.tokens.cache?.write || 0,
257
- };
258
- logger.debug(`[Aggregator] Tokens: input=${tokens.input}, output=${tokens.output}, reasoning=${tokens.reasoning}`);
259
- // Call synchronously so keyboardManager is updated before onComplete sends the reply
260
- this.onTokensCallback(tokens);
261
- }
262
640
  // Extract and report cost
263
641
  if (this.onCostCallback && assistantInfo.cost !== undefined) {
264
642
  logger.debug(`[Aggregator] Cost: $${assistantInfo.cost.toFixed(2)}`);
@@ -282,7 +660,35 @@ class SummaryAggregator {
282
660
  }
283
661
  handleMessagePartUpdated(event) {
284
662
  const { part } = event.properties;
285
- if (part.sessionID !== this.currentSessionId) {
663
+ if (part.sessionID !== this.currentSessionId &&
664
+ !this.trackedSessionParents.has(part.sessionID) &&
665
+ part.type !== "subtask") {
666
+ this.attachUnknownSessionToPendingSubagent(part.sessionID);
667
+ }
668
+ const isCurrentRootSession = part.sessionID === this.currentSessionId;
669
+ const isTrackedChildSession = this.isTrackedChildSession(part.sessionID);
670
+ if (!isCurrentRootSession && !isTrackedChildSession) {
671
+ return;
672
+ }
673
+ if (part.type === "subtask") {
674
+ this.registerSubtaskPart(part.sessionID, part.id, part.agent, part.description, part.prompt, part.command);
675
+ this.lastUpdated = Date.now();
676
+ return;
677
+ }
678
+ if (isTrackedChildSession) {
679
+ if (part.type === "tool") {
680
+ const state = part.state;
681
+ const input = "input" in state ? state.input : undefined;
682
+ const title = "title" in state ? state.title : undefined;
683
+ this.updateSubagentToolState(part.sessionID, state, part.tool, input, title);
684
+ }
685
+ if (part.type === "step-start") {
686
+ this.updateSubagentStepStart(part.sessionID, part.snapshot);
687
+ }
688
+ if (part.type === "step-finish") {
689
+ this.updateSubagentStepFinish(part.sessionID, part.tokens, part.cost, part.snapshot);
690
+ }
691
+ this.lastUpdated = Date.now();
286
692
  return;
287
693
  }
288
694
  const messageID = part.messageID;
@@ -337,6 +743,9 @@ class SummaryAggregator {
337
743
  const state = part.state;
338
744
  const input = "input" in state ? state.input : undefined;
339
745
  const title = "title" in state ? state.title : undefined;
746
+ if (part.tool === "task") {
747
+ this.updateSubagentFromTaskTool(part.sessionID, input);
748
+ }
340
749
  logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
341
750
  if (part.tool === "question") {
342
751
  logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
@@ -618,6 +1027,11 @@ class SummaryAggregator {
618
1027
  }
619
1028
  handleSessionIdle(event) {
620
1029
  const { sessionID } = event.properties;
1030
+ if (this.isTrackedChildSession(sessionID)) {
1031
+ logger.info(`[Aggregator] Subagent session became idle: ${sessionID}`);
1032
+ this.setSubagentTerminalStatus(sessionID, "completed");
1033
+ return;
1034
+ }
621
1035
  if (sessionID !== this.currentSessionId) {
622
1036
  return;
623
1037
  }
@@ -644,10 +1058,15 @@ class SummaryAggregator {
644
1058
  }
645
1059
  handleSessionError(event) {
646
1060
  const { sessionID, error } = event.properties;
1061
+ const message = error?.data?.message || error?.message || error?.name || "Unknown session error";
1062
+ if (sessionID && this.isTrackedChildSession(sessionID)) {
1063
+ logger.warn(`[Aggregator] Subagent session error: ${sessionID}: ${message}`);
1064
+ this.setSubagentTerminalStatus(sessionID, "error", message);
1065
+ return;
1066
+ }
647
1067
  if (sessionID !== this.currentSessionId) {
648
1068
  return;
649
1069
  }
650
- const message = error?.data?.message || error?.message || error?.name || "Unknown session error";
651
1070
  logger.warn(`[Aggregator] Session error: ${sessionID}: ${message}`);
652
1071
  this.stopTypingIndicator();
653
1072
  if (this.onSessionErrorCallback) {
@@ -6,18 +6,44 @@ import { t } from "../i18n/index.js";
6
6
  import { getCurrentProject } from "../settings/manager.js";
7
7
  const TELEGRAM_MESSAGE_LIMIT = 4096;
8
8
  const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
9
- function splitText(text, maxLength) {
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) {
10
38
  const parts = [];
11
39
  let currentIndex = 0;
12
40
  while (currentIndex < text.length) {
13
- let endIndex = currentIndex + maxLength;
14
- if (endIndex >= text.length) {
15
- parts.push(text.slice(currentIndex));
16
- break;
17
- }
18
- const breakPoint = text.lastIndexOf("\n", endIndex);
19
- if (breakPoint > currentIndex) {
20
- endIndex = breakPoint + 1;
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;
21
47
  }
22
48
  parts.push(text.slice(currentIndex, endIndex));
23
49
  currentIndex = endIndex;
@@ -192,7 +218,9 @@ export function formatSummaryWithMode(text, mode, maxLength = TELEGRAM_MESSAGE_L
192
218
  }
193
219
  if (mode === "markdown") {
194
220
  const converted = formatMarkdownForTelegram(trimmed);
195
- const convertedParts = splitText(converted, normalizedMaxLength);
221
+ const convertedParts = splitText(converted, normalizedMaxLength, {
222
+ avoidTrailingMarkdownEscape: true,
223
+ });
196
224
  for (const convertedPart of convertedParts) {
197
225
  const normalizedPart = convertedPart.trim();
198
226
  if (normalizedPart) {
@@ -343,7 +371,7 @@ export function formatToolInfo(toolInfo) {
343
371
  const todos = toolInfo.metadata.todos;
344
372
  const toolIcon = getToolIcon(tool);
345
373
  const todosList = formatTodos(todos);
346
- return `${toolIcon} ${tool} (${todos.length})\n${todosList}`;
374
+ return `${toolIcon} ${tool} (${todos.length})\n\n${todosList}`;
347
375
  }
348
376
  let details = title || getToolDetails(tool, input);
349
377
  const toolIcon = getToolIcon(tool);
@@ -393,6 +421,17 @@ export function formatToolInfo(toolInfo) {
393
421
  }
394
422
  return `${toolIcon} ${description}${tool}${detailsStr}${lineInfo}`;
395
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
+ }
396
435
  function countLines(text) {
397
436
  return text.split("\n").length;
398
437
  }
@@ -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
+ }