@grinev/opencode-telegram-bot 0.21.2 → 0.22.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.
Files changed (37) hide show
  1. package/.env.example +0 -15
  2. package/README.md +15 -8
  3. package/dist/app/formatters/summary-formatter.js +16 -0
  4. package/dist/app/managers/summary-aggregation-manager.js +174 -10
  5. package/dist/app/services/file-download-service.js +51 -2
  6. package/dist/app/services/scheduled-task-executor-service.js +22 -7
  7. package/dist/app/stores/settings-store.js +46 -4
  8. package/dist/bot/callbacks/callback-router.js +4 -1
  9. package/dist/bot/callbacks/scheduled-task-callback-handler.js +20 -1
  10. package/dist/bot/callbacks/settings-callback-handler.js +105 -0
  11. package/dist/bot/commands/definitions.js +1 -1
  12. package/dist/bot/commands/settings-command.js +10 -0
  13. package/dist/bot/commands/status-command.js +7 -2
  14. package/dist/bot/handlers/document-handler.js +1 -1
  15. package/dist/bot/handlers/media-group-handler.js +1 -1
  16. package/dist/bot/handlers/prompt.js +3 -3
  17. package/dist/bot/handlers/voice-handler.js +5 -2
  18. package/dist/bot/menus/inline-menu.js +3 -1
  19. package/dist/bot/menus/settings-menu.js +52 -0
  20. package/dist/bot/messages/thinking-message.js +1 -4
  21. package/dist/bot/messages/thinking-rendering.js +66 -0
  22. package/dist/bot/routers/command-router.js +2 -2
  23. package/dist/bot/services/event-subscription-service.js +304 -30
  24. package/dist/bot/streaming/compact-progress-streamer.js +168 -0
  25. package/dist/bot/streaming/finalize-assistant-response.js +9 -3
  26. package/dist/bot/streaming/response-streamer.js +16 -1
  27. package/dist/config.js +0 -15
  28. package/dist/i18n/ar.js +31 -6
  29. package/dist/i18n/de.js +32 -7
  30. package/dist/i18n/en.js +32 -7
  31. package/dist/i18n/es.js +32 -7
  32. package/dist/i18n/fr.js +32 -7
  33. package/dist/i18n/ru.js +32 -7
  34. package/dist/i18n/zh.js +32 -7
  35. package/dist/opencode/process.js +40 -3
  36. package/package.json +1 -1
  37. package/dist/bot/commands/tts-command.js +0 -13
@@ -5,9 +5,10 @@ import { InputFile } from "grammy";
5
5
  import { config } from "../../config.js";
6
6
  import { t } from "../../i18n/index.js";
7
7
  import { summaryAggregator } from "../../app/managers/summary-aggregation-manager.js";
8
- import { formatToolInfo } from "../../app/formatters/summary-formatter.js";
8
+ import { formatCompactToolActivity, formatToolInfo } from "../../app/formatters/summary-formatter.js";
9
9
  import { renderSubagentCards } from "../../app/formatters/subagent-formatter.js";
10
10
  import { ToolMessageBatcher } from "../../app/formatters/tool-message-batcher.js";
11
+ import { getCompactOutputMode, getResponseStreamingMode, getSendDiffFileAttachments, getShowAssistantRunFooter, getShowThinkingContent, } from "../../app/stores/settings-store.js";
11
12
  import { getCurrentSession } from "../../app/services/session-service.js";
12
13
  import { ingestSessionInfoForCache } from "../../app/services/session-cache-service.js";
13
14
  import { logger } from "../../utils/logger.js";
@@ -27,10 +28,12 @@ import { scheduledTaskRuntime } from "../../app/services/scheduled-task-runtime-
27
28
  import { assistantRunState } from "../../app/managers/assistant-run-state-manager.js";
28
29
  import { ResponseStreamer } from "../streaming/response-streamer.js";
29
30
  import { ToolCallStreamer } from "../streaming/tool-call-streamer.js";
31
+ import { CompactProgressStreamer } from "../streaming/compact-progress-streamer.js";
30
32
  import { attachManager } from "../../app/managers/attach-manager.js";
31
33
  import { markAttachedSessionBusy, markAttachedSessionIdle, } from "../../app/services/attach-service.js";
32
34
  import { externalUserInputSuppressionManager } from "../../app/managers/external-input-suppression-manager.js";
