@llblab/pi-telegram 0.52.2 → 0.53.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 (204) hide show
  1. package/AGENTS.md +9 -7
  2. package/BACKLOG.md +1 -18
  3. package/CHANGELOG.md +15 -0
  4. package/README.md +7 -11
  5. package/api/commands.ts +3 -0
  6. package/dist/api/commands.d.ts +1 -1
  7. package/dist/lib/activity-verbosity.d.ts +1 -5
  8. package/dist/lib/activity-verbosity.js +15 -42
  9. package/dist/lib/activity.d.ts +9 -0
  10. package/dist/lib/activity.js +39 -10
  11. package/dist/lib/agent-messages.js +7 -3
  12. package/dist/lib/bindings.d.ts +43 -5
  13. package/dist/lib/bindings.js +163 -20
  14. package/dist/lib/bus-api.d.ts +12 -0
  15. package/dist/lib/bus-api.js +182 -79
  16. package/dist/lib/bus-follower.d.ts +111 -31
  17. package/dist/lib/bus-follower.js +1324 -359
  18. package/dist/lib/bus-leader.d.ts +41 -91
  19. package/dist/lib/bus-leader.js +969 -637
  20. package/dist/lib/bus-transport.js +16 -9
  21. package/dist/lib/bus.d.ts +215 -18
  22. package/dist/lib/bus.js +1048 -327
  23. package/dist/lib/channel-posts.d.ts +5 -0
  24. package/dist/lib/channel-posts.js +434 -261
  25. package/dist/lib/command-templates.d.ts +26 -5
  26. package/dist/lib/command-templates.js +92 -176
  27. package/dist/lib/commands.d.ts +168 -82
  28. package/dist/lib/commands.js +1409 -291
  29. package/dist/lib/config.d.ts +1 -8
  30. package/dist/lib/config.js +34 -76
  31. package/dist/lib/delivery.d.ts +2 -2
  32. package/dist/lib/delivery.js +16 -16
  33. package/dist/lib/extension.js +639 -171
  34. package/dist/lib/generative-app-worker.mjs +19 -7
  35. package/dist/lib/generative-apps.d.ts +0 -5
  36. package/dist/lib/generative-apps.js +61 -49
  37. package/dist/lib/inbound.d.ts +5 -0
  38. package/dist/lib/inbound.js +35 -59
  39. package/dist/lib/journal.d.ts +33 -3
  40. package/dist/lib/journal.js +2063 -782
  41. package/dist/lib/lifecycle.d.ts +2 -6
  42. package/dist/lib/lifecycle.js +75 -58
  43. package/dist/lib/locks.d.ts +29 -3
  44. package/dist/lib/locks.js +289 -108
  45. package/dist/lib/logging.js +50 -14
  46. package/dist/lib/media.d.ts +22 -1
  47. package/dist/lib/media.js +46 -28
  48. package/dist/lib/menu-model.d.ts +19 -5
  49. package/dist/lib/menu-model.js +53 -11
  50. package/dist/lib/menu-queue.d.ts +5 -4
  51. package/dist/lib/menu-queue.js +45 -63
  52. package/dist/lib/menu-settings.d.ts +5 -4
  53. package/dist/lib/menu-settings.js +114 -78
  54. package/dist/lib/menu-status.d.ts +1 -1
  55. package/dist/lib/menu-status.js +8 -25
  56. package/dist/lib/menu-thinking.d.ts +6 -1
  57. package/dist/lib/menu-thinking.js +14 -21
  58. package/dist/lib/menu.d.ts +8 -61
  59. package/dist/lib/menu.js +92 -164
  60. package/dist/lib/model.d.ts +1 -1
  61. package/dist/lib/model.js +2 -16
  62. package/dist/lib/outbound-attachments.d.ts +6 -27
  63. package/dist/lib/outbound-attachments.js +45 -38
  64. package/dist/lib/outbound-buttons.js +22 -16
  65. package/dist/lib/outbound-markup.d.ts +0 -2
  66. package/dist/lib/outbound-markup.js +51 -64
  67. package/dist/lib/outbound-voice.js +6 -3
  68. package/dist/lib/outbound.d.ts +6 -6
  69. package/dist/lib/outbound.js +65 -51
  70. package/dist/lib/ownership.js +6 -2
  71. package/dist/lib/paths.d.ts +5 -0
  72. package/dist/lib/paths.js +37 -21
  73. package/dist/lib/pi.d.ts +2 -0
  74. package/dist/lib/pi.js +8 -1
  75. package/dist/lib/polling.d.ts +21 -37
  76. package/dist/lib/polling.js +59 -50
  77. package/dist/lib/preview.js +39 -19
  78. package/dist/lib/process-identity.d.ts +24 -0
  79. package/dist/lib/process-identity.js +133 -0
  80. package/dist/lib/prompts.js +2 -7
  81. package/dist/lib/queue.d.ts +14 -16
  82. package/dist/lib/queue.js +252 -127
  83. package/dist/lib/recovery.d.ts +2 -83
  84. package/dist/lib/recovery.js +20 -274
  85. package/dist/lib/replies.d.ts +16 -24
  86. package/dist/lib/replies.js +99 -116
  87. package/dist/lib/routing.d.ts +120 -7
  88. package/dist/lib/routing.js +4667 -1152
  89. package/dist/lib/runtime.d.ts +1 -11
  90. package/dist/lib/sections.d.ts +4 -29
  91. package/dist/lib/sections.js +7 -20
  92. package/dist/lib/skills.js +4 -3
  93. package/dist/lib/status.js +86 -57
  94. package/dist/lib/sync.d.ts +1 -3
  95. package/dist/lib/sync.js +67 -43
  96. package/dist/lib/target.d.ts +4 -0
  97. package/dist/lib/target.js +18 -0
  98. package/dist/lib/telegram-api.d.ts +18 -13
  99. package/dist/lib/telegram-api.js +148 -53
  100. package/dist/lib/text-groups.js +4 -39
  101. package/dist/lib/thread-cleanup-manager.d.ts +6 -27
  102. package/dist/lib/thread-cleanup-manager.js +369 -186
  103. package/dist/lib/thread-display.d.ts +1 -2
  104. package/dist/lib/thread-display.js +58 -38
  105. package/dist/lib/thread-naming.d.ts +28 -8
  106. package/dist/lib/thread-naming.js +175 -66
  107. package/dist/lib/thread-reconciler.d.ts +42 -16
  108. package/dist/lib/thread-reconciler.js +90 -50
  109. package/dist/lib/threads.d.ts +82 -20
  110. package/dist/lib/threads.js +2247 -855
  111. package/dist/lib/time-injection.js +1 -2
  112. package/dist/lib/turns.js +1 -19
  113. package/dist/lib/updates.d.ts +169 -80
  114. package/dist/lib/updates.js +2418 -641
  115. package/dist/lib/voice.d.ts +7 -22
  116. package/dist/lib/voice.js +7 -22
  117. package/dist/lib/wire.js +1 -1
  118. package/dist/lib/workspace-admission.d.ts +3 -1
  119. package/dist/lib/workspace-admission.js +425 -211
  120. package/dist/lib/workspace-identity.js +8 -13
  121. package/dist/lib/workspace-retirement.d.ts +1 -1
  122. package/dist/lib/workspace-retirement.js +328 -140
  123. package/dist/lib/workspace-slots.js +8 -4
  124. package/dist/package.json +1 -1
  125. package/docs/architecture.md +31 -29
  126. package/docs/inbound.md +1 -1
  127. package/docs/multi-instance-bus.md +15 -3
  128. package/docs/outbound.md +1 -1
  129. package/docs/public-api.md +29 -1
  130. package/docs/ui-style.md +25 -8
  131. package/lib/activity-verbosity.ts +59 -82
  132. package/lib/activity.ts +146 -102
  133. package/lib/agent-messages.ts +12 -5
  134. package/lib/bindings.ts +389 -82
  135. package/lib/bus-api.ts +263 -102
  136. package/lib/bus-follower.ts +2440 -601
  137. package/lib/bus-leader.ts +2209 -1290
  138. package/lib/bus-transport.ts +46 -12
  139. package/lib/bus.ts +1673 -444
  140. package/lib/channel-posts.ts +848 -327
  141. package/lib/command-templates.ts +152 -265
  142. package/lib/commands.ts +2398 -555
  143. package/lib/config.ts +177 -170
  144. package/lib/delivery.ts +41 -29
  145. package/lib/extension.ts +1206 -433
  146. package/lib/generative-app-worker.mjs +19 -7
  147. package/lib/generative-apps.ts +324 -211
  148. package/lib/inbound.ts +95 -97
  149. package/lib/journal.ts +5311 -1763
  150. package/lib/keyboard.ts +1 -3
  151. package/lib/lifecycle.ts +128 -78
  152. package/lib/locks.ts +677 -203
  153. package/lib/logging.ts +147 -58
  154. package/lib/media.ts +82 -43
  155. package/lib/menu-model.ts +85 -18
  156. package/lib/menu-queue.ts +88 -129
  157. package/lib/menu-settings.ts +260 -155
  158. package/lib/menu-status.ts +20 -60
  159. package/lib/menu-thinking.ts +41 -56
  160. package/lib/menu.ts +112 -285
  161. package/lib/model.ts +8 -22
  162. package/lib/outbound-attachments.ts +156 -125
  163. package/lib/outbound-buttons.ts +76 -49
  164. package/lib/outbound-markup.ts +96 -94
  165. package/lib/outbound-voice.ts +33 -27
  166. package/lib/outbound.ts +128 -125
  167. package/lib/ownership.ts +19 -10
  168. package/lib/paths.ts +148 -49
  169. package/lib/pi.ts +35 -15
  170. package/lib/polling.ts +144 -147
  171. package/lib/preview.ts +93 -40
  172. package/lib/process-identity.ts +164 -0
  173. package/lib/prompts.ts +4 -11
  174. package/lib/queue.ts +516 -301
  175. package/lib/recovery.ts +50 -454
  176. package/lib/rendering.ts +1 -3
  177. package/lib/replies.ts +248 -209
  178. package/lib/routing.ts +8509 -2186
  179. package/lib/runtime.ts +1 -12
  180. package/lib/sections.ts +24 -89
  181. package/lib/setup.ts +1 -3
  182. package/lib/skills.ts +9 -4
  183. package/lib/status.ts +215 -110
  184. package/lib/sync.ts +448 -361
  185. package/lib/target.ts +22 -3
  186. package/lib/telegram-api.ts +302 -127
  187. package/lib/text-groups.ts +5 -39
  188. package/lib/thread-cleanup-manager.ts +833 -331
  189. package/lib/thread-display.ts +213 -97
  190. package/lib/thread-naming.ts +232 -87
  191. package/lib/thread-reconciler.ts +155 -72
  192. package/lib/threads.ts +5318 -1845
  193. package/lib/time-injection.ts +1 -4
  194. package/lib/turns.ts +15 -28
  195. package/lib/updates.ts +5184 -1612
  196. package/lib/voice.ts +7 -35
  197. package/lib/wire.ts +1 -1
  198. package/lib/workspace-admission.ts +996 -416
  199. package/lib/workspace-identity.ts +14 -17
  200. package/lib/workspace-retirement.ts +878 -376
  201. package/lib/workspace-slots.ts +22 -13
  202. package/package.json +1 -1
  203. package/scripts/check-downgrade.mjs +0 -472
  204. package/scripts/measure-workspace.mjs +0 -108
