@llblab/pi-telegram 0.16.6 → 0.17.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.
package/lib/preview.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Telegram preview streaming helpers
3
- * Zones: telegram outbound, streaming preview, rendering
3
+ * Zones: telegram outbound, native rich markdown drafts, fallback preview transport
4
4
  * Owns preview transport selection, runtime updates, and preview finalization
5
5
  */
6
6
 
@@ -11,22 +11,15 @@ import type {
11
11
  TelegramSentMessage,
12
12
  } from "./telegram-api.ts";
13
13
  import {
14
- buildTelegramPreviewSnapshot,
15
- MAX_MESSAGE_LENGTH,
16
- renderMarkdownPreviewText,
17
- renderTelegramMessage,
18
- type TelegramPreviewRenderStrategy,
19
- type TelegramPreviewSnapshot,
20
- type TelegramRenderedChunk,
21
- type TelegramRenderMode,
22
- } from "./rendering.ts";
23
-
24
- import { buildTelegramReplyParameters } from "./replies.ts";
14
+ buildTelegramReplyParameters,
15
+ normalizeTelegramNativeMarkdown,
16
+ } from "./replies.ts";
25
17
  import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
26
18
  import { shouldSuppressPreviewForVoice } from "./voice.ts";
27
19
 
28
- const TELEGRAM_PREVIEW_THROTTLE_MS = 750;
20
+ const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
29
21
  const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
22
+ const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
30
23
 
31
24
  export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
32
25
 
@@ -37,7 +30,6 @@ export interface TelegramPreviewState {
37
30
  pendingText: string;
38
31
  lastSentText: string;
39
32
  lastSentParseMode?: "HTML";
40
- lastSentStrategy?: TelegramPreviewRenderStrategy;
41
33
  }
42
34
 
43
35
  export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
@@ -56,7 +48,6 @@ export interface TelegramPreviewRuntimeDeps<
56
48
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
57
49
  clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
58
50
  maxMessageLength: number;
59
- renderPreviewText: (markdown: string) => string;
60
51
  getDraftSupport: () => TelegramDraftSupport;
61
52
  setDraftSupport: (support: TelegramDraftSupport) => void;
62
53
  allocateDraftId: () => number;
@@ -81,21 +72,6 @@ export interface TelegramPreviewRuntimeDeps<
81
72
  text: string,
82
73
  options?: { parseMode?: "HTML" },
83
74
  ) => Promise<unknown>;
84
- renderTelegramMessage: (
85
- text: string,
86
- options?: { mode?: TelegramRenderMode },
87
- ) => TelegramRenderedChunk[];
88
- sendRenderedChunks: (
89
- chatId: number,
90
- chunks: TelegramRenderedChunk[],
91
- options?: { replyMarkup?: TReplyMarkup },
92
- ) => Promise<number | undefined>;
93
- editRenderedMessage: (
94
- chatId: number,
95
- messageId: number,
96
- chunks: TelegramRenderedChunk[],
97
- options?: { replyMarkup?: TReplyMarkup },
98
- ) => Promise<number | undefined>;
99
75
  canSend?: () => boolean;