33
35
  import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "../messages/assistant-rendering.js";
36
+ import { makeThinkingPayloadExpandable, prepareThinkingStreamingPayload, } from "../messages/thinking-rendering.js";
34
37
  import { deliverExternalUserInputNotification } from "../messages/external-user-input-notification.js";
35
38
  import { backgroundSessionTracker, } from "../../app/managers/background-session-manager.js";
36
39
  import { buildBackgroundSessionOpenKeyboard } from "../menus/session-selection-menu.js";
@@ -41,13 +44,15 @@ import { clearAllInteractionState } from "../../app/managers/interaction-manager
41
44
  import { stopEventListening, subscribeToEvents } from "../../opencode/events.js";
42
45
  const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
43
46
  const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
44
- const RESPONSE_STREAMING_MODE = config.bot.responseStreamingMode;
45
47
  const RESPONSE_STREAM_TEXT_LIMIT = 3800;
46
48
  const SESSION_RETRY_PREFIX = "🔁";
47
49
  const SUBAGENT_STREAM_PREFIX = "🧩";
48
50
  const __filename = fileURLToPath(import.meta.url);
49
51
  const __dirname = path.dirname(__filename);
50
52
  const TEMP_DIR = path.join(__dirname, "..", "..", ".tmp");
53
+ function isCompactProgressMode() {
54
+ return getCompactOutputMode();
55
+ }
51
56
  export function createEventSubscriptionService() {
52
57
  return new EventSubscriptionService();
53
58
  }