@@ -4,26 +4,13 @@
4
4
  * Owns status-menu payloads, status callback handling, and status-menu message rendering
5
5
  */
6
6
  import { formatTelegramCommandEmojiPrefix } from "./commands.js";
7
- import { getTelegramSectionMainMenuRows, } from "./sections.js";
8
- import { formatStatusButtonLabel, } from "./menu-model.js";
7
+ import { refuseUnavailableTelegramThinkingControls } from "./menu-thinking.js";
8
+ import { editTelegramMenuMessage, formatStatusButtonLabel, sendTelegramMenuMessage, } from "./menu-model.js";
9
9
  import { getCanonicalModelId, } from "./model.js";
10
+ import { getTelegramSectionMainMenuRows, } from "./sections.js";
10
11
  function isTelegramStatusMenuCallbackAction(data, action) {
11
12
  return data === `menu:${action}` || data === `status:${action}`;
12
13
  }
13
- function applyTelegramMenuRenderPayload(state, payload) {
14
- state.mode = payload.nextMode;
15
- return payload;
16
- }
17
- async function editTelegramMenuMessage(state, payload, deps) {
18
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
19
- await deps.editInteractiveMessage(state.chatId, state.messageId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup);
20
- }
21
- function sendTelegramMenuMessage(state, payload, deps) {
22
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
23
- return deps.sendInteractiveMessage(state.chatId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup, state.threadId !== undefined
24
- ? { target: { chatId: state.chatId, threadId: state.threadId } }
25
- : undefined);
26
- }
27
14
  export async function openTelegramStatusMenu(deps) {
28
15
  const state = await deps.getModelMenuState();
29
16
  const messageId = await deps.sendStatusMenu(state, deps.buildStatusHtml(), deps.getActiveModel(), deps.getThinkingLevel(), deps.getQueueItemCount?.() ?? 0);
@@ -48,14 +35,8 @@ export async function handleTelegramStatusMenuCallbackAction(callbackQueryId, da
48
35
  }
49
36
  if (!isTelegramStatusMenuCallbackAction(data, "thinking"))
50
37
  return false;
51
- if (deps.isVoiceReplyActive?.()) {
52
- await deps.answerCallbackQuery(callbackQueryId, "Thinking controls are disabled during voice replies.");
53
- return true;
54
- }
55
- if (!activeModel?.reasoning) {
56
- await deps.answerCallbackQuery(callbackQueryId, "This model has no reasoning controls.");
38
+ if (await refuseUnavailableTelegramThinkingControls(callbackQueryId, activeModel, deps))
57
39
  return true;
58
- }
59
40
  await deps.updateThinkingMenuMessage();
60
41
  await deps.answerCallbackQuery(callbackQueryId);
61
42
  return true;
@@ -83,10 +64,12 @@ export function buildStatusReplyMarkup(activeModel, currentThinkingLevel, queueI
83
64
  },
84
65
  ]);