100
76
  recordRuntimeEvent?: (
101
77
  category: string,
@@ -162,7 +138,6 @@ export interface TelegramPreviewControllerDeps<
162
138
  > {
163
139
  getDefaultReplyToMessageId?: () => number | undefined;
164
140
  maxMessageLength?: number;
165
- renderPreviewText?: (markdown: string) => string;
166
141
  initialDraftSupport?: TelegramDraftSupport;
167
142
  sendDraft: (
168
143
  chatId: number,
@@ -186,22 +161,6 @@ export interface TelegramPreviewControllerDeps<
186
161
  text: string,
187
162
  options?: { parseMode?: "HTML" },
188
163
  ) => Promise<unknown>;
189
- renderTelegramMessage?: (
190
- text: string,
191
- options?: { mode?: TelegramRenderMode },
192
- ) => TelegramRenderedChunk[];
193
- sendRenderedChunks: (
194
- chatId: number,
195
- chunks: TelegramRenderedChunk[],
196
- replyToMessageId: number | undefined,
197
- options?: { replyMarkup?: TReplyMarkup },
198
- ) => Promise<number | undefined>;
199
- editRenderedMessage: (
200
- chatId: number,
201
- messageId: number,
202
- chunks: TelegramRenderedChunk[],
203
- options?: { replyMarkup?: TReplyMarkup },
204
- ) => Promise<number | undefined>;
205
164
  canSend?: () => boolean;
206
165
  throttleMs?: number;
207
166
  maxDraftId?: number;
@@ -229,12 +188,6 @@ export interface TelegramPreviewController<
229
188
  flush: (chatId: number) => Promise<void>;
230
189
  scheduleFlush: (chatId: number) => void;
231
190
  finalize: (chatId: number, replyToMessageId?: number) => Promise<boolean>;
232
- finalizeMarkdown: (
233
- chatId: number,
234
- markdown: string,
235
- replyToMessageId?: number,
236
- options?: { replyMarkup?: TReplyMarkup },
237
- ) => Promise<boolean>;
238
191
  }
239
192
 
240
193
  export interface TelegramPreviewMessageTransportDeps {
@@ -271,55 +224,13 @@ export function createTelegramPreviewMessageTransport(
271
224
  };
272
225
  }
273
226
 
274
- export interface TelegramPreviewRenderedChunkTransportDeps<
275
- TReplyMarkup = TelegramPreviewReplyMarkup,
276
- > {
277
- sendRenderedChunks: (
278
- chatId: number,
279
- chunks: TelegramRenderedChunk[],
280
- options?: {
281
- replyToMessageId?: number;
282
- replyMarkup?: TReplyMarkup;
283
- },
284
- ) => Promise<number | undefined>;
285
- editRenderedMessage: (
286
- chatId: number,
287
- messageId: number,
288
- chunks: TelegramRenderedChunk[],
289
- options?: { replyMarkup?: TReplyMarkup },
290
- ) => Promise<number | undefined>;
291
- }
292
-
293
- export function createTelegramPreviewRenderedChunkTransport<
294
- TReplyMarkup = TelegramPreviewReplyMarkup,
295
- >(
296
- deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
297
- ): Pick<
298
- TelegramPreviewControllerDeps<TReplyMarkup>,
299
- "sendRenderedChunks" | "editRenderedMessage"
300
- > {
301
- return {
302
- sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
303
- deps.sendRenderedChunks(chatId, chunks, {
304
- replyToMessageId,
305
- ...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
306
- }),
307
- editRenderedMessage: (chatId, messageId, chunks, options) =>
308
- deps.editRenderedMessage(chatId, messageId, chunks, options),
309
- };
310
- }
311
-
312
227
  export type TelegramPreviewControllerRuntimeDeps<
313
228
  TReplyMarkup = TelegramPreviewReplyMarkup,
314
229
  > = Omit<
315
230
  TelegramPreviewControllerDeps<TReplyMarkup>,
316
- | "sendMessage"
317
- | "editMessageText"
318
- | "sendRenderedChunks"
319
- | "editRenderedMessage"
231
+ "sendMessage" | "editMessageText"
320
232
  > &
321
- TelegramPreviewMessageTransportDeps &
322
- TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
233
+ TelegramPreviewMessageTransportDeps;
323
234
 
324
235
  export function createTelegramPreviewControllerRuntime<
325
236
  TReplyMarkup = TelegramPreviewReplyMarkup,
@@ -329,7 +240,6 @@ export function createTelegramPreviewControllerRuntime<
329
240
  return createTelegramPreviewController({
330
241
  getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
331
242
  maxMessageLength: deps.maxMessageLength,
332
- renderPreviewText: deps.renderPreviewText,
333
243
  initialDraftSupport: deps.initialDraftSupport,
334
244
  sendDraft: deps.sendDraft,
335
245
  ...createTelegramPreviewMessageTransport({
@@ -337,11 +247,6 @@ export function createTelegramPreviewControllerRuntime<
337
247
  editMessageText: deps.editMessageText,
338
248
  buildReplyParameters: deps.buildReplyParameters,
339
249
  }),
340
- renderTelegramMessage: deps.renderTelegramMessage,
341
- ...createTelegramPreviewRenderedChunkTransport({
342
- sendRenderedChunks: deps.sendRenderedChunks,
343
- editRenderedMessage: deps.editRenderedMessage,
344
- }),
345
250
  throttleMs: deps.throttleMs,
346
251
  maxDraftId: deps.maxDraftId,
347
252
  setTimer: deps.setTimer,
@@ -357,13 +262,94 @@ export interface TelegramAssistantPreviewRuntimeDeps<
357
262
  getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
358
263
  isAssistantMessage: (message: TMessage) => boolean;
359
264
  getMessageText: (message: TMessage) => string;
265
+ sendMarkdownReply: (
266
+ chatId: number,
267
+ replyToMessageId: number | undefined,
268
+ markdown: string,
269
+ options?: { replyMarkup?: TReplyMarkup },
270
+ ) => Promise<number | undefined>;
271
+ editMarkdownMessage?: (
272
+ chatId: number,
273
+ messageId: number,
274
+ markdown: string,
275
+ options?: { replyMarkup?: TReplyMarkup },
276
+ ) => Promise<unknown>;
360
277
  }
361
278
 
362
279
  export type TelegramAssistantPreviewRuntime<
363
280
  TMessage,
364
281
  TReplyMarkup = TelegramPreviewReplyMarkup,
365
282
  > = TelegramPreviewController<TReplyMarkup> &
366
- TelegramAssistantMessagePreviewHooks<TMessage>;
283
+ TelegramAssistantMessagePreviewHooks<TMessage> & {
284
+ finalizeMarkdown: (
285
+ chatId: number,
286
+ markdown: string,
287
+ replyToMessageId?: number,
288
+ options?: { replyMarkup?: TReplyMarkup },
289
+ ) => Promise<boolean>;
290
+ };
291
+
292
+ export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
293
+ editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
294
+ }): (
295
+ chatId: number,
296
+ messageId: number,
297
+ markdown: string,
298
+ options?: { replyMarkup?: TReplyMarkup },
299
+ ) => Promise<unknown> {
300
+ return (chatId, messageId, markdown, options) =>
301
+ deps.editMessageText({
302
+ chat_id: chatId,
303
+ message_id: messageId,
304
+ rich_message: {
305
+ markdown: normalizeTelegramNativeMarkdown(markdown),
306
+ skip_entity_detection: true,
307
+ },
308
+ ...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
309
+ });
310
+ }
311
+
312
+ export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
313
+ getState: () => TelegramPreviewRuntimeState | undefined;
314
+ clear: (chatId: number) => Promise<void>;
315
+ discard?: () => void;
316
+ sendMarkdownReply: (
317
+ chatId: number,
318
+ replyToMessageId: number | undefined,
319
+ markdown: string,
320
+ options?: { replyMarkup?: TReplyMarkup },
321
+ ) => Promise<number | undefined>;
322
+ editMarkdownMessage?: (
323
+ chatId: number,
324
+ messageId: number,
325
+ markdown: string,
326
+ options?: { replyMarkup?: TReplyMarkup },
327
+ ) => Promise<unknown>;
328
+ }): (
329
+ chatId: number,
330
+ markdown: string,
331
+ replyToMessageId?: number,
332
+ options?: { replyMarkup?: TReplyMarkup },
333
+ ) => Promise<boolean> {
334
+ return async (chatId, markdown, replyToMessageId, options) => {
335
+ const state = deps.getState();
336
+ if (state?.mode === "message" && state.messageId !== undefined) {
337
+ try {
338
+ await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
339
+ await deps.clear(chatId);
340
+ return true;
341
+ } catch {
342
+ // Fall through to a fresh native Rich Message so final text is not lost.
343
+ }
344
+ }
345
+ if (state?.flushPromise) {
346
+ await state.flushPromise.catch(() => {});
347
+ }
348
+ await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
349
+ deps.discard?.();
350
+ return true;
351
+ };
352
+ }
367
353
 
