@llblab/pi-telegram 0.19.1 → 0.19.2

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/CHANGELOG.md CHANGED
@@ -1,10 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.2: Draft And Rendering Isolation Hotfix
4
+
5
+ - `[Config]` Grouped assistant answer output under `assistant: { rendering, draftPreviews }`, while still reading and cleaning up legacy `assistantRendering`, `draftPreviews`, and `richDraftPreviews`. Impact: config vocabulary now matches the feature boundary; draft visibility and final rendering live together without implying that previews are inherently Rich Markdown.
6
+ - `[Preview]` Hard-gated preview state creation behind the Draft previews setting. Impact: when Draft previews are off, `message_start` / `message_update` cannot create or flush draft frames; Telegram should show only native active status until the final answer.
7
+ - `[Preview]` Aligned enabled draft previews with the selected final renderer: `assistant.rendering: "rich"` uses `sendRichMessageDraft`, while `assistant.rendering: "html"` uses legacy `sendMessageDraft` with HTML. Impact: the visible draft no longer morphs from Native Rich Markdown into legacy HTML at finalization.
8
+ - `[Rendering]` Removed the thread-reply special case that forced anchored thread assistant replies through legacy Markdown-to-HTML. Impact: `assistant.rendering: "rich"` now uses native Rich Markdown for final assistant replies in Threaded Mode too, while `assistant.rendering: "html"` remains the only path that selects legacy HTML rendering.
9
+ - `[Tests]` Updated reply regressions to assert native Rich Markdown delivery for anchored thread replies.
10
+
3
11
  ## 0.19.1: Settings Layer Hotfix
4
12
 
5
13
  - `[Settings]` Split the overloaded Rich Draft setting into two independent controls: `Draft previews` for live `sendRichMessageDraft` streaming and `Assistant rendering` for final-answer delivery mode. Impact: operators can hide/show in-progress drafts without changing how final Markdown is rendered.
6
14
  - `[Rendering]` Added persisted `assistantRendering: "rich" | "html"`, defaulting to Native Rich Markdown and allowing legacy Markdown-to-HTML final assistant replies when selected. Impact: renderer compatibility is explicit instead of being conflated with preview visibility.
7
- - `[Preview]` Kept `richDraftPreviews` as the stored draft-preview flag for compatibility, but renamed the Settings UI to `Draft previews`. Impact: existing configs keep working while the product vocabulary matches the Bot API layer.
15
+ - `[Preview]` Kept `richDraftPreviews` as the stored draft-preview flag for compatibility, but renamed the Settings UI to `Draft previews`. Impact: existing configs keep working while the product vocabulary moves toward the preview feature boundary.
8
16
  - `[Validation]` Updated menu/settings/reply regressions for the two-axis configuration model.
9
17
 
10
18
  ## 0.19.0: Telegram Companion Hub
package/README.md CHANGED
@@ -202,7 +202,7 @@ Most controls live in Pi commands or the Telegram menu. Environment variables re
202
202
  | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
203
203
  | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
204
204
 
205
- Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistantRendering: "rich"` for final replies, `richDraftPreviews: false` for no live draft text by default, and native Telegram active status for long-running turns.
205
+ Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: false }` for assistant answer output, and native Telegram active status for long-running turns.
206
206
 
207
207
  ## Extension Platform
208
208
 
@@ -223,11 +223,11 @@ During active Telegram-owned turns, assistant message start/update hooks re-arm
223
223
 
224
224
  ### Rendering And Delivery
225
225
 
226
- Rich Markdown is the default model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage` when `assistantRendering` is `rich`, and through the legacy Markdown-to-HTML renderer when `assistantRendering` is `html`; guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering. Streaming previews may use `sendRichMessageDraft` only when Draft previews are enabled and draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
226
+ Rich Markdown is the default model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage` when `assistant.rendering` is `rich`, and through the legacy Markdown-to-HTML renderer when `assistant.rendering` is `html`; guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering. Streaming previews may use `sendRichMessageDraft` only when `assistant.draftPreviews` is enabled and draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
227
227
 