85
66
  if (pendingCancellationCount > 0)
86
- rows.push([{
67
+ rows.push([
68
+ {
87
69
  text: `❌ Pending cancellations: ${pendingCancellationCount}`,
88
70
  callback_data: "reroutecancel:review:open",
89
- }]);
71
+ },
72
+ ]);
90
73
  if (sectionRegistry) {
91
74
  const sectionRows = getTelegramSectionMainMenuRows(sectionRegistry);
92
75
  for (const row of sectionRows) {
@@ -3,7 +3,7 @@
3
3
  * Zones: telegram ui, thinking controls, menu composition
4
4
  * Owns thinking-menu text, reply markup, callback handling, and thinking-menu message rendering
5
5
  */
6
- import type { TelegramMenuMessageRuntimeDeps, TelegramMenuRenderPayload, TelegramModelMenuState, TelegramReplyMarkup } from "./menu-model.ts";
6
+ import { type TelegramMenuMessageRuntimeDeps, type TelegramMenuRenderPayload, type TelegramModelMenuState, type TelegramReplyMarkup } from "./menu-model.ts";
7
7
  import { type MenuModel, type ThinkingLevel } from "./model.ts";
8
8
  export interface TelegramThinkingMenuCallbackDeps {
9
9
  setThinkingLevel: (level: ThinkingLevel) => void;
@@ -19,6 +19,11 @@ export interface TelegramThinkingMenuOpenDeps<TModel extends MenuModel = MenuMod
19
19
  storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
20
20
  isVoiceReplyActive?: () => boolean;
21
21
  }
22
+ /** Answers and returns true when voice replies or a non-reasoning model disable thinking controls. */
23
+ export declare function refuseUnavailableTelegramThinkingControls(callbackQueryId: string, activeModel: MenuModel | undefined, deps: {
24
+ answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
25
+ isVoiceReplyActive?: () => boolean;
26
+ }): Promise<boolean>;
22
27
  export declare function handleTelegramThinkingMenuCallbackAction(callbackQueryId: string, data: string | undefined, activeModel: MenuModel | undefined, deps: TelegramThinkingMenuCallbackDeps): Promise<boolean>;
23
28
  export declare function buildThinkingMenuText(): string;
24
29
  export declare function buildThinkingMenuReplyMarkup(currentThinkingLevel: ThinkingLevel): TelegramReplyMarkup;
@@ -3,42 +3,35 @@
3
3
  * Zones: telegram ui, thinking controls, menu composition
4
4
  * Owns thinking-menu text, reply markup, callback handling, and thinking-menu message rendering
5
5
  */
6
+ import { editTelegramMenuMessage, sendTelegramMenuMessage, } from "./menu-model.js";
6
7
  import { isThinkingLevel, THINKING_LEVELS, } from "./model.js";
7
8
  function parseTelegramThinkingMenuCallbackAction(data) {
8
9
  if (!data?.startsWith("thinking:set:"))
9
10
  return undefined;
10
11
  return { kind: "thinking:set", level: data.slice("thinking:set:".length) };
11
12
  }
12
- function applyTelegramMenuRenderPayload(state, payload) {
13
- state.mode = payload.nextMode;
14
- return payload;
15
- }
16
- async function editTelegramMenuMessage(state, payload, deps) {
17
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
18
- await deps.editInteractiveMessage(state.chatId, state.messageId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup);
19
- }
20
- function sendTelegramMenuMessage(state, payload, deps) {
21
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
22
- return deps.sendInteractiveMessage(state.chatId, appliedPayload.text, appliedPayload.mode, appliedPayload.replyMarkup, state.threadId !== undefined
23
- ? { target: { chatId: state.chatId, threadId: state.threadId } }
24
- : undefined);
13
+ /** Answers and returns true when voice replies or a non-reasoning model disable thinking controls. */
14
+ export async function refuseUnavailableTelegramThinkingControls(callbackQueryId, activeModel, deps) {
15
+ if (deps.isVoiceReplyActive?.()) {
16
+ await deps.answerCallbackQuery(callbackQueryId, "Thinking controls are disabled during voice replies");
17
+ return true;
18
+ }
19
+ if (!activeModel?.reasoning) {
20
+ await deps.answerCallbackQuery(callbackQueryId, "This model has no reasoning controls");
21
+ return true;
22
+ }
23
+ return false;
25
24
  }
26
25
  export async function handleTelegramThinkingMenuCallbackAction(callbackQueryId, data, activeModel, deps) {
27
26
  const action = parseTelegramThinkingMenuCallbackAction(data);
28
27
  if (!action)
29
28
  return false;
30
29
  if (!isThinkingLevel(action.level)) {
31
- await deps.answerCallbackQuery(callbackQueryId, "Invalid thinking level.");
32
- return true;
33
- }
34
- if (deps.isVoiceReplyActive?.()) {
35
- await deps.answerCallbackQuery(callbackQueryId, "Thinking controls are disabled during voice replies.");
30
+ await deps.answerCallbackQuery(callbackQueryId, "Invalid thinking level");
36
31
  return true;
37
32
  }
38
- if (!activeModel?.reasoning) {
39
- await deps.answerCallbackQuery(callbackQueryId, "This model has no reasoning controls.");
33
+ if (await refuseUnavailableTelegramThinkingControls(callbackQueryId, activeModel, deps))
40
34
  return true;
41
- }
42
35
  deps.setThinkingLevel(action.level);
43
36
  await deps.updateThinkingMenuMessage();
44
37
  await deps.answerCallbackQuery(callbackQueryId, `Thinking: ${deps.getCurrentThinkingLevel()}`);
@@ -3,10 +3,10 @@
3
3
  * Zones: telegram ui, controls, status menu
4
4
  * Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
5
5
  */
6
- import { type TelegramMenuMessageRuntimeDeps, type TelegramModelMenuState, type TelegramModelMenuStateBuilderContext, type TelegramModelMenuStateBuilderDeps, type TelegramReplyMarkup } from "./menu-model.ts";
6
+ import { type TelegramMenuMessageRuntimeDeps, type TelegramModelMenuState, type TelegramReplyMarkup } from "./menu-model.ts";
7
7
  import { type MenuModel, type ScopedTelegramModel, type TelegramModelSwitchContinuationSource, type ThinkingLevel } from "./model.ts";
8
- import type { TelegramInputRichMessage } from "./telegram-api.ts";
9
8
  import { type TelegramSectionRegistry } from "./sections.ts";
9
+ import type { TelegramApiCallOptions, TelegramInputRichMessage } from "./telegram-api.ts";
10
10
  export { applyTelegramModelPageSelection, applyTelegramModelScopeSelection, buildModelMenuReplyMarkup, buildModelPageMenuReplyMarkup, buildTelegramModelCallbackPlan, buildTelegramModelMenuRenderPayload, buildTelegramModelMenuState, buildTelegramModelMenuStateRuntime, buildTelegramModelPageMenuRenderPayload, createTelegramModelMenuRuntime, createTelegramModelMenuStateBuilder, formatScopedModelButtonText, getModelMenuItems, getStoredTelegramModelMenuState, getTelegramModelMenuPage, getTelegramModelSelection, handleTelegramModelMenuCallbackAction, MODEL_MENU_TITLE, MODEL_PAGE_MENU_TITLE, openTelegramModelMenu, pruneStoredTelegramModelMenus, resolveCachedTelegramModelMenuInputs, sendTelegramModelMenuMessage, storeTelegramModelMenuState, TELEGRAM_MODEL_PAGE_SIZE, updateTelegramModelMenuMessage, } from "./menu-model.ts";
11
11
  export type { BuildTelegramModelCallbackPlanParams, BuildTelegramModelMenuStateParams, CachedTelegramModelMenuInputs, MenuSettingsManager, StoredTelegramModelMenuState, TelegramMenuMessageRuntimeDeps, TelegramMenuMutationResult, TelegramMenuRenderPayload, TelegramMenuSelectionResult, TelegramModelCallbackPlan, TelegramModelMenuCallbackDeps, TelegramModelMenuInputCacheDeps, TelegramModelMenuOpenDeps, TelegramModelMenuPage, TelegramModelMenuRuntime, TelegramModelMenuRuntimeContext, TelegramModelMenuRuntimeOptions, TelegramModelMenuState, TelegramModelMenuStateBuilderContext, TelegramModelMenuStateBuilderDeps, TelegramModelMenuStoreOptions, TelegramModelScope, TelegramReplyMarkup, } from "./menu-model.ts";
12
12
  export { buildStatusReplyMarkup, buildTelegramStatusMenuRenderPayload, handleTelegramStatusMenuCallbackAction, openTelegramStatusMenu, sendTelegramStatusMessage, updateTelegramStatusMessage, } from "./menu-status.ts";
@@ -93,6 +93,7 @@ export interface TelegramMenuActionRuntimeDeps<TContext, TModel extends MenuMode
93
93
  threadId?: number;
94
94
  };
95
95
  parseMode?: "HTML";
96
+ assertAuthority?: TelegramApiCallOptions["assertAuthority"];
96
97
  }) => Promise<unknown>;
97
98
  sectionRegistry?: TelegramSectionRegistry;
98
99
  isVoiceReplyActive?: () => boolean;
@@ -101,70 +102,16 @@ export interface TelegramMenuActionRuntime<TContext, TModel extends MenuModel =
101
102
  updateModelMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
102
103
  updateThinkingMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
103
104
  updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
104
- sendStatusMessage: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number) => Promise<void>;
105
- openModelMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number) => Promise<void>;
106
- openThinkingMenu: (chatId: number, replyToMessageId: number, ctx: TContext) => Promise<void>;
105
+ sendStatusMessage: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
106
+ openModelMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
107
+ openThinkingMenu: (chatId: number, replyToMessageId: number, ctx: TContext, threadId?: number, options?: Pick<TelegramApiCallOptions, "assertAuthority">) => Promise<void>;
107
108
  }
