@grinev/opencode-telegram-bot 0.21.1 → 0.22.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 (193) hide show
  1. package/.env.example +10 -15
  2. package/README.md +33 -13
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +20 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +177 -13
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +27 -12
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +49 -7
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +113 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/callbacks/settings-callback-handler.js +105 -0
  62. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  63. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  64. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  65. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  66. package/dist/bot/commands/command-catalog-command.js +42 -0
  67. package/dist/bot/commands/definitions.js +1 -1
  68. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  69. package/dist/bot/commands/ls-command.js +48 -0
  70. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  71. package/dist/bot/commands/messages-command.js +59 -0
  72. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  73. package/dist/bot/commands/open-command.js +45 -0
  74. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  75. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  76. package/dist/bot/commands/projects-command.js +32 -0
  77. package/dist/bot/commands/rename-command.js +33 -0
  78. package/dist/bot/commands/sessions-command.js +42 -0
  79. package/dist/bot/commands/settings-command.js +10 -0
  80. package/dist/bot/commands/skills-catalog-command.js +42 -0
  81. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  82. package/dist/bot/commands/{status.js → status-command.js} +15 -10
  83. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  84. package/dist/bot/commands/tasklist-command.js +40 -0
  85. package/dist/bot/commands/worktree-command.js +47 -0
  86. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  87. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  88. package/dist/bot/handlers/photo-handler.js +44 -0
  89. package/dist/bot/handlers/prompt.js +17 -17
  90. package/dist/bot/handlers/text-message-handler.js +61 -0
  91. package/dist/bot/handlers/tts-response-handler.js +29 -0
  92. package/dist/bot/handlers/{voice.js → voice-handler.js} +6 -3
  93. package/dist/bot/index.js +27 -1179
  94. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  95. package/dist/bot/keyboards/keyboard-types.js +1 -0
  96. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  97. package/dist/bot/menus/agent-selection-menu.js +54 -0
  98. package/dist/bot/menus/command-catalog-menu.js +101 -0
  99. package/dist/bot/menus/context-control-menu.js +33 -0
  100. package/dist/bot/menus/file-browser-menu.js +265 -0
  101. package/dist/bot/{handlers → menus}/inline-menu.js +7 -34
  102. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  103. package/dist/bot/menus/message-history-menu.js +107 -0
  104. package/dist/bot/menus/model-selection-menu.js +66 -0
  105. package/dist/bot/menus/permission-menu.js +116 -0
  106. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  107. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  108. package/dist/bot/menus/rename-menu.js +6 -0
  109. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  110. package/dist/bot/menus/session-selection-menu.js +115 -0
  111. package/dist/bot/menus/settings-menu.js +52 -0
  112. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  113. package/dist/bot/menus/variant-selection-menu.js +67 -0
  114. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  115. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  116. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  117. package/dist/bot/messages/external-user-input-notification.js +22 -0
  118. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  119. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  120. package/dist/bot/messages/summary-message-formatter.js +130 -0
  121. package/dist/bot/{utils → messages}/thinking-message.js +1 -4
  122. package/dist/bot/messages/thinking-rendering.js +66 -0
  123. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  124. package/dist/bot/middleware/interaction-guard.js +2 -2
  125. package/dist/bot/middleware/unknown-command.js +1 -1
  126. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  127. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  128. package/dist/bot/pinned/pinned-message-types.js +1 -0
  129. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  130. package/dist/bot/render/types.js +1 -0
  131. package/dist/bot/routers/command-router.js +73 -0
  132. package/dist/bot/routers/message-router.js +150 -0
  133. package/dist/bot/services/attach-presentation.js +40 -0
  134. package/dist/bot/services/event-subscription-service.js +1116 -0
  135. package/dist/bot/services/project-switch-presentation.js +26 -0
  136. package/dist/bot/streaming/compact-progress-streamer.js +162 -0
  137. package/dist/bot/{utils → streaming}/finalize-assistant-response.js +9 -3
  138. package/dist/bot/streaming/response-streamer.js +16 -1
  139. package/dist/cli.js +4 -4
  140. package/dist/config.js +9 -18
  141. package/dist/i18n/ar.js +31 -6
  142. package/dist/i18n/de.js +32 -7
  143. package/dist/i18n/en.js +32 -7
  144. package/dist/i18n/es.js +32 -7
  145. package/dist/i18n/fr.js +32 -7
  146. package/dist/i18n/ru.js +32 -7
  147. package/dist/i18n/zh.js +32 -7
  148. package/dist/index.js +1 -1
  149. package/dist/opencode/ready-refresh.js +2 -2
  150. package/dist/{service → runtime/service}/manager.js +2 -2
  151. package/dist/runtime/service/types.js +1 -0
  152. package/package.json +1 -1
  153. package/dist/bot/commands/ls.js +0 -445
  154. package/dist/bot/commands/open.js +0 -319
  155. package/dist/bot/commands/tts.js +0 -13
  156. package/dist/bot/commands/worktree.js +0 -201
  157. package/dist/bot/handlers/permission.js +0 -235
  158. package/dist/bot/utils/browser-roots.js +0 -140
  159. package/dist/bot/utils/file-tree.js +0 -92
  160. package/dist/bot/utils/send-tts-response.js +0 -37
  161. package/dist/bot/utils/switch-project.js +0 -45
  162. package/dist/bot/utils/telegram-file-url.js +0 -11
  163. package/dist/interaction/busy.js +0 -8
  164. package/dist/interaction/cleanup.js +0 -32
  165. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  166. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  167. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  168. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  169. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  170. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  171. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  172. /package/dist/{model/types.js → app/types/model.js} +0 -0
  173. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  174. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  175. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  176. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  177. /package/dist/{question/types.js → app/types/session.js} +0 -0
  178. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  179. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  180. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  181. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  182. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  183. /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
  184. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  185. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  186. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  187. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  188. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  189. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  190. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  191. /package/dist/{telegram → bot}/render/validator.js +0 -0
  192. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  193. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { normalizePathForDisplay, prepareCodeFile } from "./formatter.js";