368
354
  export function createTelegramAssistantPreviewRuntime<
369
355
  TMessage,
@@ -372,8 +358,16 @@ export function createTelegramAssistantPreviewRuntime<
372
358
  deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
373
359
  ): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
374
360
  const controller = createTelegramPreviewControllerRuntime(deps);
361
+ const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
362
+ getState: controller.getState,
363
+ clear: controller.clear,
364
+ discard: () => controller.setState(undefined),
365
+ sendMarkdownReply: deps.sendMarkdownReply,
366
+ editMarkdownMessage: deps.editMarkdownMessage,
367
+ });
375
368
  return {
376
369
  ...controller,
370
+ finalizeMarkdown: finalizeMarkdownPreview,
377
371
  ...createTelegramAssistantMessagePreviewHooks({
378
372
  getActiveTurn: deps.getActiveTurn,
379
373
  isAssistantMessage: deps.isAssistantMessage,
@@ -381,7 +375,7 @@ export function createTelegramAssistantPreviewRuntime<
381
375
  setState: controller.setState,
382
376
  createPreviewState: controller.createState,
383
377
  finalizePreview: controller.finalize,
384
- finalizeMarkdownPreview: controller.finalizeMarkdown,
378
+ finalizeMarkdownPreview,
385
379
  getMessageText: deps.getMessageText,
386
380
  schedulePreviewFlush: controller.scheduleFlush,
387
381
  }),
@@ -401,9 +395,8 @@ export function createTelegramPreviewController<
401
395
  setTimeout(callback, ms));
