@llblab/pi-telegram 0.37.2 → 0.39.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.
package/lib/commands.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  TELEGRAM_DEFAULT_PROFILE_NAME,
10
10
  } from "./config.ts";
11
11
  import type { ExtensionAPI, ExtensionCommandContext } from "./pi.ts";
12
+ import { escapeHtml } from "./rendering.ts";
12
13
  import type { TelegramBridgeStatusLineOptions } from "./status.ts";
13
14
  import {
14
15
  createTelegramControlItemBuilder,
@@ -185,9 +186,24 @@ export function formatTelegramCommandEmojiPrefix(
185
186
  return `${getTelegramCommandEmoji(command)} `;
186
187
  }
187
188
 
189
+ export function formatTelegramInformationHeading(
190
+ emoji: string,
191
+ text: string,
192
+ ): string {
193
+ return `<b>${escapeHtml(emoji)} ${escapeHtml(text)}</b>`;
194
+ }
195
+
188
196
  export const TELEGRAM_COMPACTION_STARTED_TEXT =
189
- `${formatTelegramCommandEmojiPrefix("compact")}Compaction started.`;
190
- export const TELEGRAM_COMPACTION_COMPLETED_TEXT = "✅ Compaction completed.";
197
+ formatTelegramInformationHeading(
198
+ getTelegramCommandEmoji("compact"),
199
+ "Compaction started.",
200
+ );
201
+ export const TELEGRAM_COMPACTION_COMPLETED_TEXT =
202
+ formatTelegramInformationHeading("✅", "Compaction completed.");
203
+ export const TELEGRAM_COMPACTION_STARTED_MARKDOWN =
204
+ `**${formatTelegramCommandEmojiPrefix("compact")}Compaction started.**`;
205
+ export const TELEGRAM_COMPACTION_COMPLETED_MARKDOWN =
206
+ "**✅ Compaction completed.**";
191
207
 
192
208
  function formatTelegramBotCommandDescription(
193
209
  command: TelegramCommandEmojiName,
@@ -585,7 +601,10 @@ export interface TelegramStopCommandDeps {
585
601
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
586
602
  abortCurrentTurn: () => void;
587
603
  updateStatus: () => void;
588
- sendTextReply: (text: string) => Promise<void>;
604
+ sendTextReply: (
605
+ text: string,
606
+ options?: { parseMode?: "HTML" },
607
+ ) => Promise<void>;
589
608
  }
590
609
 
591
610
  export interface TelegramRuntimeEventRecorderPort {
@@ -619,7 +638,10 @@ export interface TelegramCompactCommandDeps extends TelegramRuntimeEventRecorder
619
638
  onComplete: () => void;
620
639
  onError: (error: unknown) => void;
621
640
  }) => void;
622
- sendTextReply: (text: string) => Promise<void>;
641
+ sendTextReply: (
642
+ text: string,
643
+ options?: { parseMode?: "HTML" },
644
+ ) => Promise<void>;
623
645
  suppressStartNotice?: boolean;
624
646
  }
625
647
 
@@ -921,8 +943,11 @@ export function createTelegramCommandTargetRuntime<
921
943
  await deps.sendTextReply(
922
944
  target.chatId,
923
945
  target.replyToMessageId,
924
- "Settings menu is unavailable.",
925
- { target },
946
+ formatTelegramInformationHeading(
947
+ "🚫",
948
+ "Settings menu is unavailable.",
949
+ ),
950
+ { target, parseMode: "HTML" },
926
951
  );
927
952
  return;
928
953
  }
@@ -1165,7 +1190,10 @@ export async function handleTelegramStopCommand(
1165
1190
  ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
1166
1191
  : "";
1167
1192
  if (clearedCount > 0) deps.updateStatus();
1168
- await deps.sendTextReply(`No active turn.${clearedSuffix}`);
1193
+ await deps.sendTextReply(
1194
+ formatTelegramInformationHeading("💤", `No active turn.${clearedSuffix}`),
1195
+ { parseMode: "HTML" },
1196
+ );
1169
1197
  return;
1170
1198
  }
1171
1199
  deps.abortCurrentTurn();