108
- export type TelegramMenuCallbackAction = {
109
- kind: "ignore";
110
- } | {
111
- kind: "status";
112
- action: "model" | "thinking" | "queue" | "settings";
113
- } | {
114
- kind: "thinking:set";
115
- level: string;
116
- } | {
117
- kind: "model";
118
- action: "noop" | "scope" | "page" | "pages" | "open" | "pick" | "pick-selected" | "scope-enable" | "scope-disable" | "scope-toggle";
119
- value?: string;
120
- };
121
- export declare function parseTelegramMenuCallbackAction(data: string | undefined): TelegramMenuCallbackAction;
122
109
  export declare function handleTelegramMenuCallbackEntry(callbackQueryId: string, data: string | undefined, state: TelegramModelMenuState | undefined, deps: TelegramMenuCallbackEntryDeps): Promise<void>;
123
110
  export declare function handleStoredTelegramMenuCallback<TModel extends MenuModel = MenuModel>(query: MenuCallbackQuery, deps: StoredTelegramMenuCallbackDeps<TModel>): Promise<void>;
124
- export interface TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel extends MenuModel = MenuModel> {
125
- getStoredModelMenuState: (messageId: number | undefined, chatId?: number) => TelegramModelMenuState<TModel> | undefined;
126
- getActiveModel: (ctx: TContext) => TModel | undefined;
127
- getThinkingLevel: () => ThinkingLevel;
128
- setThinkingLevel: (level: ThinkingLevel) => void;
129
- updateStatus: (ctx: TContext, error?: string) => void;
130
- updateModelMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
131
- updateThinkingMenuMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
132
- updateStatusMessage: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
133
- updateSettingsMenuMessage?: (state: TelegramModelMenuState<TModel>, ctx: TContext) => Promise<void>;
134
- answerCallbackQuery: (callbackQueryId: string, text?: string) => Promise<void>;
135
- isIdle: (ctx: TContext) => boolean;
136
- hasAbortHandler: () => boolean;
111
+ export type TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel extends MenuModel = MenuModel> = Omit<TelegramMenuCallbackRuntimeDeps<TContext, TModel>, "hasActiveToolExecutions"> & {
137
112
  getActiveToolExecutions: () => number;
138
- persistScopedModelPatterns?: (patterns: string[], ctx: TContext) => Promise<void>;
139
- setModel: (model: TModel) => Promise<boolean>;
140
- setCurrentModel: (model: TModel, ctx: TContext) => void;
141
- stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => void;
142
- restartInterruptedTelegramTurn: (selection: ScopedTelegramModel<TModel>, ctx: TContext, continuationTurn: TelegramModelSwitchContinuationSource) => Promise<boolean> | boolean;
143
- sectionRegistry?: TelegramSectionRegistry;
144
- editInteractiveMessage?: (chatId: number, messageId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup) => Promise<void>;
145
- sendInteractiveMessage?: (chatId: number, text: string, mode: "markdown" | "html" | "plain", replyMarkup: TelegramReplyMarkup, options?: {
146
- target?: {
147
- chatId: number;
148
- threadId?: number;
149
- };
150
- }) => Promise<number | undefined>;
151
- sendSectionRichMessage?: (chatId: number, message: TelegramInputRichMessage, options?: {
152
- target?: {
153
- chatId: number;
154
- threadId?: number;
155
- };
156
- }) => Promise<number | undefined>;
157
- enqueueSectionPrompt?: (prompt: string, ctx: TContext, target?: {
158
- chatId: number;
159
- threadId?: number;
160
- }, source?: unknown) => Promise<void>;
161
- deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
162
- }
113
+ };
163
114
  export declare function createTelegramMenuCallbackHandler<TQuery extends MenuCallbackQuery, TContext, TModel extends MenuModel = MenuModel>(deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>): (query: TQuery, ctx: TContext) => Promise<void>;