402
396
  const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
403
397
  const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
404
- const maxMessageLength = deps.maxMessageLength ?? MAX_MESSAGE_LENGTH;
405
- const renderPreview = deps.renderPreviewText ?? renderMarkdownPreviewText;
406
- const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
398
+ const maxMessageLength =
399
+ deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
407
400
  let draftSupport = deps.initialDraftSupport ?? "unknown";
408
401
  let nextDraftId = 0;
409
402
  const getRuntimeDeps = (
@@ -419,7 +412,6 @@ export function createTelegramPreviewController<
419
412
  nextState.flushTimer = undefined;
420
413
  },
421
414
  maxMessageLength,
422
- renderPreviewText: renderPreview,
423
415
  getDraftSupport: () => draftSupport,
424
416
  setDraftSupport: (support) => {
425
417
  draftSupport = support;
@@ -437,15 +429,6 @@ export function createTelegramPreviewController<
437
429
  replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
438
430
  ),
439
431
  editMessageText: deps.editMessageText,
440
- renderTelegramMessage: renderMessage,
441
- sendRenderedChunks: (chatId, chunks, options) =>
442
- deps.sendRenderedChunks(
443
- chatId,
444
- chunks,
445
- replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
446
- options,
447
- ),
448
- editRenderedMessage: deps.editRenderedMessage,
449
432
  canSend: deps.canSend,
450
433
  recordRuntimeEvent: deps.recordRuntimeEvent,
451
434
  });
@@ -465,6 +448,10 @@ export function createTelegramPreviewController<
465
448
  flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
466
449
  scheduleFlush: (chatId) => {
467
450
  if (!state || state.flushTimer) return;
451
+ if (throttleMs <= 0) {
452
+ void flushTelegramPreview(chatId, getRuntimeDeps());
453
+ return;
454
+ }
468
455
  state.flushTimer = setTimer(() => {
469
456
  void flushTelegramPreview(chatId, getRuntimeDeps());
470
457
  }, throttleMs);
@@ -472,13 +459,6 @@ export function createTelegramPreviewController<
472
459
  },
473
460
  finalize: (chatId, replyToMessageId) =>
474
461
  finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
475
- finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
476
- finalizeTelegramMarkdownPreview(
477
- chatId,
478
- markdown,
479
- getRuntimeDeps(replyToMessageId),
480
- options,
481
- ),
482
462
  };
483
463
  }
484
464
 