@@ -55,10 +60,16 @@ class EventSubscriptionService {
55
60
  botInstance = null;
56
61
  chatIdInstance = null;
57
62
  nextDraftId = 1;
63
+ thinkingStreamingPayloads = new Map();
58
64
  sessionCompletionTasks = new Map();
59
- responseStreamer;
65
+ compactProgressFinalizationTasks = new Map();
66
+ assistantEditResponseStreamer;
67
+ assistantDraftResponseStreamer;
68
+ thinkingResponseStreamer;
69
+ assistantResponseStreamModes = new Map();
60
70
  toolCallStreamer;
61
71
  toolMessageBatcher;
72
+ compactProgressStreamer;
62
73
  constructor() {
63
74
  this.toolMessageBatcher = new ToolMessageBatcher({
64
75
  sendText: async (sessionId, text) => {
@@ -100,9 +111,48 @@ class EventSubscriptionService {
100
111
  }
101
112
  },
102
113
  });
103
- this.responseStreamer = this.createResponseStreamer();
104
- setResponseStreamerForReconciliation(this.responseStreamer);
114
+ this.assistantEditResponseStreamer = this.createResponseStreamer("edit");
115
+ this.assistantDraftResponseStreamer = this.createResponseStreamer("draft");
116
+ this.thinkingResponseStreamer = this.createResponseStreamer("edit");
117
+ setResponseStreamerForReconciliation({
118
+ hasActiveStream: (sessionId) => this.hasActiveAssistantResponseStream(sessionId),
119
+ });
105
120
  setPromptResponseModeClearerForReconciliation(clearPromptResponseMode);
121
+ this.compactProgressStreamer = new CompactProgressStreamer({
122
+ throttleMs: RESPONSE_STREAM_THROTTLE_MS,
123
+ sendText: async (sessionId, text) => {
124
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
125
+ throw new Error("Bot context missing for compact progress send");
126
+ }
127
+ const currentSession = getCurrentSession();
128
+ if (!currentSession || currentSession.id !== sessionId) {
129
+ throw new Error(`Compact progress session mismatch for send: ${sessionId}`);
130
+ }
131
+ const sentMessage = await this.botInstance.api.sendMessage(this.chatIdInstance, text, {
132
+ disable_notification: true,
133
+ });
134
+ return sentMessage.message_id;
135
+ },
136
+ editText: async (sessionId, messageId, text) => {
137
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
138
+ throw new Error("Bot context missing for compact progress edit");
139
+ }
140
+ const currentSession = getCurrentSession();
141
+ if (!currentSession || currentSession.id !== sessionId) {
142
+ throw new Error(`Compact progress session mismatch for edit: ${sessionId}`);
143
+ }
144
+ try {
145
+ await this.botInstance.api.editMessageText(this.chatIdInstance, messageId, text);
146
+ }
147
+ catch (error) {
148
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
149
+ if (errorMessage.includes("message is not modified")) {
150
+ return;
151
+ }
152
+ throw error;
153
+ }
154
+ },
155
+ });
106
156
  this.toolCallStreamer = new ToolCallStreamer({
107
157
  throttleMs: RESPONSE_STREAM_THROTTLE_MS,
108
158
  sendText: async (sessionId, text) => {
@@ -163,9 +213,12 @@ class EventSubscriptionService {
163
213
  clearRuntimeState(reason) {
164
214
  backgroundSessionTracker.clear();
165
215
  this.nextDraftId = 1;
166
- this.responseStreamer.clearAll(reason);
216
+ this.clearAllResponseStreams(reason);
167
217
  this.toolCallStreamer.clearAll(reason);
168
218
  this.toolMessageBatcher.clearAll(reason);
219
+ this.compactProgressStreamer.clearAll(reason);
220
+ this.compactProgressFinalizationTasks.clear();
221
+ this.thinkingStreamingPayloads.clear();
169
222
  this.sessionCompletionTasks.clear();
170
223
  assistantRunState.clearAll(reason);
171
224
  }
@@ -189,7 +242,10 @@ class EventSubscriptionService {
189
242
  summaryAggregator.setOnCleared(() => {
190
243
  this.toolMessageBatcher.clearAll("summary_aggregator_clear");
191
244
  this.toolCallStreamer.clearAll("summary_aggregator_clear");
192
- this.responseStreamer.clearAll("summary_aggregator_clear");
245
+ this.clearAllResponseStreams("summary_aggregator_clear");
246
+ this.compactProgressStreamer.clearAll("summary_aggregator_clear");
247
+ this.compactProgressFinalizationTasks.clear();
248
+ this.thinkingStreamingPayloads.clear();
193
249
  });
194
250
  summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
195
251
  if (!this.botInstance || !this.chatIdInstance) {
@@ -199,21 +255,43 @@ class EventSubscriptionService {
199
255
  if (!currentSession || currentSession.id !== sessionId) {
200
256
  return;
201
257
  }
258
+ if (isCompactProgressMode()) {
259
+ void this.finalizeCompactProgress(sessionId)
260
+ .then(() => {
261
+ const activeSession = getCurrentSession();
262
+ if (!activeSession || activeSession.id !== sessionId) {
263
+ return;
264
+ }
265
+ const preparedStreamPayload = this.prepareStreamingPayload(messageText);
266
+ if (!preparedStreamPayload) {
267
+ return;
268
+ }
269
+ preparedStreamPayload.sendOptions = { disable_notification: true };
270
+ preparedStreamPayload.editOptions = undefined;
271
+ this.enqueueAssistantResponse(sessionId, messageId, preparedStreamPayload);
272
+ })
273
+ .catch((error) => {
274
+ logger.error("[Bot] Failed to finalize compact progress before assistant stream", error);
275
+ });
276
+ return;
277
+ }
202
278
  const preparedStreamPayload = this.prepareStreamingPayload(messageText);
203
279
  if (!preparedStreamPayload) {
204
280
  return;
205
281
  }
206
282
  preparedStreamPayload.sendOptions = { disable_notification: true };
207
283
  preparedStreamPayload.editOptions = undefined;
208
- this.responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
284
+ this.enqueueAssistantResponse(sessionId, messageId, preparedStreamPayload);
209
285
  });
210
286
  summaryAggregator.setOnComplete((sessionId, messageId, messageText, completionInfo) => {
211
287
  void this.enqueueSessionCompletionTask(sessionId, async () => {
212
288
  if (!this.botInstance || !this.chatIdInstance) {
213
289
  logger.error("Bot or chat ID not available for sending message");
214
290
  clearPromptResponseMode(sessionId);
215
- this.responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
291
+ this.clearAssistantResponseStream(sessionId, messageId, "bot_context_missing");
292
+ this.clearThinkingStream(sessionId, messageId, "bot_context_missing");
216
293
  this.toolCallStreamer.clearSession(sessionId, "bot_context_missing");
294
+ this.compactProgressStreamer.clearSession(sessionId, "bot_context_missing");
217
295
  assistantRunState.clearRun(sessionId, "bot_context_missing");
218
296
  foregroundSessionState.markIdle(sessionId);
219
297
  return;
@@ -221,8 +299,10 @@ class EventSubscriptionService {
221
299
  const currentSession = getCurrentSession();
222
300
  if (currentSession?.id !== sessionId) {
223
301
  clearPromptResponseMode(sessionId);
224
- this.responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
302
+ this.clearAssistantResponseStream(sessionId, messageId, "session_mismatch");
303
+ this.clearThinkingStream(sessionId, messageId, "session_mismatch");
225
304
  this.toolCallStreamer.clearSession(sessionId, "session_mismatch");
305
+ this.compactProgressStreamer.clearSession(sessionId, "session_mismatch");
226
306
  assistantRunState.clearRun(sessionId, "session_mismatch");
227
307
  foregroundSessionState.markIdle(sessionId);
228
308
  await scheduledTaskRuntime.flushDeferredDeliveries();
@@ -236,11 +316,18 @@ class EventSubscriptionService {
236
316
  providerID: completionInfo.providerID,
237
317
  modelID: completionInfo.modelID,
238
318
  });
319
+ await this.completeThinkingStream(sessionId, messageId);
320
+ if (isCompactProgressMode()) {
321
+ await this.finalizeCompactProgress(sessionId);
322
+ }
323
+ const assistantResponseMode = this.getAssistantResponseStreamMode(sessionId, messageId);
239
324
  await finalizeAssistantResponse({
240
325
  sessionId,
241
326
  messageId,
242
327
  messageText,
243
- responseStreamer: this.responseStreamer,
328
+ responseStreamer: {
329
+ complete: (completeSessionId, completeMessageId, payload, options) => this.completeAssistantResponse(completeSessionId, completeMessageId, payload, options),
330
+ },
244
331
  flushPendingServiceMessages: () => Promise.all([
245
332
  this.toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
246
333
  this.toolCallStreamer.breakSession(sessionId, "assistant_message_completed"),
@@ -248,6 +335,7 @@ class EventSubscriptionService {
248
335
  prepareStreamingPayload: this.prepareFinalStreamingPayload,
249
336
  renderFinalParts: (text) => renderAssistantFinalPartsSafe(text),
250
337
  getReplyKeyboard: this.getCurrentReplyKeyboard,
338
+ notifyFirstFinalPart: assistantResponseMode === "draft" && !getShowAssistantRunFooter(),
251
339
  sendRenderedPart: async (part, options) => {
252
340
  await sendRenderedBotPart({
253
341
  api: botApi,
@@ -266,6 +354,8 @@ class EventSubscriptionService {
266
354
  }
267
355
  catch (err) {
268
356
  clearPromptResponseMode(sessionId);
357
+ this.clearThinkingStream(sessionId, messageId, "assistant_finalize_failed");
358
+ this.compactProgressStreamer.clearSession(sessionId, "assistant_finalize_failed");
269
359
  assistantRunState.clearRun(sessionId, "assistant_finalize_failed");
270
360
  logger.error("Failed to send message to Telegram:", err);
271
361
  logger.error("[Bot] CRITICAL: Stopping event processing due to error");
@@ -297,6 +387,22 @@ class EventSubscriptionService {
297
387
  }
298
388
  });
299
389
  });
390
+ summaryAggregator.setOnRootToolUpdate((toolInfo) => {
391
+ if (!isCompactProgressMode()) {
392
+ return;
393
+ }
394
+ const currentSession = getCurrentSession();
395
+ if (!currentSession || currentSession.id !== toolInfo.sessionId) {
396
+ return;
397
+ }
398
+ const activity = this.getCompactToolActivity(toolInfo);
399
+ if (activity) {
400
+ this.compactProgressStreamer.updateActivity(toolInfo.sessionId, activity);
401
+ }
402
+ if ("status" in toolInfo.state && toolInfo.state.status === "completed") {
403
+ this.compactProgressStreamer.addToolCall(toolInfo.sessionId, toolInfo.callId);
404
+ }
405
+ });
300
406
  summaryAggregator.setOnTool(async (toolInfo) => {
301
407
  if (!this.botInstance || !this.chatIdInstance) {
302
408
  logger.error("Bot or chat ID not available for sending tool notification");
@@ -306,11 +412,13 @@ class EventSubscriptionService {
306
412
  if (!currentSession || currentSession.id !== toolInfo.sessionId) {
307
413
  return;
308
414
  }
309
- const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
415
+ if (isCompactProgressMode()) {
416
+ return;
417
+ }
418
+ const shouldSendToolFileAttachment = toolInfo.hasFileAttachment &&
419
+ getSendDiffFileAttachments() &&
310
420
  (toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
311
- if (config.bot.hideToolCallMessages ||
312
- shouldIncludeToolInfoInFileCaption ||
313
- toolInfo.tool === "task") {
421
+ if (shouldSendToolFileAttachment || toolInfo.tool === "task") {
314
422
  return;
315
423
  }
316
424
  try {
@@ -327,7 +435,7 @@ class EventSubscriptionService {
327
435
  if (!this.botInstance || !this.chatIdInstance) {
328
436
  return;
329
437
  }
330
- if (config.bot.hideToolCallMessages) {
438
+ if (isCompactProgressMode()) {
331
439
  return;
332
440
  }
333
441
  const currentSession = getCurrentSession();
@@ -354,7 +462,10 @@ class EventSubscriptionService {
354
462
  if (!currentSession || currentSession.id !== fileInfo.sessionId) {
355
463
  return;
356
464
  }
357
- if (config.bot.hideToolFileMessages) {
465
+ if (isCompactProgressMode()) {
466
+ return;
467
+ }
468
+ if (!getSendDiffFileAttachments()) {
358
469
  return;
359
470
  }
360
471
  try {
@@ -379,6 +490,9 @@ class EventSubscriptionService {
379
490
  if (!currentSession || currentSession.id !== sessionId) {
380
491
  return;
381
492
  }
493
+ if (isCompactProgressMode()) {
494
+ this.compactProgressStreamer.updateWaitingForQuestion(sessionId);
495
+ }
382
496
  await Promise.all([
383
497
  this.toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
384
498
  this.toolCallStreamer.flushSession(currentSession.id, "question_asked"),
@@ -418,6 +532,9 @@ class EventSubscriptionService {
418
532
  if (!currentSession || (!isCurrent && !isSubagent)) {
419
533
  return;
420
534
  }
535
+ if (isCompactProgressMode()) {
536
+ this.compactProgressStreamer.updateWaitingForPermission(currentSession.id);
537
+ }
421
538
  await Promise.all([
422
539
  this.toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
423
540
  this.toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
@@ -425,23 +542,63 @@ class EventSubscriptionService {
425
542
  logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}, subagent=${isSubagent}`);
426
543
  await showPermissionRequest(this.botInstance.api, this.chatIdInstance, request);
427
544
  });
428
- summaryAggregator.setOnThinking(async (sessionId) => {
545
+ summaryAggregator.setOnThinking(async (update) => {
429
546
  if (!this.botInstance || !this.chatIdInstance) {
430
547
  return;
431
548
  }
432
549
  const currentSession = getCurrentSession();
433
- if (!currentSession || currentSession.id !== sessionId) {
550
+ if (!currentSession || currentSession.id !== update.sessionId) {
434
551
  return;
435
552
  }
436
- logger.debug("[Bot] Agent started thinking");
437
- await this.toolCallStreamer.breakSession(sessionId, "thinking_started");
438
- deliverThinkingMessage(sessionId, this.toolMessageBatcher, {
439
- hideThinkingMessages: config.bot.hideThinkingMessages,
553
+ logger.debug("[Bot] Agent thinking update", {
554
+ sessionId: update.sessionId,
555
+ messageId: update.messageId,
556
+ sectionCount: update.sections.length,
557
+ isFirstUpdate: update.isFirstUpdate,
440
558
  });
441
- if (pinnedMessageManager.isInitialized()) {
559
+ if (isCompactProgressMode()) {
560
+ this.compactProgressStreamer.updateThinking(update.sessionId);
561
+ if (update.isFirstUpdate && pinnedMessageManager.isInitialized()) {
562
+ await pinnedMessageManager.refresh();
563
+ }
564
+ return;
565
+ }
566
+ if (update.isFirstUpdate) {
567
+ void this.toolCallStreamer.breakSession(update.sessionId, "thinking_started").catch((error) => {
568
+ logger.error("[Bot] Failed to break tool stream before thinking message", error);
569
+ });
570
+ }
571
+ if (getShowThinkingContent()) {
572
+ const payload = prepareThinkingStreamingPayload(update.sections, RESPONSE_STREAM_TEXT_LIMIT, {
573
+ expandable: false,
574
+ });
575
+ if (payload) {
576
+ payload.sendOptions = { disable_notification: true };
577
+ payload.editOptions = undefined;
578
+ this.thinkingStreamingPayloads.set(this.getThinkingPayloadKey(update.sessionId, update.messageId), payload);
579
+ this.thinkingResponseStreamer.enqueue(update.sessionId, this.getThinkingStreamId(update.messageId), payload);
580
+ }
581
+ }
582
+ else if (update.isFirstUpdate) {
583
+ deliverThinkingMessage(update.sessionId, this.toolMessageBatcher);
584
+ }
585
+ if (update.isFirstUpdate && pinnedMessageManager.isInitialized()) {
442
586
  await pinnedMessageManager.refresh();
443
587
  }
444
588
  });
589
+ summaryAggregator.setOnThinkingFinished((sessionId, messageId) => {
590
+ if (!this.botInstance || !this.chatIdInstance) {
591
+ return;
592
+ }
593
+ const currentSession = getCurrentSession();
594
+ if (!currentSession || currentSession.id !== sessionId) {
595
+ return;
596
+ }
597
+ logger.debug("[Bot] Agent thinking finished", { sessionId, messageId });
598
+ void this.completeThinkingStream(sessionId, messageId).catch((error) => {
599
+ logger.error("[Bot] Failed to finalize thinking stream early", error);
600
+ });
601
+ });
445
602
  summaryAggregator.setOnTokens(async (tokens, isCompleted) => {
446
603
  if (!pinnedMessageManager.isInitialized()) {
447
604
  return;
@@ -510,7 +667,7 @@ class EventSubscriptionService {
510
667
  this.toolMessageBatcher.flushSession(sessionId, "session_idle"),
511
668
  this.toolCallStreamer.flushSession(sessionId, "session_idle"),
512
669
  ]);
513
- if (completedRun?.hasCompletedResponse) {
670
+ if (getShowAssistantRunFooter() && completedRun?.hasCompletedResponse) {
514
671
  const agent = completedRun.actualAgent || completedRun.configuredAgent;
515
672
  const providerID = completedRun.actualProviderID || completedRun.configuredProviderID;
516
673
  const modelID = completedRun.actualModelID || completedRun.configuredModelID;
@@ -539,6 +696,7 @@ class EventSubscriptionService {
539
696
  await markAttachedSessionIdle(sessionId);
540
697
  if (!this.botInstance || !this.chatIdInstance) {
541
698
  clearPromptResponseMode(sessionId);
699
+ this.compactProgressStreamer.clearSession(sessionId, "session_error_no_bot_context");
542
700
  assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
543
701
  foregroundSessionState.markIdle(sessionId);
544
702
  return;
@@ -546,14 +704,16 @@ class EventSubscriptionService {
546
704
  const currentSession = getCurrentSession();
547
705
  if (!currentSession || currentSession.id !== sessionId) {
548
706
  clearPromptResponseMode(sessionId);
549
- this.responseStreamer.clearSession(sessionId, "session_error_not_current");
707
+ this.clearAssistantResponseSession(sessionId, "session_error_not_current");
550
708
  this.toolCallStreamer.clearSession(sessionId, "session_error_not_current");
709
+ this.compactProgressStreamer.clearSession(sessionId, "session_error_not_current");
551
710
  assistantRunState.clearRun(sessionId, "session_error_not_current");
552
711
  foregroundSessionState.markIdle(sessionId);
553
712
  await scheduledTaskRuntime.flushDeferredDeliveries();
554
713
  return;
555
714
  }
556
- this.responseStreamer.clearSession(sessionId, "session_error");
715
+ this.clearAssistantResponseSession(sessionId, "session_error");
716
+ this.compactProgressStreamer.clearSession(sessionId, "session_error");
557
717
  clearPromptResponseMode(sessionId);
558
718
  assistantRunState.clearRun(sessionId, "session_error");
559
719
  await Promise.all([
@@ -586,6 +746,10 @@ class EventSubscriptionService {
586
746
  if (!currentSession || currentSession.id !== sessionId) {
587
747
  return;
588
748
  }
749
+ if (isCompactProgressMode()) {
750
+ this.compactProgressStreamer.updateActivity(sessionId, t("progress.compact.retrying"));
751
+ return;
752
+ }
589
753
  const normalizedMessage = message.trim() || t("common.unknown_error");
590
754
  const truncatedMessage = normalizedMessage.length > 3500
591
755
  ? `${normalizedMessage.slice(0, 3497)}...`
@@ -593,7 +757,12 @@ class EventSubscriptionService {
593
757
  const retryMessage = t("bot.session_retry", { message: truncatedMessage });
594
758
  this.toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
595
759
  });
596
- summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
760
+ summaryAggregator.setOnSessionDiff(async (sessionId, diffs) => {
761
+ if (isCompactProgressMode()) {
762
+ for (const diff of diffs) {
763
+ this.compactProgressStreamer.addFileChange(sessionId, diff.file);
764
+ }
765
+ }
597
766
  if (!pinnedMessageManager.isInitialized()) {
598
767
  return;
599
768
  }
@@ -605,6 +774,12 @@ class EventSubscriptionService {
605
774
  }
606
775
  });
607
776
  summaryAggregator.setOnFileChange((change) => {
777
+ if (isCompactProgressMode()) {
778
+ const currentSession = getCurrentSession();
779
+ if (currentSession) {
780
+ this.compactProgressStreamer.addFileChange(currentSession.id, change.file);
781
+ }
782
+ }
608
783
  if (!pinnedMessageManager.isInitialized()) {
609
784
  return;
610
785
  }
@@ -648,8 +823,57 @@ class EventSubscriptionService {
648
823
  logger.error("Failed to subscribe to events:", err);
649
824
  });
650
825
  };
651
- createResponseStreamer() {
652
- if (RESPONSE_STREAMING_MODE === "draft") {
826
+ getAssistantResponseStreamKey(sessionId, messageId) {
827
+ return `${sessionId}:${messageId}`;
828
+ }
829
+ getAssistantResponseStreamer(mode) {
830
+ return mode === "draft"
831
+ ? this.assistantDraftResponseStreamer
832
+ : this.assistantEditResponseStreamer;
833
+ }
834
+ getAssistantResponseStreamMode(sessionId, messageId) {
835
+ return (this.assistantResponseStreamModes.get(this.getAssistantResponseStreamKey(sessionId, messageId)) ??
836
+ getResponseStreamingMode());
837
+ }
838
+ enqueueAssistantResponse(sessionId, messageId, payload) {
839
+ const key = this.getAssistantResponseStreamKey(sessionId, messageId);
840
+ const mode = this.getAssistantResponseStreamMode(sessionId, messageId);
841
+ this.assistantResponseStreamModes.set(key, mode);
842
+ this.getAssistantResponseStreamer(mode).enqueue(sessionId, messageId, payload);
843
+ }
844
+ async completeAssistantResponse(sessionId, messageId, payload, options) {
845
+ const key = this.getAssistantResponseStreamKey(sessionId, messageId);
846
+ const mode = this.getAssistantResponseStreamMode(sessionId, messageId);
847
+ const result = await this.getAssistantResponseStreamer(mode).complete(sessionId, messageId, payload, options);
848
+ this.assistantResponseStreamModes.delete(key);
849
+ return result;
850
+ }
851
+ clearAssistantResponseStream(sessionId, messageId, reason) {
852
+ this.assistantResponseStreamModes.delete(this.getAssistantResponseStreamKey(sessionId, messageId));
853
+ this.assistantEditResponseStreamer.clearMessage(sessionId, messageId, reason);
854
+ this.assistantDraftResponseStreamer.clearMessage(sessionId, messageId, reason);
855
+ }
856
+ clearAssistantResponseSession(sessionId, reason) {
857
+ for (const key of this.assistantResponseStreamModes.keys()) {
858
+ if (key.startsWith(`${sessionId}:`)) {
859
+ this.assistantResponseStreamModes.delete(key);
860
+ }
861
+ }
862
+ this.assistantEditResponseStreamer.clearSession(sessionId, reason);
863
+ this.assistantDraftResponseStreamer.clearSession(sessionId, reason);
864
+ }
865
+ clearAllResponseStreams(reason) {
866
+ this.assistantResponseStreamModes.clear();
867
+ this.assistantEditResponseStreamer.clearAll(reason);
868
+ this.assistantDraftResponseStreamer.clearAll(reason);
869
+ this.thinkingResponseStreamer.clearAll(reason);
870
+ }
871
+ hasActiveAssistantResponseStream(sessionId) {
872
+ return (this.assistantEditResponseStreamer.hasActiveStream(sessionId) ||
873
+ this.assistantDraftResponseStreamer.hasActiveStream(sessionId));
874
+ }
875
+ createResponseStreamer(mode) {
876
+ if (mode === "draft") {
653
877
  return new ResponseStreamer({
654
878
  throttleMs: RESPONSE_STREAM_THROTTLE_MS,
655
879
  sendPart: async (part) => {
@@ -763,6 +987,37 @@ class EventSubscriptionService {
763
987
  prepareFinalStreamingPayload(messageText) {
764
988
  return prepareAssistantFinalStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
765
989
  }
990
+ getThinkingStreamId(messageId) {
991
+ return `thinking:${messageId}`;
992
+ }
993
+ getThinkingPayloadKey(sessionId, messageId) {
994
+ return `${sessionId}:${messageId}`;
995
+ }
996
+ clearThinkingStream(sessionId, messageId, reason) {
997
+ this.thinkingResponseStreamer.clearMessage(sessionId, this.getThinkingStreamId(messageId), reason);
998
+ this.thinkingStreamingPayloads.delete(this.getThinkingPayloadKey(sessionId, messageId));
999
+ }
1000
+ async completeThinkingStream(sessionId, messageId) {
1001
+ const key = this.getThinkingPayloadKey(sessionId, messageId);
1002
+ const payload = this.thinkingStreamingPayloads.get(key);
1003
+ const finalPayload = payload ? makeThinkingPayloadExpandable(payload) : undefined;
1004
+ const result = await this.thinkingResponseStreamer.complete(sessionId, this.getThinkingStreamId(messageId), finalPayload);
1005
+ this.thinkingStreamingPayloads.delete(key);
1006
+ if (result.streamed || !finalPayload) {
1007
+ return;
1008
+ }
1009
+ if (!this.botInstance || !this.chatIdInstance) {
1010
+ return;
1011
+ }
1012
+ for (const part of finalPayload.parts) {
1013
+ await sendRenderedBotPart({
1014
+ api: this.botInstance.api,
1015
+ chatId: this.chatIdInstance,
1016
+ part,
1017
+ options: finalPayload.sendOptions,
1018
+ });
1019
+ }
1020
+ }
766
1021
  enqueueSessionCompletionTask(sessionId, task) {
767
1022
  const previousTask = this.sessionCompletionTasks.get(sessionId) ?? Promise.resolve();
768
1023
  const nextTask = previousTask
@@ -776,6 +1031,19 @@ class EventSubscriptionService {
776
1031
  this.sessionCompletionTasks.set(sessionId, nextTask);
777
1032
  return nextTask;
778
1033
  }
1034
+ finalizeCompactProgress(sessionId) {
1035
+ const existingTask = this.compactProgressFinalizationTasks.get(sessionId);
1036
+ if (existingTask) {
1037
+ return existingTask;
1038
+ }
1039
+ const nextTask = this.compactProgressStreamer.finalize(sessionId).finally(() => {
1040
+ if (this.compactProgressFinalizationTasks.get(sessionId) === nextTask) {
1041
+ this.compactProgressFinalizationTasks.delete(sessionId);
1042
+ }
1043
+ });
1044
+ this.compactProgressFinalizationTasks.set(sessionId, nextTask);
1045
+ return nextTask;
1046
+ }
779
1047
  getNextDraftId() {
780
1048
  const id = this.nextDraftId;
781
1049
  this.nextDraftId += 1;
@@ -787,6 +1055,12 @@ class EventSubscriptionService {
787
1055
  }
788
1056
  return "default";
789
1057
  }
1058
+ getCompactToolActivity(toolInfo) {
1059
+ if (toolInfo.tool === "task") {
1060
+ return t("progress.compact.task");
1061
+ }
1062
+ return formatCompactToolActivity(toolInfo, 128);
1063
+ }
790
1064
  formatShortSessionId(sessionId) {
791
1065
  return sessionId.length <= 8 ? sessionId : sessionId.slice(0, 8);
792
1066
  }