164
115
  export declare function createTelegramMenuCallbackHandlerForContext<TQuery extends MenuCallbackQuery, TContext, TModel extends MenuModel = MenuModel>(deps: TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel>): (query: TQuery, ctx: TContext) => Promise<void>;
165
116
  export declare function handleTelegramMenuCallbackRuntime<TQuery extends MenuCallbackQuery, TContext, TModel extends MenuModel = MenuModel>(query: TQuery, ctx: TContext, deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>): Promise<void>;
166
- export interface TelegramMenuActionRuntimeWithStateBuilderDeps<TModel extends MenuModel = MenuModel, TContext extends TelegramModelMenuStateBuilderContext<TModel> = TelegramModelMenuStateBuilderContext<TModel>> extends Omit<TelegramMenuActionRuntimeDeps<TContext, TModel>, "getModelMenuState">, TelegramModelMenuStateBuilderDeps<TModel, TContext> {
167
- isVoiceReplyActive?: () => boolean;
168
- }
169
- export declare function createTelegramMenuActionRuntimeWithStateBuilder<TModel extends MenuModel = MenuModel, TContext extends TelegramModelMenuStateBuilderContext<TModel> = TelegramModelMenuStateBuilderContext<TModel>>(deps: TelegramMenuActionRuntimeWithStateBuilderDeps<TModel, TContext>): TelegramMenuActionRuntime<TContext, TModel>;
170
117
  export declare function createTelegramMenuActionRuntime<TContext, TModel extends MenuModel = MenuModel>(deps: TelegramMenuActionRuntimeDeps<TContext, TModel>): TelegramMenuActionRuntime<TContext, TModel>;
package/dist/lib/menu.js CHANGED
@@ -3,56 +3,20 @@
3
3
  * Zones: telegram ui, controls, status menu
4
4
  * Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
5
5
  */
6
- import { createTelegramModelMenuStateBuilder, handleTelegramModelMenuCallbackAction, openTelegramModelMenu, sendTelegramModelMenuMessage, updateTelegramModelMenuMessage, } from "./menu-model.js";
6
+ import { createTelegramMenuDelivery, handleTelegramModelMenuCallbackAction, openTelegramModelMenu, sendTelegramModelMenuMessage, updateTelegramModelMenuMessage, } from "./menu-model.js";
7
7
  import { handleTelegramStatusMenuCallbackAction, openTelegramStatusMenu, sendTelegramStatusMessage, updateTelegramStatusMessage, } from "./menu-status.js";
8
8
  import { handleTelegramThinkingMenuCallbackAction, openTelegramThinkingMenu, updateTelegramThinkingMenuMessage, } from "./menu-thinking.js";
9
9
  import { handleTelegramSectionCallback, handleTelegramSectionOpen, handleTelegramSectionSettingsOpen, parseTelegramSectionCallback, } from "./sections.js";
10
10
  export { applyTelegramModelPageSelection, applyTelegramModelScopeSelection, buildModelMenuReplyMarkup, buildModelPageMenuReplyMarkup, buildTelegramModelCallbackPlan, buildTelegramModelMenuRenderPayload, buildTelegramModelMenuState, buildTelegramModelMenuStateRuntime, buildTelegramModelPageMenuRenderPayload, createTelegramModelMenuRuntime, createTelegramModelMenuStateBuilder, formatScopedModelButtonText, getModelMenuItems, getStoredTelegramModelMenuState, getTelegramModelMenuPage, getTelegramModelSelection, handleTelegramModelMenuCallbackAction, MODEL_MENU_TITLE, MODEL_PAGE_MENU_TITLE, openTelegramModelMenu, pruneStoredTelegramModelMenus, resolveCachedTelegramModelMenuInputs, sendTelegramModelMenuMessage, storeTelegramModelMenuState, TELEGRAM_MODEL_PAGE_SIZE, updateTelegramModelMenuMessage, } from "./menu-model.js";
11
11
  export { buildStatusReplyMarkup, buildTelegramStatusMenuRenderPayload, handleTelegramStatusMenuCallbackAction, openTelegramStatusMenu, sendTelegramStatusMessage, updateTelegramStatusMessage, } from "./menu-status.js";
12
12
  export { buildTelegramThinkingMenuRenderPayload, buildThinkingMenuReplyMarkup, buildThinkingMenuText, handleTelegramThinkingMenuCallbackAction, openTelegramThinkingMenu, updateTelegramThinkingMenuMessage, } from "./menu-thinking.js";