2
- import { logger } from "../utils/logger.js";
3
- import { getCurrentProject } from "../settings/manager.js";
1
+ import { normalizePathForDisplay, prepareCodeFile } from "../formatters/summary-formatter.js";
2
+ import { logger } from "../../utils/logger.js";
3
+ import { getCurrentProject } from "../stores/settings-store.js";
4
4
  function extractFirstUpdatedFileFromTitle(title) {
5
5
  for (const rawLine of title.split("\n")) {
6
6
  const line = rawLine.trim();
@@ -38,6 +38,7 @@ function normalizeSnapshotValue(value) {
38
38
  class SummaryAggregator {
39
39
  currentSessionId = null;
40
40
  textMessageStates = new Map();
41
+ thinkingMessageStates = new Map();
41
42
  messages = new Map();
42
43
  messageCount = 0;
43
44
  lastUpdated = 0;
@@ -45,10 +46,12 @@ class SummaryAggregator {
45
46
  onPartialCallback = null;
46
47
  onExternalUserInputCallback = null;
47
48
  onToolCallback = null;
49
+ onRootToolUpdateCallback = null;
48
50
  onToolFileCallback = null;
49
51
  onQuestionCallback = null;
50
52
  onQuestionErrorCallback = null;
51
53
  onThinkingCallback = null;
54
+ onThinkingFinishedCallback = null;
52
55
  onTokensCallback = null;
53
56
  onCostCallback = null;
54
57
  onSubagentCallback = null;
@@ -62,6 +65,7 @@ class SummaryAggregator {
62
65
  onClearedCallback = null;
63
66
  processedToolStates = new Set();
64
67
  thinkingFiredForMessages = new Set();
68
+ thinkingFinishedForMessages = new Set();
65
69
  deliveredExternalUserMessageIds = new Set();
66
70
  knownTextPartIds = new Map();
67
71
  bot = null;
@@ -93,6 +97,9 @@ class SummaryAggregator {
93
97
  setOnTool(callback) {
94
98
  this.onToolCallback = callback;
95
99
  }
100
+ setOnRootToolUpdate(callback) {
101
+ this.onRootToolUpdateCallback = callback;
102
+ }
96
103
  setOnToolFile(callback) {
97
104
  this.onToolFileCallback = callback;
98
105
  }
@@ -105,6 +112,9 @@ class SummaryAggregator {
105
112
  setOnThinking(callback) {
106
113
  this.onThinkingCallback = callback;
107
114
  }
115
+ setOnThinkingFinished(callback) {
116
+ this.onThinkingFinishedCallback = callback;
117
+ }
108
118
  setOnTokens(callback) {
109
119
  this.onTokensCallback = callback;
110
120
  }
@@ -242,11 +252,13 @@ class SummaryAggregator {
242
252
  this.stopTypingIndicator();
243
253
  this.currentSessionId = null;
244
254
  this.textMessageStates.clear();
255
+ this.thinkingMessageStates.clear();
245
256
  this.messages.clear();
246
257
  this.partHashes.clear();
247
258
  this.knownTextPartIds.clear();
248
259
  this.processedToolStates.clear();
249
260
  this.thinkingFiredForMessages.clear();
261
+ this.thinkingFinishedForMessages.clear();
250
262
  this.deliveredExternalUserMessageIds.clear();
251
263
  this.trackedSessionParents.clear();
252
264
  this.subagentStates.clear();
@@ -755,26 +767,38 @@ class SummaryAggregator {
755
767
  this.registerKnownTextPart(messageID, part.id);
756
768
  this.registerTextPart(messageID, part.id);
757
769
  }
770
+ if (part.type === "reasoning") {
771
+ this.registerThinkingPart(messageID, part.id, this.extractReasoningTitle(part));
772
+ }
758
773
  const deltaFromUpdated = event.properties.delta;
759
774
  if (part.type === "text" &&
760
775
  typeof deltaFromUpdated === "string" &&
761
776
  deltaFromUpdated.length > 0) {
777
+ this.emitThinkingFinishedOnce(part.sessionID, messageID);
762
778
  this.applyTextDelta(part.sessionID, messageID, part.id, deltaFromUpdated, part.text);
763
779
  this.lastUpdated = Date.now();
764
780
  return;
765
781
  }
782
+ if (part.type === "reasoning" &&
783
+ typeof deltaFromUpdated === "string" &&
784
+ deltaFromUpdated.length > 0) {
785
+ const partText = "text" in part && typeof part.text === "string" ? part.text : undefined;
786
+ this.applyThinkingDelta(part.sessionID, messageID, part.id, deltaFromUpdated, partText, this.extractReasoningTitle(part));
787
+ this.lastUpdated = Date.now();
788
+ return;
789
+ }
766
790
  if (part.type === "reasoning") {
767
- // Fire the thinking callback once per message on the first reasoning part.
768
- // This is the signal that the model is actually doing extended thinking.
769
- if (!this.thinkingFiredForMessages.has(messageID) && this.onThinkingCallback) {
791
+ // Fire the thinking callback on every reasoning update. The first update
792
+ // preserves the old lightweight indicator behavior for callers that do
793
+ // not display full reasoning content.
794
+ const isFirstUpdate = !this.thinkingFiredForMessages.has(messageID);
795
+ if (isFirstUpdate) {
770
796
  this.thinkingFiredForMessages.add(messageID);
771
- const callback = this.onThinkingCallback;
772
- const sessionID = part.sessionID;
773
- setImmediate(() => {
774
- if (typeof callback === "function") {
775
- callback(sessionID);
776
- }
777
- });
797
+ }
798
+ const partText = "text" in part && typeof part.text === "string" ? part.text : "";
799
+ const wasUpdated = this.setThinkingPartSnapshot(messageID, part.id, partText, this.extractReasoningTitle(part));
800
+ if (isFirstUpdate || wasUpdated) {
801
+ this.emitThinkingUpdate(part.sessionID, messageID, isFirstUpdate);
778
802
  }
779
803
  }
780
804
  else if (part.type === "text" && "text" in part && part.text) {
@@ -784,6 +808,7 @@ class SummaryAggregator {
784
808
  if (!wasUpdated) {
785
809
  return;
786
810
  }
811
+ this.emitThinkingFinishedOnce(part.sessionID, messageID);
787
812
  const fullText = this.getCombinedMessageText(messageID);
788
813
  if (messageInfo && messageInfo.role === "assistant") {
789
814
  this.startTypingIndicator();
@@ -808,6 +833,19 @@ class SummaryAggregator {
808
833
  this.updateSubagentFromTaskTool(part.sessionID, input);
809
834
  }
810
835
  logger.debug(`[Aggregator] Tool event: callID=${part.callID}, tool=${part.tool}, status=${"status" in state ? state.status : "unknown"}`);
836
+ if (this.onRootToolUpdateCallback) {
837
+ this.onRootToolUpdateCallback({
838
+ sessionId: part.sessionID,
839
+ messageId: messageID,
840
+ callId: part.callID,
841
+ tool: part.tool,
842
+ state: part.state,
843
+ input,
844
+ title,
845
+ metadata: "metadata" in state ? state.metadata : undefined,
846
+ hasFileAttachment: false,
847
+ });
848
+ }
811
849
  if (part.tool === "question") {
812
850
  logger.debug(`[Aggregator] Question tool part update:`, JSON.stringify(part, null, 2));
813
851
  // If the question tool fails, clear the active poll
@@ -871,6 +909,13 @@ class SummaryAggregator {
871
909
  if (!sessionID || !messageID || typeof delta !== "string" || delta.length === 0) {
872
910
  return;
873
911
  }
912
+ if (partType === "reasoning" || (!partType && this.isKnownThinkingPart(messageID, partID))) {
913
+ const title = part
914
+ ? this.extractReasoningTitle(part)
915
+ : undefined;
916
+ this.applyThinkingDelta(sessionID, messageID, partID, delta, part?.text, title);
917
+ return;
918
+ }
874
919
  if (partType && partType !== "text") {
875
920
  return;
876
921
  }
@@ -890,6 +935,7 @@ class SummaryAggregator {
890
935
  this.registerTextPart(messageID, partID);
891
936
  }
892
937
  }
938
+ this.emitThinkingFinishedOnce(sessionID, messageID);
893
939
  this.applyTextDelta(sessionID, messageID, partID, delta, part?.text);
894
940
  }
895
941
  applyTextDelta(sessionID, messageID, partID, delta, fullTextHint) {
@@ -916,6 +962,121 @@ class SummaryAggregator {
916
962
  this.startTypingIndicator();
917
963
  this.emitPartialText(sessionID, messageID, combined);
918
964
  }
965
+ extractReasoningTitle(part) {
966
+ for (const key of ["title", "heading", "summary", "name"]) {
967
+ const value = part[key];
968
+ if (typeof value === "string" && value.trim()) {
969
+ return value;
970
+ }
971
+ }
972
+ const metadata = part.metadata;
973
+ if (metadata && typeof metadata === "object") {
974
+ for (const key of ["title", "heading", "summary", "name"]) {
975
+ const value = metadata[key];
976
+ if (typeof value === "string" && value.trim()) {
977
+ return value;
978
+ }
979
+ }
980
+ }
981
+ return undefined;
982
+ }
983
+ getOrCreateThinkingMessageState(messageID) {
984
+ let state = this.thinkingMessageStates.get(messageID);
985
+ if (!state) {
986
+ state = {
987
+ orderedPartIds: [],
988
+ sections: new Map(),
989
+ };
990
+ this.thinkingMessageStates.set(messageID, state);
991
+ }
992
+ return state;
993
+ }
994
+ isKnownThinkingPart(messageID, partID) {
995
+ return this.thinkingMessageStates.get(messageID)?.sections.has(partID) ?? false;
996
+ }
997
+ registerThinkingPart(messageID, partID, title) {
998
+ const state = this.getOrCreateThinkingMessageState(messageID);
999
+ if (!state.orderedPartIds.includes(partID)) {
1000
+ state.orderedPartIds.push(partID);
1001
+ }
1002
+ const existing = state.sections.get(partID);
1003
+ if (!existing) {
1004
+ const section = { id: partID, text: "" };
1005
+ if (title) {
1006
+ section.title = title;
1007
+ }
1008
+ state.sections.set(partID, section);
1009
+ return;
1010
+ }
1011
+ if (title && existing.title !== title) {
1012
+ existing.title = title;
1013
+ }
1014
+ }
1015
+ setThinkingPartSnapshot(messageID, partID, text, title) {
1016
+ this.registerThinkingPart(messageID, partID, title);
1017
+ const state = this.getOrCreateThinkingMessageState(messageID);
1018
+ const existing = state.sections.get(partID);
1019
+ const nextTitle = title ?? existing?.title;
1020
+ if (existing && existing.text === text && existing.title === nextTitle) {
1021
+ return false;
1022
+ }
1023
+ const next = { id: partID, text };
1024
+ if (nextTitle) {
1025
+ next.title = nextTitle;
1026
+ }
1027
+ state.sections.set(partID, next);
1028
+ return true;
1029
+ }
1030
+ applyThinkingDelta(sessionID, messageID, partID, delta, fullTextHint, title) {
1031
+ if (sessionID !== this.currentSessionId) {
1032
+ return;
1033
+ }
1034
+ this.registerThinkingPart(messageID, partID, title);
1035
+ const state = this.getOrCreateThinkingMessageState(messageID);
1036
+ const existing = state.sections.get(partID);
1037
+ const previous = existing?.text ?? "";
1038
+ let accumulated = `${previous}${delta}`;
1039
+ if (typeof fullTextHint === "string" && fullTextHint.length > accumulated.length) {
1040
+ accumulated = fullTextHint;
1041
+ }
1042
+ this.setThinkingPartSnapshot(messageID, partID, accumulated, title ?? existing?.title);
1043
+ this.emitThinkingUpdate(sessionID, messageID, false);
1044
+ }
1045
+ getThinkingSections(messageID) {
1046
+ const state = this.thinkingMessageStates.get(messageID);
1047
+ if (!state) {
1048
+ return [];
1049
+ }
1050
+ return state.orderedPartIds
1051
+ .map((partID) => state.sections.get(partID))
1052
+ .filter((section) => Boolean(section))
1053
+ .map((section) => ({ ...section }));
1054
+ }
1055
+ emitThinkingUpdate(sessionId, messageId, isFirstUpdate) {
1056
+ if (!this.onThinkingCallback) {
1057
+ return;
1058
+ }
1059
+ const sections = this.getThinkingSections(messageId);
1060
+ if (sections.length === 0) {
1061
+ return;
1062
+ }
1063
+ const callback = this.onThinkingCallback;
1064
+ setImmediate(() => {
1065
+ callback({ sessionId, messageId, sections, isFirstUpdate });
1066
+ });
1067
+ }
1068
+ emitThinkingFinishedOnce(sessionId, messageId) {
1069
+ if (!this.onThinkingFinishedCallback ||
1070
+ !this.thinkingFiredForMessages.has(messageId) ||
1071
+ this.thinkingFinishedForMessages.has(messageId)) {
1072
+ return;
1073
+ }
1074
+ this.thinkingFinishedForMessages.add(messageId);
1075
+ const callback = this.onThinkingFinishedCallback;
1076
+ setImmediate(() => {
1077
+ callback(sessionId, messageId);
1078
+ });
1079
+ }
919
1080
  emitExternalUserInputIfReady(sessionId, messageId) {
920
1081
  if (sessionId !== this.currentSessionId || this.deliveredExternalUserMessageIds.has(messageId)) {
921
1082
  return;
@@ -942,9 +1103,12 @@ class SummaryAggregator {
942
1103
  }
943
1104
  cleanupCompletedMessage(messageId) {
944
1105
  this.textMessageStates.delete(messageId);
1106
+ this.thinkingMessageStates.delete(messageId);
945
1107
  this.messages.delete(messageId);
946
1108
  this.partHashes.delete(messageId);
947
1109
  this.knownTextPartIds.delete(messageId);
1110
+ this.thinkingFiredForMessages.delete(messageId);
1111
+ this.thinkingFinishedForMessages.delete(messageId);
948
1112
  if (this.textMessageStates.size === 0) {
949
1113
  logger.debug("[Aggregator] No more active messages, stopping typing indicator");
950
1114
  this.stopTypingIndicator();
@@ -1,8 +1,7 @@
1
- import { opencodeClient } from "../opencode/client.js";
2
- import { getCurrentProject } from "../settings/manager.js";
3
- import { getCurrentSession } from "../session/manager.js";
4
- import { getCurrentAgent, setCurrentAgent } from "../settings/manager.js";
5
- import { logger } from "../utils/logger.js";
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { getCurrentAgent, getCurrentProject, setCurrentAgent } from "../stores/settings-store.js";
3
+ import { getCurrentSession } from "./session-service.js";
4
+ import { logger } from "../../utils/logger.js";
6
5
  /**
7
6
  * Get list of available agents from OpenCode API
8
7
  * @returns Array of available agents (filtered by mode and hidden flag)
@@ -1,17 +1,17 @@
1
- import { opencodeClient } from "../opencode/client.js";
2
- import { isOpencodeServerHealthy } from "../opencode/ready-refresh.js";
3
- import { summaryAggregator } from "../summary/aggregator.js";
4
- import { pinnedMessageManager } from "../pinned/manager.js";
5
- import { keyboardManager } from "../keyboard/manager.js";
6
- import { questionManager } from "../question/manager.js";
7
- import { permissionManager } from "../permission/manager.js";
8
- import { showCurrentQuestion } from "../bot/handlers/question.js";
9
- import { showPermissionRequest } from "../bot/handlers/permission.js";
10
- import { getCurrentSession } from "../session/manager.js";
11
- import { getCurrentProject } from "../settings/manager.js";
12
- import { attachManager } from "./manager.js";
13
- import { logger } from "../utils/logger.js";
14
- import { isExpectedOpencodeUnavailableError } from "../utils/opencode-error.js";
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ import { isOpencodeServerHealthy } from "../../opencode/ready-refresh.js";
3
+ import { summaryAggregator } from "../managers/summary-aggregation-manager.js";
4
+ import { questionManager } from "../managers/question-manager.js";
5
+ import { permissionManager } from "../managers/permission-manager.js";
6
+ import { getCurrentSession } from "./session-service.js";
7
+ import { getCurrentProject } from "../stores/settings-store.js";
8
+ import { attachManager } from "../managers/attach-manager.js";
9
+ import { logger } from "../../utils/logger.js";
10
+ import { isExpectedOpencodeUnavailableError } from "../../utils/opencode-error.js";
11
+ let attachPresentation = null;
12
+ export function configureAttachPresentation(deps) {
13
+ attachPresentation = deps;
14
+ }
15
15
  function getAttachBusyStatus(sessionId, statuses) {
16
16
  if (!statuses || typeof statuses !== "object") {
17
17
  return false;
@@ -19,36 +19,12 @@ function getAttachBusyStatus(sessionId, statuses) {
19
19
  const sessionStatus = statuses[sessionId];
20
20
  return sessionStatus?.type === "busy";
21
21
  }
22
- async function ensureAttachPinnedSession({ api, chatId, session, forceFullRestore = false, }) {
23
- if (!pinnedMessageManager.isInitialized()) {
24
- pinnedMessageManager.initialize(api, chatId);
25
- }
26
- keyboardManager.initialize(api, chatId);
27
- const pinnedState = pinnedMessageManager.getState();
28
- if (pinnedState.sessionId === session.id && pinnedState.messageId) {
29
- if (forceFullRestore) {
30
- await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
31
- }
32
- return;
33
- }
34
- if (pinnedState.messageId && pinnedState.sessionId === null) {
35
- await pinnedMessageManager.restoreExistingSession(session.id, session.title);
36
- }
37
- else {
38
- await pinnedMessageManager.onSessionChange(session.id, session.title);
39
- }
40
- await pinnedMessageManager.loadContextFromHistory(session.id, session.directory);
41
- const contextInfo = pinnedMessageManager.getContextInfo();
42
- if (contextInfo) {
43
- keyboardManager.updateContext(contextInfo.tokensUsed, contextInfo.tokensLimit);
44
- }
45
- }
46
22
  async function syncPinnedAttachState() {
47
- if (!pinnedMessageManager.isInitialized()) {
23
+ if (!attachPresentation) {
48
24
  return;
49
25
  }
50
26
  const attached = attachManager.getSnapshot();
51
- await pinnedMessageManager.setAttachState(attached !== null, attached?.busy ?? false);
27
+ await attachPresentation.syncAttachState(attached !== null, attached?.busy ?? false);
52
28
  }
53
29
  async function restorePendingQuestion(bot, chatId, sessionId, directory) {
54
30
  const { data, error } = await opencodeClient.question.list({
@@ -64,11 +40,11 @@ async function restorePendingQuestion(bot, chatId, sessionId, directory) {
64
40
  return false;
65
41
  }
66
42
  const pendingQuestion = data.find((request) => request.sessionID === sessionId);
67
- if (!pendingQuestion) {
43
+ if (!pendingQuestion || !attachPresentation) {
68
44
  return false;
69
45
  }
70
46
  questionManager.startQuestions(pendingQuestion.questions, pendingQuestion.id);
71
- await showCurrentQuestion(bot.api, chatId);
47
+ await attachPresentation.showCurrentQuestion(bot.api, chatId);
72
48
  return true;
73
49
  }
74
50
  async function restorePendingPermissions(bot, chatId, sessionId, directory) {
@@ -85,15 +61,18 @@ async function restorePendingPermissions(bot, chatId, sessionId, directory) {
85
61
  return 0;
86
62
  }
87
63
  const pendingPermissions = data.filter((request) => request.sessionID === sessionId);
64
+ if (!attachPresentation) {
65
+ return 0;
66
+ }
88
67
  for (const request of pendingPermissions) {
89
- await showPermissionRequest(bot.api, chatId, request);
68
+ await attachPresentation.showPermissionRequest(bot.api, chatId, request);
90
69
  }
91
70
  return pendingPermissions.length;
92
71
  }
93
72
  export async function attachToSession(deps) {
94
73
  const { bot, chatId, session, ensureEventSubscription, forceFullRestore = false } = deps;
95
74
  const alreadyAttached = attachManager.isAttachedSession(session.id, session.directory);
96
- await ensureAttachPinnedSession({
75
+ await attachPresentation?.ensurePinnedSession({
97
76
  api: bot.api,
98
77
  chatId,
99
78
  session,
@@ -1,19 +1,22 @@
1
1
  import { opencodeClient } from "../../opencode/client.js";
2
- import { foregroundSessionState, } from "../../scheduled-task/foreground-state.js";
3
- import { scheduledTaskRuntime } from "../../scheduled-task/runtime.js";
4
- import { attachManager } from "../../attach/manager.js";
5
- import { markAttachedSessionBusy, markAttachedSessionIdle } from "../../attach/service.js";
6
- import { assistantRunState } from "../assistant-run-state.js";
7
- import { clearPromptResponseMode } from "../handlers/prompt.js";
2
+ import { foregroundSessionState, } from "../managers/foreground-session-state-manager.js";
3
+ import { scheduledTaskRuntime } from "./scheduled-task-runtime-service.js";
4
+ import { attachManager } from "../managers/attach-manager.js";
5
+ import { markAttachedSessionBusy, markAttachedSessionIdle } from "./attach-service.js";
6
+ import { assistantRunState } from "../managers/assistant-run-state-manager.js";
8
7
  import { logger } from "../../utils/logger.js";
9
8
  const RECONCILE_MIN_INTERVAL_MS = 10_000;
10
9
  const FOREGROUND_BUSY_RECONCILE_GRACE_MS = 2_000;
11
10
  const inFlightDirectories = new Set();
12
11
  const lastReconcileAtByDirectory = new Map();
13
12
  let responseStreamerInstance = null;
13
+ let clearPromptResponseModeForReconciliation = null;
14
14
  export function setResponseStreamerForReconciliation(streamer) {
15
15
  responseStreamerInstance = streamer;
16
16
  }
17
+ export function setPromptResponseModeClearerForReconciliation(clearer) {
18
+ clearPromptResponseModeForReconciliation = clearer;
19
+ }
17
20
  function getReconciliationTargets(directory) {
18
21
  const foregroundBusySessions = foregroundSessionState
19
22
  .getBusySessions()
@@ -38,7 +41,7 @@ function isWithinForegroundBusyGracePeriod(session, now) {
38
41
  async function clearForegroundBusySession(sessionId, reason) {
39
42
  foregroundSessionState.markIdle(sessionId);
40
43
  assistantRunState.clearRun(sessionId, reason);
41
- clearPromptResponseMode(sessionId);
44
+ clearPromptResponseModeForReconciliation?.(sessionId);
42
45
  }
43
46
  export async function reconcileBusyStateNow(directory, now = Date.now()) {
44
47
  if (!directory) {
@@ -118,4 +121,6 @@ export async function reconcileBusyState(directory, now = Date.now()) {
118
121
  export function __resetBusyReconciliationForTests() {
119
122
  inFlightDirectories.clear();
120
123
  lastReconcileAtByDirectory.clear();
124
+ responseStreamerInstance = null;
125
+ clearPromptResponseModeForReconciliation = null;
121
126
  }
@@ -0,0 +1,21 @@
1
+ import { opencodeClient } from "../../opencode/client.js";
2
+ function normalizeDirectoryForCommandApi(directory) {
3
+ return directory.replace(/\\/g, "/");
4
+ }
5
+ export async function loadCommandCatalog(projectDirectory) {
6
+ const { data, error } = await opencodeClient.command.list({
7
+ directory: normalizeDirectoryForCommandApi(projectDirectory),
8
+ });
9
+ if (error || !data) {
10
+ throw error || new Error("No command data received");
11
+ }
12
+ return data
13
+ .filter((command) => {
14
+ const source = command.source;
15
+ return (typeof command.name === "string" && command.name.trim().length > 0 && source === "command");
16
+ })
17
+ .map((command) => ({
18
+ name: command.name,
19
+ description: command.description,
20
+ }));
21
+ }
@@ -1,7 +1,9 @@
1
1
  import { t } from "../../i18n/index.js";
2
- import { escapePlainTextForTelegramMarkdownV2 } from "../../summary/formatter.js";
3
- import { sendBotText } from "./telegram-text.js";
4
2
  const EXTERNAL_USER_INPUT_MAX_DISPLAY_LENGTH = 2000;
3
+ const MARKDOWN_V2_RESERVED_CHARS = /([_\*\[\]\(\)~`>#+\-=|{}.!\\])/g;
4
+ function escapePlainTextForTelegramMarkdownV2(text) {
5
+ return text.replace(MARKDOWN_V2_RESERVED_CHARS, "\\$1");
6
+ }
5
7
  function normalizeExternalUserInputText(text) {
6
8
  return text.replace(/\r\n/g, "\n").trim();
7
9
  }
@@ -35,20 +37,3 @@ export function buildExternalUserInputNotification(text) {
35
37
  rawFallbackText: `${title}\n\n${buildQuotedPlainText(displayText)}`,
36
38
  };
37
39
  }
38
- export async function deliverExternalUserInputNotification({ api, chatId, currentSessionId, sessionId, text, consumeSuppressedInput, }) {
39
- const notification = buildExternalUserInputNotification(text);
40
- if (!notification || currentSessionId !== sessionId) {
41
- return false;
42
- }
43
- if (consumeSuppressedInput(sessionId, text)) {
44
- return false;
45
- }
46
- await sendBotText({
47
- api,
48
- chatId,
49
- text: notification.text,
50
- rawFallbackText: notification.rawFallbackText,
51
- format: "markdown_v2",
52
- });
53
- return true;
54
- }