228
228
  Assistant delivery guarantees:
229
229
 
230
- - Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistantRendering: "html"` for compatibility.
230
+ - Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
231
231
  - Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
232
232
  - Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text.
233
233
  - Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.
package/index.ts CHANGED
@@ -478,7 +478,9 @@ export default function (pi: Pi.ExtensionAPI) {
478
478
  recordRuntimeEvent,
479
479
  });
480
480
  const nativeMarkdownDraftSender =
481
- TelegramApi.createTelegramNativeMarkdownDraftSender({
481
+ TelegramApi.createTelegramAssistantDraftSender({
482
+ getAssistantRenderingMode: configControls.getAssistantRenderingMode,
483
+ renderMarkdownToHtmlDraft: Replies.renderTelegramMarkdownToHtmlDraft,
482
484
  sendMessageDraft,
483
485
  sendRichMessageDraft,
484
486
  });
@@ -488,7 +490,7 @@ export default function (pi: Pi.ExtensionAPI) {
488
490
  getMessageText: Replies.getAgentMessageText,
489
491
  getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
490
492
  sendDraft: nativeMarkdownDraftSender,
491
- canSend: configControls.areRichDraftPreviewsEnabled,
493
+ canSend: configControls.areDraftPreviewsEnabled,
492
494
  sendMarkdownReply,
493
495
  recordRuntimeEvent,
494
496
  ...replyTransport,
package/lib/config.ts CHANGED
@@ -59,7 +59,15 @@ export interface TelegramConfig {
59
59
  attachmentHandlers?: TelegramInboundHandlerConfig[];
60
60
  outboundHandlers?: TelegramOutboundHandlerConfig[];
61
61
  proactivePush?: boolean;
62
+ assistant?: {
63
+ draftPreviews?: boolean;
64
+ rendering?: TelegramAssistantRenderingMode;
65
+ };
66
+ /** @deprecated use assistant.draftPreviews */
67
+ draftPreviews?: boolean;
68
+ /** @deprecated use assistant.draftPreviews */
62
69
  richDraftPreviews?: boolean;
70
+ /** @deprecated use assistant.rendering */
63
71
  assistantRendering?: TelegramAssistantRenderingMode;
64
72
  voice?: {
65
73
  replyMode?: "manual" | "mirror" | "always";
@@ -263,18 +271,34 @@ export function createTelegramProactivePushSetter(
263
271
  };
264
272
  }
265
273
 
266
- export function createTelegramRichDraftPreviewsChecker(
274
+ export function createTelegramDraftPreviewsChecker(
267
275
  configStore: Pick<TelegramConfigStore, "get">,
268
276
  ): () => boolean {
269
- return () => configStore.get().richDraftPreviews ?? false;
277
+ return () => {
278
+ const config = configStore.get();
279
+ return (
280
+ config.assistant?.draftPreviews ??
281
+ config.draftPreviews ??
282
+ config.richDraftPreviews ??
283
+ false
284
+ );
285
+ };
270
286
  }
271
287
 
272
- export function createTelegramRichDraftPreviewsSetter(
288
+ export function createTelegramDraftPreviewsSetter(
273
289
  configStore: TelegramMutableConfigStore,
274
290
  ): (enabled: boolean) => Promise<void> {
275
291
  return async (enabled) => {
276
292
  await loadLatestTelegramConfig(configStore);
277
- const config = { ...configStore.get(), richDraftPreviews: enabled };
293
+ const {
294
+ draftPreviews: _legacyDraftPreviews,
295
+ richDraftPreviews: _legacyRichDraftPreviews,
296
+ ...current
297
+ } = configStore.get();
298
+ const config = {
299
+ ...current,
300
+ assistant: { ...current.assistant, draftPreviews: enabled },
301
+ };
278
302
  configStore.set(config);
279
303
  await configStore.persist(config);
280
304
  };
@@ -284,7 +308,8 @@ export function createTelegramAssistantRenderingModeGetter(
284
308
  configStore: Pick<TelegramConfigStore, "get">,
285
309
  ): () => TelegramAssistantRenderingMode {
286
310
  return () => {
287
- const mode = configStore.get().assistantRendering;
311
+ const config = configStore.get();
312
+ const mode = config.assistant?.rendering ?? config.assistantRendering;
288
313
  return mode === "html" ? "html" : "rich";
289
314
  };
290
315
  }
@@ -294,7 +319,12 @@ export function createTelegramAssistantRenderingModeSetter(
294
319
  ): (mode: TelegramAssistantRenderingMode) => Promise<void> {
295
320
  return async (mode) => {
296
321
  await loadLatestTelegramConfig(configStore);
297
- const config = { ...configStore.get(), assistantRendering: mode };
322
+ const { assistantRendering: _legacyAssistantRendering, ...current } =
323
+ configStore.get();
324
+ const config = {
325
+ ...current,
326
+ assistant: { ...current.assistant, rendering: mode },
327
+ };
298
328
  configStore.set(config);
299
329
  await configStore.persist(config);
300
330
  };
@@ -435,10 +465,8 @@ export function createTelegramConfigControls(
435
465
  return {
436
466
  isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
437
467
  setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
438
- areRichDraftPreviewsEnabled:
439
- createTelegramRichDraftPreviewsChecker(configStore),
440
- setRichDraftPreviewsEnabled:
441
- createTelegramRichDraftPreviewsSetter(configStore),
468
+ areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
469
+ setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
442
470
  getAssistantRenderingMode:
443
471
  createTelegramAssistantRenderingModeGetter(configStore),
444
472
  setAssistantRenderingMode:
@@ -21,7 +21,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
21
21
 
22
22
  export interface TelegramSettingsStateDeps {
23
23
  isProactivePushEnabled: () => boolean;
24
- areRichDraftPreviewsEnabled: () => boolean;
24
+ areDraftPreviewsEnabled: () => boolean;
25
25
  getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
26
26
  getTimeInjectionMode: () => TelegramTimeMode;
27
27
  getVoiceReplyMode: () => TelegramVoiceReplyMode;
@@ -30,7 +30,7 @@ export interface TelegramSettingsStateDeps {
30
30
 
31
31
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
32
32
  setProactivePushEnabled: (enabled: boolean) => Promise<void>;
33
- setRichDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
33
+ setDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
34
34
  setAssistantRenderingMode: (
35
35
  mode: TelegramAssistantRenderingMode,
36
36
  ) => Promise<void>;
@@ -311,7 +311,7 @@ export async function openTelegramSettingsMenu<
311
311
  buildTelegramSettingsMenuText(),
312
312
  buildTelegramSettingsMenuReplyMarkup(
313
313
  deps.isProactivePushEnabled(),
314
- deps.areRichDraftPreviewsEnabled(),
314
+ deps.areDraftPreviewsEnabled(),
315
315
  deps.getAssistantRenderingMode(),
316
316
  deps.getVoiceReplyMode(),
317
317
  deps.getTimeInjectionMode(),
@@ -431,7 +431,7 @@ export async function updateTelegramSettingsMenuMessage(
431
431
  buildTelegramSettingsMenuText(),
432
432
  buildTelegramSettingsMenuReplyMarkup(
433
433
  deps.isProactivePushEnabled(),
434
- deps.areRichDraftPreviewsEnabled(),
434
+ deps.areDraftPreviewsEnabled(),
435
435
  deps.getAssistantRenderingMode(),
436
436
  deps.getVoiceReplyMode(),
437
437
  deps.getTimeInjectionMode(),
@@ -454,7 +454,7 @@ export async function updateProactivePushSettingsMessage(
454
454
  export async function updateDraftPreviewsSettingsMessage(
455
455
  deps: TelegramSettingsMenuCallbackDeps,
456
456
  ): Promise<void> {
457
- const enabled = deps.areRichDraftPreviewsEnabled();
457
+ const enabled = deps.areDraftPreviewsEnabled();
458
458
  await deps.updateSettingsMessage(
459
459
  buildDraftPreviewsSettingsText(enabled),
460
460
  buildDraftPreviewsSettingsReplyMarkup(enabled),
@@ -577,7 +577,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
577
577
  data === "settings:set:rich-drafts:off"
578
578
  ) {
579
579
  const enabled = data.endsWith(":on");
580
- await deps.setRichDraftPreviewsEnabled(enabled);
580
+ await deps.setDraftPreviewsEnabled(enabled);
581
581
  await updateDraftPreviewsSettingsMessage(deps);
582
582
  await deps.answerCallbackQuery(
583
583
  callbackQueryId,
@@ -624,7 +624,7 @@ export function createTelegramSettingsMenuRuntime<
624
624
  {
625
625
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
626
626
  isProactivePushEnabled: deps.isProactivePushEnabled,
627
- areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
627
+ areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
628
628
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
629
629
  getVoiceReplyMode: deps.getVoiceReplyMode,
630
630
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
@@ -644,7 +644,7 @@ export function createTelegramSettingsMenuRuntime<
644
644
  updateTelegramSettingsMenuMessage(
645
645
  {
646
646
  isProactivePushEnabled: deps.isProactivePushEnabled,
647
- areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
647
+ areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
648
648
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
649
649
  getVoiceReplyMode: deps.getVoiceReplyMode,
650
650
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
@@ -691,7 +691,7 @@ export function createTelegramSettingsMenuRuntime<
691
691
  query.data === "settings:set:rich-drafts:off"
692
692
  ) {
693
693
  const enabled = query.data.endsWith(":on");
694
- await deps.setRichDraftPreviewsEnabled(enabled);
694
+ await deps.setDraftPreviewsEnabled(enabled);
695
695
  await deps.answerCallbackQuery(
696
696
  query.id,
697
697
  `Draft previews ${enabled ? "enabled" : "disabled"}`,
@@ -736,13 +736,13 @@ export function createTelegramSettingsMenuRuntime<
736
736
  }
737
737
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
738
738
  isProactivePushEnabled: deps.isProactivePushEnabled,
739
- areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
739
+ areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
740
740
  getAssistantRenderingMode: deps.getAssistantRenderingMode,
741
741
  getVoiceReplyMode: deps.getVoiceReplyMode,
742
742
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
743
743
  getTimeInjectionMode: deps.getTimeInjectionMode,
744
744
  setProactivePushEnabled: deps.setProactivePushEnabled,
745
- setRichDraftPreviewsEnabled: deps.setRichDraftPreviewsEnabled,
745
+ setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
746
746
  setAssistantRenderingMode: deps.setAssistantRenderingMode,
747
747
  setVoiceReplyMode: deps.setVoiceReplyMode,
748
748
  setTimeInjectionMode: deps.setTimeInjectionMode,
package/lib/preview.ts CHANGED
@@ -73,6 +73,7 @@ export interface TelegramAssistantMessagePreviewStartDeps<
73
73
  getState: () => TelegramPreviewRuntimeState | undefined;
74
74
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
75
75
  createPreviewState: () => TelegramPreviewRuntimeState;
76
+ canSend?: () => boolean;
76
77
  finalizePreview: (chatId: number) => Promise<boolean>;
77
78
  finalizeMarkdownPreview: (
78
79
  chatId: number,
@@ -88,6 +89,7 @@ export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
88
89
  getState: () => TelegramPreviewRuntimeState | undefined;
89
90
  setState: (state: TelegramPreviewRuntimeState | undefined) => void;
90
91
  createPreviewState: () => TelegramPreviewRuntimeState;
92
+ canSend?: () => boolean;
91
93
  getMessageText: (message: TMessage) => string;
92
94
  schedulePreviewFlush: (
93
95
  chatId: number,
@@ -260,6 +262,7 @@ export function createTelegramAssistantPreviewRuntime<
260
262
  getState: controller.getState,
261
263
  setState: controller.setState,
262
264
  createPreviewState: controller.createState,
265
+ canSend: deps.canSend,
263
266
  finalizePreview: controller.finalize,
264
267
  finalizeMarkdownPreview,
265
268
  getMessageText: deps.getMessageText,
@@ -349,6 +352,10 @@ export async function handleTelegramAssistantMessagePreviewStart<
349
352
  ): Promise<void> {
350
353
  const turn = deps.getActiveTurn();
351
354
  if (!turn || !deps.isAssistantMessage(message)) return;
355
+ if (deps.canSend && !deps.canSend()) {
356
+ deps.setState(undefined);
357
+ return;
358
+ }
352
359
  if (shouldSuppressPreviewForVoice(turn)) {
353
360
  deps.setState(undefined);
354
361
  return;
@@ -382,6 +389,10 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
382
389
  ): Promise<void> {
383
390
  const turn = deps.getActiveTurn();
384
391
  if (!turn || !deps.isAssistantMessage(message)) return;
392
+ if (deps.canSend && !deps.canSend()) {
393
+ deps.setState(undefined);
394
+ return;
395
+ }
385
396
  if (shouldSuppressPreviewForVoice(turn)) return;
386
397
  let state = deps.getState();
387
398
  if (!state) {
package/lib/replies.ts CHANGED
@@ -27,6 +27,12 @@ export {
27
27
  type TelegramRenderMode,
28
28
  };
29
29
 
30
+ export function renderTelegramMarkdownToHtmlDraft(markdown: string): string {
31
+ return renderTelegramMessage(markdown, { mode: "markdown" })
32
+ .map((chunk) => chunk.text)
33
+ .join("\n");
34
+ }
35
+
30
36
  export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
31
37
  export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
32
38
 
@@ -783,12 +789,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
783
789
  },
784
790
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
785
791
  const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
786
- if (
787
- renderingMode === "html" ||
788
- (typeof options?.target?.threadId === "number" &&
789
- replyToMessageId !== undefined &&
790
- replyToMessageId > 0)
791
- ) {
792
+ if (renderingMode === "html") {
792
793
  return deps.replyTransport.sendRenderedChunks(
793
794
  chatId,
794
795
  deps.renderTelegramMessage(markdown, { mode: "markdown" }),
@@ -1112,6 +1112,24 @@ export function createTelegramNativeMarkdownDraftSender(deps: {
1112
1112
  };
1113
1113
  }
1114
1114
 
1115
+ export function createTelegramAssistantDraftSender(deps: {
1116
+ getAssistantRenderingMode: () => "rich" | "html";
1117
+ renderMarkdownToHtmlDraft: (markdown: string) => string;
1118
+ sendMessageDraft: TelegramBridgeApiRuntime["sendMessageDraft"];
1119
+ sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
1120
+ }): TelegramBridgeApiRuntime["sendMessageDraft"] {
1121
+ const sendNativeDraft = createTelegramNativeMarkdownDraftSender(deps);
1122
+ return (chatId, draftId, text, options) => {
1123
+ if (text === undefined || deps.getAssistantRenderingMode() === "rich") {
1124
+ return sendNativeDraft(chatId, draftId, text, options);
1125
+ }
1126
+ return deps.sendMessageDraft(chatId, draftId, deps.renderMarkdownToHtmlDraft(text), {
1127
+ ...options,
1128
+ parse_mode: "HTML",
1129
+ });
1130
+ };
1131
+ }
1132
+
1115
1133
  export function createDefaultTelegramBridgeApiRuntime(deps: {
1116
1134
  getBotToken: () => string | undefined;
1117
1135
  recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.19.1",
3
+ "version": "0.19.2",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"