13
- export function parseTelegramMenuCallbackAction(data) {
14
- if (data === "menu:model" || data === "status:model") {
15
- return { kind: "status", action: "model" };
16
- }
17
- if (data === "menu:thinking" || data === "status:thinking") {
18
- return { kind: "status", action: "thinking" };
19
- }
20
- if (data === "menu:queue" || data === "status:queue") {
21
- return { kind: "status", action: "queue" };
22
- }
23
- if (data === "menu:settings" || data === "status:settings") {
24
- return { kind: "status", action: "settings" };
25
- }
26
- if (data?.startsWith("thinking:set:")) {
27
- return {
28
- kind: "thinking:set",
29
- level: data.slice("thinking:set:".length),
30
- };
31
- }
32
- if (data?.startsWith("model:")) {
33
- const [, action, value] = data.split(":");
34
- if (action === "noop" ||
35
- action === "scope" ||
36
- action === "page" ||
37
- action === "pages" ||
38
- action === "open" ||
39
- action === "pick" ||
40
- action === "pick-selected" ||
41
- action === "scope-enable" ||
42
- action === "scope-disable" ||
43
- action === "scope-toggle") {
44
- return { kind: "model", action, value };
45
- }
46
- }
47
- return { kind: "ignore" };
48
- }
49
13
  export async function handleTelegramMenuCallbackEntry(callbackQueryId, data, state, deps) {
50
14
  if (!data) {
51
15
  await deps.answerCallbackQuery(callbackQueryId);
52
16
  return;
53
17
  }
54
18
  if (!state) {
55
- await deps.answerCallbackQuery(callbackQueryId, "Interactive message expired.");
19
+ await deps.answerCallbackQuery(callbackQueryId, "Interactive message expired");
56
20
  return;
57
21
  }
58
22
  const handled = (await deps.handleStatusAction()) ||
@@ -87,38 +51,17 @@ export function createTelegramMenuCallbackHandler(deps) {
87
51
  return (query, ctx) => handleTelegramMenuCallbackRuntime(query, ctx, deps);
88
52
  }
89
53
  export function createTelegramMenuCallbackHandlerForContext(deps) {
54
+ const { getActiveToolExecutions, ...ports } = deps;
90
55
  return createTelegramMenuCallbackHandler({
91
- getStoredModelMenuState: deps.getStoredModelMenuState,
92
- getActiveModel: deps.getActiveModel,
93
- getThinkingLevel: deps.getThinkingLevel,
94
- setThinkingLevel: deps.setThinkingLevel,
95
- updateStatus: deps.updateStatus,
96
- updateModelMenuMessage: deps.updateModelMenuMessage,
97
- updateThinkingMenuMessage: deps.updateThinkingMenuMessage,
98
- updateStatusMessage: deps.updateStatusMessage,
99
- updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
100
- answerCallbackQuery: deps.answerCallbackQuery,
101
- isIdle: deps.isIdle,
102
- hasAbortHandler: deps.hasAbortHandler,
103
- hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
104
- persistScopedModelPatterns: deps.persistScopedModelPatterns,
105
- setModel: deps.setModel,
106
- setCurrentModel: deps.setCurrentModel,
107
- stagePendingModelSwitch: deps.stagePendingModelSwitch,
108
- restartInterruptedTelegramTurn: deps.restartInterruptedTelegramTurn,
109
- sectionRegistry: deps.sectionRegistry,
110
- editInteractiveMessage: deps.editInteractiveMessage,
111
- sendInteractiveMessage: deps.sendInteractiveMessage,
112
- sendSectionRichMessage: deps.sendSectionRichMessage,
113
- enqueueSectionPrompt: deps.enqueueSectionPrompt,
114
- deleteMessage: deps.deleteMessage,
56
+ ...ports,
57
+ hasActiveToolExecutions: () => getActiveToolExecutions() > 0,
115
58
  });
116
59
  }
117
60
  export async function handleTelegramMenuCallbackRuntime(query, ctx, deps) {
118
61
  if (query.data === "menu:back") {
119
62
  const state = deps.getStoredModelMenuState(query.message?.message_id, query.message?.chat?.id);
120
63
  if (!state) {
121
- await deps.answerCallbackQuery(query.id, "Interactive message expired.");
64
+ await deps.answerCallbackQuery(query.id, "Interactive message expired");
122
65
  return;
123
66
  }
124
67
  await deps.updateStatusMessage(state, ctx);
@@ -139,61 +82,37 @@ export async function handleTelegramMenuCallbackRuntime(query, ctx, deps) {
139
82
  : undefined;
140
83
  if (typeof chatId === "number" && typeof messageId === "number") {
141
84
  const { token, action, payload } = parsed;
85
+ const sectionDeps = {
86
+ answerCallbackQuery: deps.answerCallbackQuery,
87
+ target,
88
+ editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
89
+ sendInteractiveMessage: deps.sendInteractiveMessage ?? (async () => undefined),
90
+ sendRichMessage: deps.sendSectionRichMessage ??
91
+ (async () => {
92
+ throw new Error("Rich Message delivery is unavailable");
93
+ }),
94
+ enqueuePrompt: deps.enqueueSectionPrompt
95
+ ? (prompt) => deps.enqueueSectionPrompt(prompt, ctx, target, query)
96
+ : async () => { },
97
+ deleteMessage: deps.deleteMessage ?? (async () => { }),
98
+ };
142
99
  if (action === "open") {
143
100
  const state = deps.getStoredModelMenuState(messageId, chatId);
144
101
  if (!state) {
145
- await deps.answerCallbackQuery(query.id, "Interactive message expired.");
102
+ await deps.answerCallbackQuery(query.id, "Interactive message expired");
146
103
  return;
147
104
  }
148
- const handled = await handleTelegramSectionOpen(deps.sectionRegistry, token, chatId, messageId, query.id, {
149
- answerCallbackQuery: deps.answerCallbackQuery,
150
- target,
151
- editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
152
- sendInteractiveMessage: deps.sendInteractiveMessage ?? (async () => undefined),
153
- sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
154
- throw new Error("Rich Message delivery is unavailable");
155
- }),
156
- enqueuePrompt: deps.enqueueSectionPrompt
157
- ? (prompt) => deps.enqueueSectionPrompt(prompt, ctx, target, query)
158
- : async () => { },
159
- deleteMessage: deps.deleteMessage ?? (async () => { }),
160
- });
105
+ const handled = await handleTelegramSectionOpen(deps.sectionRegistry, token, chatId, messageId, query.id, sectionDeps);
161
106
  if (handled)
162
107
  return;
163
108
  }
164
109
  else if (action === "settings") {
165
- if (typeof chatId === "number" && typeof messageId === "number") {
166
- const handled = await handleTelegramSectionSettingsOpen(deps.sectionRegistry, token, chatId, messageId, query.id, {
167
- answerCallbackQuery: deps.answerCallbackQuery,
168
- target,
169
- editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
170
- sendInteractiveMessage: deps.sendInteractiveMessage ?? (async () => undefined),
171
- sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
172
- throw new Error("Rich Message delivery is unavailable");
173
- }),
174
- enqueuePrompt: deps.enqueueSectionPrompt
175
- ? (prompt) => deps.enqueueSectionPrompt(prompt, ctx, target, query)
176
- : async () => { },
177
- deleteMessage: deps.deleteMessage ?? (async () => { }),
178
- });
179
- if (handled)
180
- return;
181
- }
110
+ const handled = await handleTelegramSectionSettingsOpen(deps.sectionRegistry, token, chatId, messageId, query.id, sectionDeps);
111
+ if (handled)
112
+ return;
182
113
  }
183
114
  else {
184
- const handled = await handleTelegramSectionCallback(deps.sectionRegistry, token, action, payload, chatId, messageId, query.id, {
185
- answerCallbackQuery: deps.answerCallbackQuery,
186
- target,
187
- editInteractiveMessage: deps.editInteractiveMessage ?? (async () => { }),
188
- sendInteractiveMessage: deps.sendInteractiveMessage ?? (async () => undefined),
189
- sendRichMessage: deps.sendSectionRichMessage ?? (async () => {
190
- throw new Error("Rich Message delivery is unavailable");
191
- }),
192
- enqueuePrompt: deps.enqueueSectionPrompt
193
- ? (prompt) => deps.enqueueSectionPrompt(prompt, ctx, target, query)
194
- : async () => { },
195
- deleteMessage: deps.deleteMessage ?? (async () => { }),
196
- });
115
+ const handled = await handleTelegramSectionCallback(deps.sectionRegistry, token, action, payload, chatId, messageId, query.id, sectionDeps);
197
116
  if (handled)
198
117
  return;
199
118
  }
@@ -257,67 +176,76 @@ export async function handleTelegramMenuCallbackRuntime(query, ctx, deps) {
257
176
  answerCallbackQuery: deps.answerCallbackQuery,
258
177
  });