@@ -554,10 +534,7 @@ export function buildTelegramPreviewFinalText(
554
534
  ): string | undefined {
555
535
  const finalText = state.pendingText.trim();
556
536
  if (finalText) return finalText;
557
- if (
558
- state.lastSentStrategy === "rich-stable-blocks" ||
559
- state.lastSentParseMode === "HTML"
560
- ) {
537
+ if (state.lastSentParseMode === "HTML") {
561
538
  return undefined;
562
539
  }
563
540
  return state.lastSentText.trim() || undefined;
@@ -580,14 +557,16 @@ export function allocateTelegramDraftId(
580
557
  return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
581
558
  }
582
559
 
560
+ interface TelegramNativeMarkdownPreviewSnapshot {
561
+ text: string;
562
+ sourceText: string;
563
+ }
564
+
583
565
  export function shouldUseTelegramDraftPreview(options: {
584
566
  draftSupport: TelegramDraftSupport;
585
- snapshot?: TelegramPreviewSnapshot;
567
+ snapshot?: TelegramNativeMarkdownPreviewSnapshot;
586
568
  }): boolean {
587
- return (
588
- options.draftSupport !== "unsupported" &&
589
- (options.snapshot === undefined || options.snapshot.strategy === "plain")
590
- );
569
+ return options.draftSupport !== "unsupported";
591
570
  }
592
571
 
593
572
  export async function clearTelegramPreview<
@@ -595,12 +574,43 @@ export async function clearTelegramPreview<
595
574
  >(
596
575
  chatId: number,
597
576
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
577
+ options: { awaitFlush?: boolean } = {},
598
578
  ): Promise<void> {
599
- void chatId;
600
579
  const state = deps.getState();
601
580
  if (!state) return;
602
581
  deps.clearScheduledFlush(state);
582
+ if (state.flushPromise && options.awaitFlush !== false) {
583
+ state.flushRequested = false;
584
+ await state.flushPromise.catch(() => {});
585
+ if (deps.getState() !== state) return;
586
+ }
603
587
  deps.setState(undefined);
588
+ if (state.mode === "draft" && state.draftId !== undefined) {
589
+ try {
590
+ await deps.sendDraft(chatId, state.draftId, undefined);
591
+ } catch (error) {
592
+ deps.recordRuntimeEvent?.("preview", error, {
593
+ phase: "clear-draft",
594
+ chatId,
595
+ draftId: state.draftId,
596
+ });
597
+ }
598
+ }
599
+ }
600
+
601
+ function buildTelegramNativeMarkdownPreviewSnapshot(
602
+ state: TelegramPreviewState,
603
+ maxMessageLength: number,
604
+ ): TelegramNativeMarkdownPreviewSnapshot | undefined {
605
+ const sourceText = state.pendingText.trim();
606
+ if (!sourceText || sourceText === state.lastSentText) return undefined;
607
+ return {
608
+ text:
609
+ sourceText.length > maxMessageLength
610
+ ? sourceText.slice(0, maxMessageLength)
611
+ : sourceText,
612
+ sourceText,
613
+ };
604
614
  }
605
615
 
606
616
  async function performTelegramPreviewFlush<
@@ -611,15 +621,13 @@ async function performTelegramPreviewFlush<
611
621
  deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
612
622
  ): Promise<void> {
613
623
  if (deps.canSend && !deps.canSend()) {
614
- await clearTelegramPreview(chatId, deps);
624
+ await clearTelegramPreview(chatId, deps, { awaitFlush: false });
615
625
  return;
616
626
  }
617
- const snapshot = buildTelegramPreviewSnapshot({
627
+ const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
618
628
  state,
619
- maxMessageLength: deps.maxMessageLength,
620
- renderPreviewText: deps.renderPreviewText,
621
- renderTelegramMessage: deps.renderTelegramMessage,
622
- });
629
+ deps.maxMessageLength,
630
+ );
623
631
  if (!snapshot) return;
624
632
  if (
625
633
  shouldUseTelegramDraftPreview({
@@ -630,35 +638,32 @@ async function performTelegramPreviewFlush<
630
638
  const draftId = state.draftId ?? deps.allocateDraftId();
631
639
  state.draftId = draftId;
632
640
  try {
633
- await deps.sendDraft(chatId, draftId, snapshot.text);
641
+ await deps.sendDraft(
642
+ chatId,
643
+ draftId,
644
+ normalizeTelegramNativeMarkdown(snapshot.sourceText),
645
+ );
634
646
  deps.setDraftSupport("supported");
635
647
  state.mode = "draft";
636
648
  state.lastSentText = snapshot.text;
637
- state.lastSentParseMode = snapshot.parseMode;
638
- state.lastSentStrategy = snapshot.strategy;
649
+ state.lastSentParseMode = undefined;
639
650
  return;
640
651
  } catch {
641
652
  deps.setDraftSupport("unsupported");
642
653
  }
643
654
  }
644
655
  if (state.messageId === undefined) {
645
- const sent = await deps.sendMessage(chatId, snapshot.text, {
646
- parseMode: snapshot.parseMode,
647
- });
656
+ const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
648
657
  state.messageId = sent.message_id;
649
658
  state.mode = "message";
650
659
  state.lastSentText = snapshot.text;
651
- state.lastSentParseMode = snapshot.parseMode;
652
- state.lastSentStrategy = snapshot.strategy;
660
+ state.lastSentParseMode = undefined;
653
661
  return;
654
662
  }
655
- await deps.editMessageText(chatId, state.messageId, snapshot.text, {
656
- parseMode: snapshot.parseMode,
657
- });
663
+ await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
658
664
  state.mode = "message";
659
665
  state.lastSentText = snapshot.text;
660
- state.lastSentParseMode = snapshot.parseMode;
661
- state.lastSentStrategy = snapshot.strategy;
666
+ state.lastSentParseMode = undefined;
662
667
  }
663
668
 
664
669
  export async function flushTelegramPreview<
@@ -719,49 +724,9 @@ export async function finalizeTelegramPreview<
719
724
  }
720
725
  if (state.mode === "draft") {
721
726
  await deps.sendMessage(chatId, finalText);
722
- await clearTelegramPreview(chatId, deps);
727
+ deps.setState(undefined);
723
728
  return true;
724
729
  }
725
730
  deps.setState(undefined);
726
731
  return state.messageId !== undefined;
727
732
  }