@@ -1174,7 +1202,13 @@ export async function handleTelegramStopCommand(
1174
1202
  clearedCount > 0
1175
1203
  ? ` Cleared ${formatTelegramQueuedTurnCount(clearedCount)}.`
1176
1204
  : "";
1177
- await deps.sendTextReply(`Aborted current turn.${clearedSuffix}`);
1205
+ await deps.sendTextReply(
1206
+ formatTelegramInformationHeading(
1207
+ "⏹️",
1208
+ `Aborted current turn.${clearedSuffix}`,
1209
+ ),
1210
+ { parseMode: "HTML" },
1211
+ );
1178
1212
  }
1179
1213
 
1180
1214
  export async function handleTelegramAbortCommand(deps: {
@@ -1184,17 +1218,26 @@ export async function handleTelegramAbortCommand(deps: {
1184
1218
  abortCurrentTurn: () => void;
1185
1219
  setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
1186
1220
  updateStatus: () => void;
1187
- sendTextReply: (text: string) => Promise<void>;
1221
+ sendTextReply: (
1222
+ text: string,
1223
+ options?: { parseMode?: "HTML" },
1224
+ ) => Promise<void>;
1188
1225
  }): Promise<void> {
1189
1226
  deps.clearPendingModelSwitch();
1190
1227
  if (!deps.hasAbortHandler()) {
1191
- await deps.sendTextReply("No active turn.");
1228
+ await deps.sendTextReply(
1229
+ formatTelegramInformationHeading("💤", "No active turn."),
1230
+ { parseMode: "HTML" },
1231
+ );
1192
1232
  return;
1193
1233
  }
1194
1234
  deps.setFoldQueuedPromptsIntoHistory(deps.hasActiveTelegramTurn());
1195
1235
  deps.abortCurrentTurn();
1196
1236
  deps.updateStatus();
1197
- await deps.sendTextReply("Aborted current turn.");
1237
+ await deps.sendTextReply(
1238
+ formatTelegramInformationHeading("⏹️", "Aborted current turn."),
1239
+ { parseMode: "HTML" },
1240
+ );
1198
1241
  }
1199
1242
 
1200
1243
  export async function handleTelegramNextCommand(deps: {
@@ -1213,7 +1256,10 @@ export async function handleTelegramNextCommand(deps: {
1213
1256
  }): Promise<void> {
1214
1257
  deps.clearPendingModelSwitch();
1215
1258
  if (!deps.hasQueuedItems()) {
1216
- await deps.sendTextReply("<b>Queue is empty.</b>", { parseMode: "HTML" });
1259
+ await deps.sendTextReply(
1260
+ formatTelegramInformationHeading("⌛", "Queue is empty."),
1261
+ { parseMode: "HTML" },
1262
+ );
1217
1263
  return;
1218
1264
  }
1219
1265
  if (!deps.isIdle() && deps.hasAbortHandler()) {
@@ -1221,17 +1267,30 @@ export async function handleTelegramNextCommand(deps: {
1221
1267
  deps.abortCurrentTurn();
1222
1268
  deps.updateStatus();
1223
1269
  await deps.sendTextReply(
1224
- "Aborted current turn. Dispatching next queued turn.",
1270
+ formatTelegramInformationHeading(
1271
+ "⏩",
1272
+ "Aborted! Dispatching next queued turn.",
1273
+ ),
1274
+ { parseMode: "HTML" },
1225
1275
  );
1226
1276
  return;
1227
1277
  }
1228
1278
  if (!deps.isIdle()) {
1229
- await deps.sendTextReply("Pi is busy. Send /abort or /stop first.");
1279
+ await deps.sendTextReply(
1280
+ formatTelegramInformationHeading(
1281
+ "⏳",
1282
+ "Pi is busy. Send /abort or /stop first.",
1283
+ ),
1284
+ { parseMode: "HTML" },
1285
+ );
1230
1286
  return;
1231
1287
  }
1232
1288
  deps.dispatchNextQueuedTurn();
1233
1289
  deps.updateStatus();
1234
- await deps.sendTextReply("Dispatching next queued turn.");
1290
+ await deps.sendTextReply(
1291
+ formatTelegramInformationHeading("▶️", "Dispatching next queued turn."),
1292
+ { parseMode: "HTML" },
1293
+ );
1235
1294
  }
1236
1295
 
1237
1296
  export async function handleTelegramContinueCommand<TMessage, TContext>(
@@ -1301,15 +1360,15 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
1301
1360
  const chatId = callbackMessage?.chat?.id;
1302
1361
  const messageId = callbackMessage?.message_id;
1303
1362
  if (typeof chatId !== "number" || typeof messageId !== "number") {
1304
- await deps.answerCallbackQuery(query.id, "Interactive message expired.");
1363
+ await deps.answerCallbackQuery(query.id, "Interactive message expired.");
1305
1364
  return true;
1306
1365
  }
1307
1366
  if (query.data === "compact:cancel") {
1308
1367
  await deps.editInteractiveMessage(
1309
1368
  chatId,
1310
1369
  messageId,
1311
- "Compaction cancelled.",
1312
- "plain",
1370
+ "<b>🚫 Compaction cancelled.</b>",
1371
+ "html",
1313
1372
  { inline_keyboard: [] },
1314
1373
  );
1315
1374
  await deps.answerCallbackQuery(query.id);
@@ -1319,7 +1378,7 @@ export async function handleTelegramCompactConfirmationCallback<TContext>(
1319
1378
  chatId,
1320
1379
  messageId,
1321
1380
  TELEGRAM_COMPACTION_STARTED_TEXT,
1322
- "plain",
1381
+ "html",
1323
1382
  { inline_keyboard: [] },
1324
1383
  );
1325
1384
  await deps.answerCallbackQuery(query.id);
@@ -1345,7 +1404,11 @@ export async function handleTelegramCompactCommand(
1345
1404
  deps.isCompactionInProgress()
1346
1405
  ) {
1347
1406
  await deps.sendTextReply(
1348
- "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
1407
+ formatTelegramInformationHeading(
1408
+ "⏳",
1409
+ "Cannot compact while Pi or the Telegram queue is busy. Wait for queued turns to finish or send /abort first.",
1410
+ ),
1411
+ { parseMode: "HTML" },
1349
1412
  );
1350
1413
  return;
1351
1414
  }
@@ -1359,7 +1422,9 @@ export async function handleTelegramCompactCommand(
1359
1422
  deps.setCompactionInProgress(false);
1360
1423
  deps.updateStatus();
1361
1424
  dispatchNextQueuedTelegramTurnAfterCompact(deps);
1362
- void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT);
1425
+ void deps.sendTextReply(TELEGRAM_COMPACTION_COMPLETED_TEXT, {
1426
+ parseMode: "HTML",
1427
+ });
1363
1428
  },
1364
1429
  onError: (error) => {
1365
1430
  deps.stopTypingLoop?.();
@@ -1368,7 +1433,13 @@ export async function handleTelegramCompactCommand(
1368
1433
  dispatchNextQueuedTelegramTurnAfterCompact(deps);
1369
1434
  deps.recordRuntimeEvent?.("compact", error);
1370
1435
  const errorMessage = getTelegramCommandErrorMessage(error);
1371
- void deps.sendTextReply(`Compaction failed: ${errorMessage}`);
1436
+ void deps.sendTextReply(
1437
+ formatTelegramInformationHeading(
1438
+ "❌",
1439
+ `Compaction failed: ${errorMessage}`,
1440
+ ),
1441
+ { parseMode: "HTML" },
1442
+ );
1372
1443
  },
1373
1444
  });
1374
1445
  } catch (error) {
@@ -1377,13 +1448,19 @@ export async function handleTelegramCompactCommand(
1377
1448
  deps.updateStatus();
1378
1449
  deps.recordRuntimeEvent?.("compact", error);
1379
1450
  const errorMessage = getTelegramCommandErrorMessage(error);
1380
- await deps.sendTextReply(`Compaction failed: ${errorMessage}`);
1451
+ await deps.sendTextReply(
1452
+ formatTelegramInformationHeading(
1453
+ "❌",
1454
+ `Compaction failed: ${errorMessage}`,
1455
+ ),
1456
+ { parseMode: "HTML" },
1457
+ );
1381
1458
  return;
1382
1459
  }
1383
1460
  if (!deps.suppressStartNotice) {
1384
- await deps.sendTextReply(
1385
- TELEGRAM_COMPACTION_STARTED_TEXT,
1386
- );
1461
+ await deps.sendTextReply(TELEGRAM_COMPACTION_STARTED_TEXT, {
1462
+ parseMode: "HTML",
1463
+ });
1387
1464
  }
1388
1465
  }
1389
1466
 
package/lib/config.ts CHANGED
@@ -118,9 +118,8 @@ export interface TelegramConfig {
118
118
  /** @deprecated use assistant.rendering */
119
119
  assistantRendering?: TelegramAssistantRenderingMode;
120
120
  voice?: {
121
- replyMode?: "hidden" | "mirror" | "always";
122
- /** Whether to attach the provider's transcriptText as caption on voice messages */
123
- sendTranscript?: boolean;
121
+ /** `hidden` is a read-only compatibility alias for the former manual mode. */
122
+ replyMode?: "manual" | "hidden" | "mirror" | "always";
124
123
  };
125
124
  time?: TelegramTimeConfig;
126
125
  threads?: {
@@ -845,10 +844,10 @@ export function createTelegramActivityVerbositySetter(
845
844
 
846
845
  export function createTelegramVoiceReplyModeGetter(
847
846
  configStore: Pick<TelegramConfigStore, "get">,
848
- ): () => "hidden" | "mirror" | "always" {
847
+ ): () => "manual" | "mirror" | "always" {
849
848
  return () => {
850
849
  const mode = configStore.get().voice?.replyMode;
851
- return mode === "mirror" || mode === "always" ? mode : "hidden";
850
+ return mode === "mirror" || mode === "always" ? mode : "manual";
852
851
  };
853
852
  }
854
853
 
@@ -863,11 +862,15 @@ export function createTelegramVoiceReplyModeConfiguredChecker(
863
862
 
864
863
  export function createTelegramVoiceReplyModeSetter(
865
864
  configStore: TelegramMutableConfigStore,
866
- ): (replyMode: "hidden" | "mirror" | "always" | undefined) => Promise<void> {
865
+ ): (replyMode: "manual" | "hidden" | "mirror" | "always" | undefined) => Promise<void> {
867
866
  return async (replyMode) => {
868
867
  await loadLatestTelegramConfig(configStore);
869
868
  const current = configStore.get();
870
- if (replyMode === undefined || replyMode === "hidden") {
869
+ if (
870
+ replyMode === undefined ||
871
+ replyMode === "manual" ||
872
+ replyMode === "hidden"
873
+ ) {
871
874
  const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
872
875
  const next = { ...current };
873
876
  if (Object.keys(remainingVoice).length > 0) next.voice = remainingVoice;
package/lib/journal.ts CHANGED
@@ -3381,15 +3381,8 @@ export function createTelegramUpdateJournalStore(
3381
3381
  const recoveredUpdateIds = [...requestedIds].sort((a, b) => a - b);
3382
3382
  const published = publishMutation(
3383
3383
  current,
3384
- current.file.entries.map((entry) =>
3385
- requestedIds.has(entry.updateId)
3386
- ? {
3387
- updateId: entry.updateId,
3388
- update: entry.update,
3389
- admittedAtMs: entry.admittedAtMs,
3390
- state: "pending" as const,
3391
- }
3392
- : entry,
3384
+ current.file.entries.filter(
3385
+ (entry) => !requestedIds.has(entry.updateId),
3393
3386
  ),
3394
3387
  true,
3395
3388
  );
package/lib/lifecycle.ts CHANGED
@@ -345,25 +345,27 @@ export function createTelegramBridgeSessionLifecycleAssembly<
345
345
  stopPolling: suspendForReplacement,
346
346
  clearPendingMediaGroups: deps.services.suspendGroupedInput,
347
347
  });
348
- const servicesLifecycle = appendTelegramLifecycleHooks(
349
- queueLifecycle,
350
- {
351
- async onSessionStart(event, ctx) {
352
- deps.services.resumeGroupedInput(ctx);
353
- await deps.services.delivery.onSessionStart();
354
- await deps.services.polling.onSessionStart(event, ctx);
355
- deps.services.capabilityMonitor.start(ctx);
356
- deps.services.queueWatchdog.start(ctx);
357
- },
358
- async onSessionShutdown() {
359
- await deps.services.delivery.onSessionShutdown();
360
- deps.services.queueWatchdog.stop();
361
- deps.services.capabilityMonitor.stop();
362
- await deps.services.inboundWorker.onSessionShutdown();
363
- },
348
+ const servicesLifecycle: TelegramSessionLifecycleHooks = {
349
+ async onSessionStart(event, ctx) {
350
+ await queueLifecycle.onSessionStart(event, ctx);
351
+ if (!isSessionActive(ctx)) return;
352
+ deps.services.resumeGroupedInput(ctx);
353
+ await deps.services.delivery.onSessionStart();
354
+ await deps.services.polling.onSessionStart(event, ctx);
355
+ deps.services.capabilityMonitor.start(ctx);
356
+ deps.services.queueWatchdog.start(ctx);
364
357
  },
365
- isSessionActive,
366
- );
358
+ async onSessionShutdown(event, ctx) {
359
+ if (!isSessionActive(ctx)) return;
360
+ await deps.services.delivery.onSessionShutdown();
361
+ if (!isSessionActive(ctx)) return;
362
+ deps.services.queueWatchdog.stop();
363
+ deps.services.capabilityMonitor.stop();
364
+ await queueLifecycle.onSessionShutdown(event, ctx);
365
+ if (!isSessionActive(ctx)) return;
366
+ await deps.services.inboundWorker.onSessionShutdown();
367
+ },
368
+ };
367
369
  const followerLifecycle = appendTelegramLifecycleHooks(
368
370
  servicesLifecycle,
369
371
  {
package/lib/menu-queue.ts CHANGED
@@ -73,6 +73,10 @@ function toTelegramQueueMenuItems<Context>(
73
73
  });
74
74
  }
75
75
 
76
+ function formatSkippedTelegramQueuePosition(position: number): string {
77
+ return Array.from(String(position), (char) => `${char}\u0335`).join("");
78
+ }
79
+
76
80
  function buildTelegramQueueMenuReplyMarkup(
77
81
  items: readonly TelegramQueueMenuItem[],
78
82
  emptyRefreshIndex = 0,
@@ -86,7 +90,7 @@ function buildTelegramQueueMenuReplyMarkup(
86
90
  : "queue:refresh";
87
91
  const refreshRow = [{ text: "🌀 Refresh", callback_data: refreshData }];
88
92
  if (items.length === 0) return { inline_keyboard: [backRow, refreshRow] };
89
- const rows = items.map((item, index) => {
93
+ const rows = items.map((item) => {
90
94
  const prefix = item.reactionSuppressionEmoji
91
95
  ? `${item.reactionSuppressionEmoji} `
92
96
  : item.isPriority
@@ -94,7 +98,11 @@ function buildTelegramQueueMenuReplyMarkup(
94
98
  : item.hasAttachments
95
99
  ? "📎 "
96
100
  : "";
97
- const label = `${index + 1}. ${prefix}${item.statusSummary}`;
101
+ const position = item.reactionSuppressionEmoji
102
+ ? formatSkippedTelegramQueuePosition(item.queuePosition)
103
+ : String(item.queuePosition);
104
+ const ordinalSeparator = item.reactionSuppressionEmoji ? "\u200A" : "";
105
+ const label = `${position}${ordinalSeparator}. ${prefix}${item.statusSummary}`;
98
106
  return [
99
107
  {
100
108
  text: label,
@@ -162,7 +170,10 @@ function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
162
170
  : item.isPriority
163
171
  ? ` ${item.priorityEmoji ?? "⚡"}`
164
172
  : "";
165
- const heading = `<b>${item.queuePosition}.</b>${badge}`;
173
+ const position = item.reactionSuppressionEmoji
174
+ ? `<s>${item.queuePosition}</s>.`
175
+ : `<b>${item.queuePosition}.</b>`;
176
+ const heading = `${position}${badge}`;
166
177
  const preview = `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
167
178
  return `${heading}\n${preview}`;
168
179
  }
@@ -182,7 +193,7 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
182
193
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:priority`,
183
194
  },
184
195
  {
185
- text: isPriority ? "⚫️ Normal" : "🟣 Normal",
196
+ text: isPriority ? "⚫️ Normal" : "🔵 Normal",
186
197
  callback_data: `queue:prio-set:${chatId}:${replyToMessageId}:normal`,
187
198
  },
188
199
  ],
@@ -149,9 +149,7 @@ export const TIME_INJECTION_MODE_SETTINGS_TITLE =
149
149
  "<b>🕒 Time injection mode:</b>";
150
150
  export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
151
151
 
152
- type TelegramVoiceReplyModeSetting = TelegramVoiceReplyMode | "hidden";
153
-
154
- function getVoiceReplyModeLabel(mode: TelegramVoiceReplyModeSetting): string {
152
+ function getVoiceReplyModeLabel(mode: TelegramVoiceReplyMode): string {
155
153
  return mode;
156
154
  }
157
155
 
@@ -162,8 +160,8 @@ function getTelegramSettingsStateValueLabel(value: string): string {
162
160
  function getVoiceReplyModeSetting(
163
161
  mode: TelegramVoiceReplyMode,
164
162
  configured: boolean,
165
- ): TelegramVoiceReplyModeSetting {
166
- return configured ? mode : "hidden";
163
+ ): TelegramVoiceReplyMode {
164
+ return configured ? mode : "manual";
167
165
  }
168
166
 
169
167
  export function buildTelegramSettingsMenuText(): string {
@@ -246,8 +244,8 @@ export function buildVoiceReplyModeSettingsText(
246
244
  "",
247
245
  "Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
248
246
  "",
249
- "<code>-</code> <code>hidden</code> (default): add no automatic voice context; explicit 'telegram_voice' actions still work.",
250
- "<code>-</code> <code>mirror</code>: voice input activates automatic voice delivery; text input follows 'hidden' behavior.",
247
+ "<code>-</code> <code>manual</code> (default): add no automatic voice context; explicit 'telegram_voice' actions still work.",
248
+ "<code>-</code> <code>mirror</code>: voice input activates automatic voice delivery; text input follows 'manual' behavior.",
251
249
  "<code>-</code> <code>always</code>: activate automatic voice delivery for every reply.",
252
250
  ].join("\n");
253
251
  }
@@ -502,7 +500,7 @@ export function buildVoiceReplyModeSettingsReplyMarkup(
502
500
  configured = true,
503
501
  ): TelegramSettingsMenuReplyMarkup {
504
502
  const activeMode = getVoiceReplyModeSetting(mode, configured);
505
- const modes: TelegramVoiceReplyModeSetting[] = ["hidden", "mirror", "always"];
503
+ const modes: TelegramVoiceReplyMode[] = ["manual", "mirror", "always"];
506
504
  return {
507
505
  inline_keyboard: [
508
506
  [{ text: "⬆️ Back", callback_data: "settings:list" }],
@@ -661,12 +659,18 @@ export async function handleTelegramSettingsMenuCallbackAction(
661
659
  }
662
660
  if (data.startsWith("settings:set:voice-reply:")) {
663
661
  const mode = data.slice("settings:set:voice-reply:".length);
664
- if (mode === "hidden" || mode === "mirror" || mode === "always") {
665
- await deps.setVoiceReplyMode(mode === "hidden" ? undefined : mode);
662
+ if (
663
+ mode === "manual" ||
664
+ mode === "hidden" ||
665
+ mode === "mirror" ||
666
+ mode === "always"
667
+ ) {
668
+ const normalizedMode = mode === "hidden" ? "manual" : mode;
669
+ await deps.setVoiceReplyMode(normalizedMode);
666
670
  await updateVoiceReplyModeSettingsMessage(deps);
667
671
  await deps.answerCallbackQuery(
668
672
  callbackQueryId,
669
- `Voice reply mode: ${mode}`,
673
+ `Voice reply mode: ${normalizedMode}`,
670
674
  );
671
675
  return true;
672
676
  }
package/lib/menu.ts CHANGED
@@ -255,7 +255,10 @@ export interface TelegramMenuActionRuntimeDeps<
255
255
  chatId: number,
256
256
  replyToMessageId: number,
257
257
  text: string,
258
- options?: { target?: { chatId: number; threadId?: number } },
258
+ options?: {
259
+ target?: { chatId: number; threadId?: number };
260
+ parseMode?: "HTML";
261
+ },
259
262
  ) => Promise<unknown>;
260
263
  sectionRegistry?: TelegramSectionRegistry;
261
264
  isVoiceReplyActive?: () => boolean;
@@ -798,8 +801,8 @@ export function createTelegramMenuActionRuntime<
798
801
  await deps.sendTextReply(
799
802
  chatId,
800
803
  replyToMessageId,
801
- "Cannot open status while Pi is busy. Send /abort, /next, or /stop.",
802
- { target: { chatId, threadId } },
804
+ "<b>⏳ Cannot open status while Pi is busy. Send /abort, /next, or /stop.</b>",
805
+ { target: { chatId, threadId }, parseMode: "HTML" },
803
806
  );
804
807
  },
805
808
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
@@ -835,16 +838,16 @@ export function createTelegramMenuActionRuntime<
835
838
  await deps.sendTextReply(
836
839
  chatId,
837
840
  replyToMessageId,
838
- "Cannot switch model while Pi is busy. Send /abort, /next, or /stop.",
839
- { target: { chatId, threadId } },
841
+ "<b>⏳ Cannot switch model while Pi is busy. Send /abort, /next, or /stop.</b>",
842
+ { target: { chatId, threadId }, parseMode: "HTML" },
840
843
  );
841
844
  },
842
845
  sendNoModelsMessage: async () => {
843
846
  await deps.sendTextReply(
844
847
  chatId,
845
848
  replyToMessageId,
846
- "No available models with configured auth.",
847
- { target: { chatId, threadId } },
849
+ "<b>🚫 No available models with configured auth.</b>",
850
+ { target: { chatId, threadId }, parseMode: "HTML" },
848
851
  );
849
852
  },
850
853
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
@@ -100,17 +100,6 @@ async function ensureTelegramVoiceFileFormat(
100
100
  );
101
101
  }
102
102
 
103
- function extractVoiceResult(result: any): {
104
- filePath: string;
105
- transcriptText?: string;
106
- } {
107
- if (typeof result === "string") return { filePath: result };
108
- return {
109
- filePath: result.audioPath,
110
- transcriptText: result.transcriptText,
111
- };
112
- }
113
-
114
103
  async function sendVoiceChatAction(
115
104
  deps: TelegramVoiceReplySenderDeps,
116
105
  chatId: number,
@@ -132,7 +121,6 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
132
121
  options?: {
133
122
  replyToPrompt?: boolean;
134
123
  replyMarkup?: unknown;
135
- transcriptText?: string;
136
124
  },
137
125
  ): Promise<void> => {
138
126
  const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
@@ -148,7 +136,6 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
148
136
  "sendVoice",
149
137
  {
150
138
  chat_id: String(turn.chatId),
151
- ...(options?.transcriptText ? { caption: options.transcriptText } : {}),
152
139
  ...(replyParameters ? { reply_parameters: replyParameters } : {}),
153
140
  ...(turn.target
154
141
  ? Object.fromEntries(
@@ -257,13 +244,11 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
257
244
  continue;
258
245
  }
259
246
 
260
- const { filePath, transcriptText } = extractVoiceResult(providerResult);
261
- voiceFilePath = filePath;
262
- originalFilePath = filePath;
263
- await uploadVoiceFile(turn, filePath, {
247
+ voiceFilePath = providerResult;
248
+ originalFilePath = providerResult;
249
+ await uploadVoiceFile(turn, providerResult, {
264
250
  replyToPrompt: options?.replyToPrompt,
265
251
  replyMarkup: options?.replyMarkup,
266
- transcriptText,
267
252
  });
268
253
  return;
269
254
  } catch (error) {
package/lib/prompts.ts CHANGED
@@ -14,11 +14,11 @@ export const TELEGRAM_DISCONNECTED_CONTEXT_MESSAGE =
14
14
 
15
15
  const LOCAL_SYSTEM_PROMPT_SUFFIX = `
16
16
 
17
- ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} Load the \`telegram-bridge\` Skill for Telegram-originated turns or explicit requests involving Telegram delivery, actions, Threaded Mode, or diagnosis. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
17
+ ${TELEGRAM_CONNECTED_CONTEXT_MESSAGE} For Telegram work, consult bundled Skills in routing order: \`telegram-bridge\` for the transport and turn protocol, \`generated-control-surface\` when contextual controls materially shorten feedback, then \`generative-apps\` when the interaction warrants a reusable deterministic app. Load a Skill only if its instructions are not already present in the current context. Do not use Telegram-specific features from unrelated local/TUI prompts.`;
18
18
 
19
19
  const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
20
20
 
21
- Telegram turn note: Load and follow the \`telegram-bridge\` Skill.`;
21
+ Telegram turn note: Follow the applicable bundled Telegram Skills in routing order; load only missing instructions.`;
22
22
 
23
23
  export const TELEGRAM_ATTACH_PROMPT_SNIPPET =
24
24
  "Queue files for the active Telegram reply; outside Telegram turns, send files directly to Telegram.";