259
178
  }
260
- export function createTelegramMenuActionRuntimeWithStateBuilder(deps) {
261
- return createTelegramMenuActionRuntime({
262
- getModelMenuState: createTelegramModelMenuStateBuilder({
263
- runtime: deps.runtime,
264
- createSettingsManager: deps.createSettingsManager,
265
- getActiveModel: deps.getActiveModel,
266
- }),
267
- getActiveModel: deps.getActiveModel,
268
- getThinkingLevel: deps.getThinkingLevel,
269
- getQueueItemCount: deps.getQueueItemCount,
270
- getPendingCancellationCount: deps.getPendingCancellationCount,
271
- buildStatusHtml: deps.buildStatusHtml,
272
- storeModelMenuState: deps.storeModelMenuState,
273
- isIdle: deps.isIdle,
274
- canOfferInFlightModelSwitch: deps.canOfferInFlightModelSwitch,
275
- sendTextReply: deps.sendTextReply,
276
- editInteractiveMessage: deps.editInteractiveMessage,
277
- sendInteractiveMessage: deps.sendInteractiveMessage,
278
- sectionRegistry: deps.sectionRegistry,
279
- });
280
- }
281
179
  export function createTelegramMenuActionRuntime(deps) {
282
180
  return {
283
181
  updateModelMenuMessage: (state, ctx) => updateTelegramModelMenuMessage(state, deps.getActiveModel(ctx), deps),
284
182
  updateThinkingMenuMessage: (state, ctx) => updateTelegramThinkingMenuMessage(state, deps.getActiveModel(ctx), deps.getThinkingLevel(), deps),
285
183
  updateStatusMessage: (state, ctx) => updateTelegramStatusMessage(state, deps.buildStatusHtml(ctx), deps.getActiveModel(ctx), deps.getThinkingLevel(), deps, deps.getQueueItemCount?.() ?? 0, deps.sectionRegistry, deps.isVoiceReplyActive?.(), deps.getPendingCancellationCount?.() ?? 0),
286
- sendStatusMessage: (chatId, replyToMessageId, ctx, threadId) => openTelegramStatusMenu({
287
- isIdle: () => deps.isIdle(ctx),
288
- sendBusyMessage: async () => {
289
- await deps.sendTextReply(chatId, replyToMessageId, "<b>⏳ Cannot open status while Pi is busy. Send /abort, /next, or /stop.</b>", { target: { chatId, threadId }, parseMode: "HTML" });
290
- },
291
- getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
292
- buildStatusHtml: () => deps.buildStatusHtml(ctx),
293
- getActiveModel: () => deps.getActiveModel(ctx),
294
- getThinkingLevel: deps.getThinkingLevel,
295
- getQueueItemCount: deps.getQueueItemCount,
296
- sendStatusMenu: (state, statusHtml, activeModel, thinkingLevel, queueItemCount) => sendTelegramStatusMessage(state, statusHtml, activeModel, thinkingLevel, deps, queueItemCount, deps.sectionRegistry, deps.isVoiceReplyActive?.(), deps.getPendingCancellationCount?.() ?? 0),
297
- storeModelMenuState: deps.storeModelMenuState,
298
- }),
299
- openModelMenu: (chatId, replyToMessageId, ctx, threadId) => openTelegramModelMenu({
300
- isIdle: () => deps.isIdle(ctx),
301
- canOfferInFlightModelSwitch: () => deps.canOfferInFlightModelSwitch(ctx),
302
- sendBusyMessage: async () => {
303
- await deps.sendTextReply(chatId, replyToMessageId, "<b>⏳ Cannot switch model while Pi is busy. Send /abort, /next, or /stop.</b>", { target: { chatId, threadId }, parseMode: "HTML" });
304
- },
305
- sendNoModelsMessage: async () => {
306
- await deps.sendTextReply(chatId, replyToMessageId, "<b>🚫 No available models with configured auth.</b>", { target: { chatId, threadId }, parseMode: "HTML" });
307
- },
308
- getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
309
- getActiveModel: () => deps.getActiveModel(ctx),
310
- sendModelMenu: (state, activeModel) => sendTelegramModelMenuMessage(state, activeModel, deps),
311
- storeModelMenuState: deps.storeModelMenuState,
312
- }),
313
- openThinkingMenu: (chatId, _replyToMessageId, ctx) => openTelegramThinkingMenu({
314
- getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
315
- getActiveModel: () => deps.getActiveModel(ctx),
316
- getThinkingLevel: deps.getThinkingLevel,
317
- storeModelMenuState: deps.storeModelMenuState,
318
- editInteractiveMessage: deps.editInteractiveMessage,
319
- sendInteractiveMessage: deps.sendInteractiveMessage,
320
- isVoiceReplyActive: deps.isVoiceReplyActive,
321
- }),
184
+ sendStatusMessage: (chatId, replyToMessageId, ctx, threadId, options) => {
185
+ const delivery = createTelegramMenuDelivery({ chatId, threadId }, options, deps);
186
+ return openTelegramStatusMenu({
187
+ isIdle: () => deps.isIdle(ctx),
188
+ sendBusyMessage: async () => {
189
+ await deps.sendTextReply(chatId, replyToMessageId, "<b>⏳ Cannot open status while Pi is busy. Send /abort, /next, or /stop.</b>", {
190
+ target: { chatId, threadId },
191
+ parseMode: "HTML",
192
+ ...(delivery.assertAuthority
193
+ ? { assertAuthority: delivery.assertAuthority }
194
+ : {}),
195
+ });
196
+ },
197
+ getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
198
+ buildStatusHtml: () => deps.buildStatusHtml(ctx),
199
+ getActiveModel: () => deps.getActiveModel(ctx),
200
+ getThinkingLevel: deps.getThinkingLevel,
201
+ getQueueItemCount: deps.getQueueItemCount,
202
+ sendStatusMenu: (state, statusHtml, activeModel, thinkingLevel, queueItemCount) => sendTelegramStatusMessage(state, statusHtml, activeModel, thinkingLevel, {
203
+ ...deps,
204
+ sendInteractiveMessage: delivery.sendInteractiveMessage,
205
+ }, queueItemCount, deps.sectionRegistry, deps.isVoiceReplyActive?.(), deps.getPendingCancellationCount?.() ?? 0),
206
+ storeModelMenuState: delivery.storeModelMenuState,
207
+ });
208
+ },
209
+ openModelMenu: (chatId, replyToMessageId, ctx, threadId, options) => {
210
+ const delivery = createTelegramMenuDelivery({ chatId, threadId }, options, deps);
211
+ const assertAuthority = delivery.assertAuthority;
212
+ return openTelegramModelMenu({
213
+ isIdle: () => deps.isIdle(ctx),
214
+ canOfferInFlightModelSwitch: () => deps.canOfferInFlightModelSwitch(ctx),
215
+ sendBusyMessage: async () => {
216
+ await deps.sendTextReply(chatId, replyToMessageId, "<b>⏳ Cannot switch model while Pi is busy. Send /abort, /next, or /stop.</b>", {
217
+ target: { chatId, threadId },
218
+ parseMode: "HTML",
219
+ ...(assertAuthority ? { assertAuthority } : {}),
220
+ });
221
+ },
222
+ sendNoModelsMessage: async () => {
223
+ await deps.sendTextReply(chatId, replyToMessageId, "<b>🚫 No available models with configured auth.</b>", {
224
+ target: { chatId, threadId },
225
+ parseMode: "HTML",
226
+ ...(assertAuthority ? { assertAuthority } : {}),
227
+ });
228
+ },
229
+ getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
230
+ getActiveModel: () => deps.getActiveModel(ctx),
231
+ sendModelMenu: (state, activeModel) => sendTelegramModelMenuMessage(state, activeModel, {
232
+ ...deps,
233
+ sendInteractiveMessage: delivery.sendInteractiveMessage,
234
+ }),
235
+ storeModelMenuState: delivery.storeModelMenuState,
236
+ });
237
+ },
238
+ openThinkingMenu: (chatId, _replyToMessageId, ctx, threadId, options) => {
239
+ const delivery = createTelegramMenuDelivery({ chatId, threadId }, options, deps);
240
+ return openTelegramThinkingMenu({
241
+ getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
242
+ getActiveModel: () => deps.getActiveModel(ctx),
243
+ getThinkingLevel: deps.getThinkingLevel,
244
+ storeModelMenuState: delivery.storeModelMenuState,
245
+ editInteractiveMessage: deps.editInteractiveMessage,
246
+ sendInteractiveMessage: delivery.sendInteractiveMessage,
247
+ isVoiceReplyActive: deps.isVoiceReplyActive,
248
+ });
249
+ },
322
250
  };
323
251
  }