728
-
729
- export async function finalizeTelegramMarkdownPreview<
730
- TReplyMarkup = TelegramPreviewReplyMarkup,
731
- >(
732
- chatId: number,
733
- markdown: string,
734
- deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
735
- options?: { replyMarkup?: TReplyMarkup },
736
- ): Promise<boolean> {
737
- const state = deps.getState();
738
- if (!state) return false;
739
- if (deps.canSend && !deps.canSend()) {
740
- await clearTelegramPreview(chatId, deps);
741
- return false;
742
- }
743
- await flushTelegramPreview(chatId, deps);
744
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
745
- if (chunks.length === 0) {
746
- await clearTelegramPreview(chatId, deps);
747
- return false;
748
- }
749
- try {
750
- if (state.mode === "draft") {
751
- await deps.sendRenderedChunks(chatId, chunks, options);
752
- await clearTelegramPreview(chatId, deps);
753
- return true;
754
- }
755
- if (state.messageId === undefined) return false;
756
- await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
757
- deps.setState(undefined);
758
- return true;
759
- } catch (error) {
760
- deps.recordRuntimeEvent?.("preview", error, {
761
- phase: "finalize-markdown",
762
- chatId,
763
- messageId: state.messageId,
764
- });
765
- return false;
766
- }
767
- }
package/lib/prompts.ts CHANGED
@@ -28,13 +28,13 @@ Telegram-originated turn context:
28
28
  - Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
29
29
 
30
30
  Telegram-visible output:
31
- - Telegram is often phone-width; keep tables, dense list items, and compact text blocks at or below 37 visible cells when possible.
32
- - Count display width, not raw characters: emoji and some glyphs are wide, so prefer shorter labels when unsure.
33
- - Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
31
+ - Telegram is mobile-first: keep answers easy to scan, use headings/lists when useful, and avoid unnecessarily huge blocks of text.
32
+ - For formulas, use math delimiters like \`$E = mc^2$\` for inline formulas and \`$$\\nE = mc^2\\n$$\` for block formulas; do not wrap formulas in backticks unless they should render as literal code.
33
+ - Use inline code for short copyable literals (commands, paths, IDs, symbols); avoid wide monospace blocks unless structure or literal code requires them.
34
34
  - For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
35
35
 
36
36
  Native outbound actions:
37
- - Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
37
+ - Use normal Rich Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
38
38
  - \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line text, or \`<!-- telegram_voice: Short summary -->\` for one-line text with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
39
39
  - \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
40
40
  - If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.