@@ -3,7 +3,7 @@
3
3
  * Zones: pi agent model control, telegram controls, queue continuation
4
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
5
5
  */
6
- import type { PendingTelegramTurn } from "./queue.ts";
6
+ import { type PendingTelegramTurn } from "./queue.ts";
7
7
  export interface MenuModel {
8
8
  provider: string;
9
9
  id: string;
package/dist/lib/model.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * Zones: pi agent model control, telegram controls, queue continuation
4
4
  * Owns model identity, thinking levels, scoped resolution, current-model state, and in-flight model switching
5
5
  */
6
+ import { truncateTelegramQueueSummary, } from "./queue.js";
6
7
  import { TELEGRAM_PREFIX } from "./turns.js";
7
8
  export const THINKING_LEVELS = [
8
9
  "off",
@@ -277,21 +278,6 @@ export function restartTelegramModelSwitchContinuation(state) {
277
278
  state.abort();
278
279
  return true;
279
280
  }
280
- function truncateTelegramModelSwitchStatusSummary(text, maxWords = 4, maxLength = 32) {
281
- const normalized = text.replace(/\s+/g, " ").trim();
282
- if (!normalized)
283
- return "";
284
- const words = normalized.split(" ");
285
- let summary = words.slice(0, maxWords).join(" ");
286
- if (summary.length === 0)
287
- summary = normalized;
288
- if (summary.length > maxLength) {
289
- summary = summary.slice(0, maxLength).trimEnd();
290
- }
291
- return summary.length < normalized.length || words.length > maxWords
292
- ? `${summary}…`
293
- : summary;
294
- }
295
281
  export function buildTelegramModelSwitchContinuationText(telegramPrefix, model, thinkingLevel) {
296
282
  const modelLabel = `${model.provider}/${model.id}`;
297
283
  const thinkingSuffix = thinkingLevel ? `; thinking: ${thinkingLevel}` : "";
@@ -299,7 +285,7 @@ export function buildTelegramModelSwitchContinuationText(telegramPrefix, model,
299
285
  }
300
286
  export function buildTelegramModelSwitchContinuationTurn(options) {
301
287
  const modelLabel = `${options.selection.model.provider}/${options.selection.model.id}`;
302
- const statusLabel = truncateTelegramModelSwitchStatusSummary(`continue on ${options.selection.model.id}`);
288
+ const statusLabel = truncateTelegramQueueSummary(`continue on ${options.selection.model.id}`, 4, 32);
303
289
  return {
304
290
  kind: "prompt",
305
291
  chatId: options.turn.chatId,