@llblab/pi-telegram 0.19.0 → 0.19.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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.19.1: Settings Layer Hotfix
4
+
5
+ - `[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
+ - `[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.
8
+ - `[Validation]` Updated menu/settings/reply regressions for the two-axis configuration model.
9
+
3
10
  ## 0.19.0: Telegram Companion Hub
4
11
 
5
12
  - `[Context]` Defined Rich Markdown as the model-answer membrane: complete assistant/guest model replies use native Rich Message delivery, while tool rows, reasoning/thinking blocks, menus, status, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering.
package/README.md CHANGED
@@ -72,41 +72,41 @@ The first Telegram user to message the bot becomes the allowed owner. Other user
72
72
 
73
73
  ## Product Model
74
74
 
75
- | Lens | What `pi-telegram` owns |
76
- | ----------------------- | -------------------------------------------------------------------------------------------------- |
77
- | Operator companion | A phone-width control surface for a live Pi session |
78
- | Runtime adapter | Telegram turns mapped into Pi lifecycle, queueing, previews, final replies, and artifacts |
79
- | Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
80
- | Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
81
- | Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
82
- | Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
75
+ | Lens | What `pi-telegram` owns |
76
+ | --- | --- |
77
+ | Operator companion | A phone-width control surface for a live Pi session |
78
+ | Runtime adapter | Telegram turns mapped into Pi lifecycle, queueing, previews, final replies, and artifacts |
79
+ | Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
80
+ | Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
81
+ | Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
82
+ | Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
83
83
 
84
84
  ## Feature Showcase
85
85
 
86
86
  `pi-telegram` is intentionally broad: it is a Telegram-shaped runtime surface, not only a message relay. This catalogue keeps the practical feature surface visible while detailed contracts stay in `/docs`.
87
87
 
88
- | Surface | What you can do | Why it matters |
89
- | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
90
- | Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
91
- | Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
92
- | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
93
- | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
94
- | Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
95
- | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
96
- | Streaming previews | Show native active status and, when enabled, Rich Markdown drafts while the agent is still working. | The operator can see progress without making draft rendering the baseline. |
97
- | Final Rich Markdown | Deliver complete assistant/guest model replies through Telegram Rich Message APIs with safe splitting. | Meaningful model answers stay readable on narrow mobile screens without forcing generic HTML conversion. |
98
- | Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
99
- | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
100
- | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
101
- | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
102
- | Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
103
- | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
104
- | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
105
- | Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
106
- | Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
107
- | Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
108
- | Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and failure evidence. | Debugging lives in the operator surface instead of hidden logs only. |
109
- | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
88
+ | Surface | What you can do | Why it matters |
89
+ | --- | --- | --- |
90
+ | Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
91
+ | Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
92
+ | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
93
+ | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
94
+ | Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
95
+ | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
96
+ | Draft previews | Show native active status by default, or enable Telegram Rich Draft previews while the agent is still working. | The operator can hide/show in-progress text independently from final rendering. |
97
+ | Assistant rendering | Choose Native Rich Markdown or legacy Markdown-to-HTML for final assistant replies. | Renderer compatibility is explicit instead of being conflated with draft previews. |
98
+ | Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
99
+ | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
100
+ | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
101
+ | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
102
+ | Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
103
+ | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
104
+ | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
105
+ | Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
106
+ | Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
107
+ | Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
108
+ | Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and failure evidence. | Debugging lives in the operator surface instead of hidden logs only. |
109
+ | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
110
110
 
111
111
  ## Core Loop
112
112
 
@@ -126,14 +126,14 @@ The bridge keeps Telegram responsive without stealing Pi's runtime model. Queuei
126
126
 
127
127
  Use these in the bot DM.
128
128
 
129
- | Command | Purpose |
130
- | ----------- | ------------------------------------------------------- |
131
- | `/start` | Pair when needed and open the main operator menu |
132
- | `/compact` | Confirm and run session compaction when safe |
133
- | `/next` | Dispatch the next queued turn, aborting first if needed |
134
- | `/continue` | Enqueue a priority continuation prompt |
135
- | `/abort` | Abort the active run while preserving the queue |
136
- | `/stop` | Abort the active run and clear waiting Telegram turns |
129
+ | Command | Purpose |
130
+ | --- | --- |
131
+ | `/start` | Pair when needed and open the main operator menu |
132
+ | `/compact` | Confirm and run session compaction when safe |
133
+ | `/next` | Dispatch the next queued turn, aborting first if needed |
134
+ | `/continue` | Enqueue a priority continuation prompt |
135
+ | `/abort` | Abort the active run while preserving the queue |
136
+ | `/stop` | Abort the active run and clear waiting Telegram turns |
137
137
 
138
138
  Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings` jump into the same menu system.
139
139
 
@@ -141,12 +141,12 @@ Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/que
141
141
 
142
142
  Run these inside Pi.
143
143
 
144
- | Command | Purpose |
145
- | ---------------------- | ------------------------------------------------------------------ |
146
- | `/telegram-setup` | Save or update the bot token |
147
- | `/telegram-connect` | Start polling and acquire Telegram transport ownership |
148
- | `/telegram-disconnect` | Stop polling and release ownership |
149
- | `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
144
+ | Command | Purpose |
145
+ | --- | --- |
146
+ | `/telegram-setup` | Save or update the bot token |
147
+ | `/telegram-connect` | Start polling and acquire Telegram transport ownership |
148
+ | `/telegram-disconnect` | Stop polling and release ownership |
149
+ | `/telegram-status` | Inspect connection, mode, queue, transport, and recent diagnostics |
150
150
 
151
151
  ## Main Surfaces
152
152
 
@@ -160,7 +160,7 @@ Messages sent while Pi is busy become queued turns. Priority lanes support contr
160
160
 
161
161
  ### Native Rich Markdown
162
162
 
163
- Rich Markdown is the model-answer membrane. Complete assistant and guest model replies use Telegram's native Rich Message APIs, while tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces use explicit Telegram HTML/plain rendering. This keeps meaningful model-authored answers visually distinct from bridge-owned operational UI. Streaming previews may use rich-message drafts when enabled; final replies persist through `sendRichMessage`.
163
+ Rich Markdown is the default model-answer membrane. Complete assistant and guest model replies use Telegram's native Rich Message APIs, while tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces use explicit Telegram HTML/plain rendering. This keeps meaningful model-authored answers visually distinct from bridge-owned operational UI. Two Settings controls keep the layers separate: `Draft previews` toggles live `sendRichMessageDraft` frames, while `Assistant rendering` chooses final-answer delivery (`rich` Native Rich Markdown or `html` legacy Markdown-to-HTML).
164
164
 
165
165
  ### Files And Artifacts
166
166
 
@@ -184,25 +184,25 @@ Classic private DM mode is the base product mode. When Telegram private-chat Thr
184
184
  - Unknown threads are preserved and offered explicit reroute/restore choices.
185
185
  - Telegram never launches hidden Pi processes.
186
186
 
187
- | Mode | Best for | Runtime shape |
188
- | ------------- | -------------------------------------------------------- | ----------------------------------------------------------------------------- |
189
- | Classic DM | One live Pi session controlled from one private bot chat | One polling owner, one queue/runtime surface |
190
- | Threaded Mode | Several visible Pi terminals sharing one bot | One leader owns transport; followers route through named private-chat threads |
187
+ | Mode | Best for | Runtime shape |
188
+ | --- | --- | --- |
189
+ | Classic DM | One live Pi session controlled from one private bot chat | One polling owner, one queue/runtime surface |
190
+ | Threaded Mode | Several visible Pi terminals sharing one bot | One leader owns transport; followers route through named private-chat threads |
191
191
 
192
192
  ## Environment Configuration
193
193
 
194
194
  Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
195
195
 
196
- | Area | Variables |
197
- | ------------------------- | ---------------------------------------------------------------------------------------------- |
198
- | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
199
- | HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` |
200
- | Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY`=auto | ipv4 | ipv6 | ipv4-fallback` |
201
- | Agent data root | `PI_CODING_AGENT_DIR` |
202
- | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
203
- | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
196
+ | Area | Variables |
197
+ | --- | --- |
198
+ | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
199
+ | HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` |
200
+ | Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY=auto`, `ipv4`, `ipv6`, or `ipv4-fallback` |
201
+ | Agent data root | `PI_CODING_AGENT_DIR` |
202
+ | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
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`, native Rich Markdown replies, 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`, `assistantRendering: "rich"` for final replies, `richDraftPreviews: false` for no live draft text by default, and native Telegram active status for long-running turns.
206
206
 
207
207
  ## Extension Platform
208
208
 
@@ -223,15 +223,15 @@ 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 model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage`, and 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` when enabled and when 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 `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.
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.
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.
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.
234
- - When enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
234
+ - When Draft previews are enabled, streaming previews pass structurally closed assistant Markdown prefixes through to `sendRichMessageDraft` with ownership checks, voice suppression, and serialized flushes. Unclosed inline spans, links, fenced code, comments, and display-math blocks are held back until a safe boundary exists. Draft failures are recorded and the failing frame is skipped instead of degrading to raw plain-message previews, because partial Markdown can be invalid while the final message remains valid.
235
235
  - Preview flushes are serialized so older edits cannot race newer drafts; final delivery waits for active draft flushes and does not perform a post-final draft-clear call.
236
236
 
237
237
  UI/compat rendering guarantees:
package/index.ts CHANGED
@@ -443,6 +443,7 @@ export default function (pi: Pi.ExtensionAPI) {
443
443
  },
444
444
  sendMessage,
445
445
  sendRichMessage,
446
+ getAssistantRenderingMode: configControls.getAssistantRenderingMode,
446
447
  editMessage: editTelegramMessageText,
447
448
  });
448
449
  const { replyTransport, editInteractiveMessage, sendInteractiveMessage } =
package/lib/config.ts CHANGED
@@ -47,6 +47,8 @@ export interface ResolvedTelegramTimeConfig {
47
47
  timezone: string;
48
48
  }
49
49
 
50
+ export type TelegramAssistantRenderingMode = "rich" | "html";
51
+
50
52
  export interface TelegramConfig {
51
53
  botToken?: string;
52
54
  botUsername?: string;
@@ -58,6 +60,7 @@ export interface TelegramConfig {
58
60
  outboundHandlers?: TelegramOutboundHandlerConfig[];
59
61
  proactivePush?: boolean;
60
62
  richDraftPreviews?: boolean;
63
+ assistantRendering?: TelegramAssistantRenderingMode;
61
64
  voice?: {
62
65
  replyMode?: "manual" | "mirror" | "always";
63
66
  /** Whether to attach the provider's transcriptText as caption on voice messages */
@@ -277,6 +280,26 @@ export function createTelegramRichDraftPreviewsSetter(
277
280
  };
278
281
  }
279
282
 
283
+ export function createTelegramAssistantRenderingModeGetter(
284
+ configStore: Pick<TelegramConfigStore, "get">,
285
+ ): () => TelegramAssistantRenderingMode {
286
+ return () => {
287
+ const mode = configStore.get().assistantRendering;
288
+ return mode === "html" ? "html" : "rich";
289
+ };
290
+ }
291
+
292
+ export function createTelegramAssistantRenderingModeSetter(
293
+ configStore: TelegramMutableConfigStore,
294
+ ): (mode: TelegramAssistantRenderingMode) => Promise<void> {
295
+ return async (mode) => {
296
+ await loadLatestTelegramConfig(configStore);
297
+ const config = { ...configStore.get(), assistantRendering: mode };
298
+ configStore.set(config);
299
+ await configStore.persist(config);
300
+ };
301
+ }
302
+
280
303
  export function createTelegramVoiceReplyModeGetter(
281
304
  configStore: Pick<TelegramConfigStore, "get">,
282
305
  ): () => "manual" | "mirror" | "always" {
@@ -416,6 +439,10 @@ export function createTelegramConfigControls(
416
439
  createTelegramRichDraftPreviewsChecker(configStore),
417
440
  setRichDraftPreviewsEnabled:
418
441
  createTelegramRichDraftPreviewsSetter(configStore),
442
+ getAssistantRenderingMode:
443
+ createTelegramAssistantRenderingModeGetter(configStore),
444
+ setAssistantRenderingMode:
445
+ createTelegramAssistantRenderingModeSetter(configStore),
419
446
  getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
420
447
  isVoiceReplyModeConfigured:
421
448
  createTelegramVoiceReplyModeConfiguredChecker(configStore),
@@ -8,7 +8,10 @@ import {
8
8
  getTelegramExtensionSettingsRows,
9
9
  type TelegramSectionRegistry,
10
10
  } from "./sections.ts";
11
- import type { TelegramTimeMode } from "./config.ts";
11
+ import type {
12
+ TelegramAssistantRenderingMode,
13
+ TelegramTimeMode,
14
+ } from "./config.ts";
12
15
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
13
16
  import type { TelegramModelMenuState } from "./menu-model.ts";
14
17
  import type { MenuModel } from "./model.ts";
@@ -19,6 +22,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
19
22
  export interface TelegramSettingsStateDeps {
20
23
  isProactivePushEnabled: () => boolean;
21
24
  areRichDraftPreviewsEnabled: () => boolean;
25
+ getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
22
26
  getTimeInjectionMode: () => TelegramTimeMode;
23
27
  getVoiceReplyMode: () => TelegramVoiceReplyMode;
24
28
  isVoiceReplyModeConfigured: () => boolean;
@@ -27,6 +31,9 @@ export interface TelegramSettingsStateDeps {
27
31
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
28
32
  setProactivePushEnabled: (enabled: boolean) => Promise<void>;
29
33
  setRichDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
34
+ setAssistantRenderingMode: (
35
+ mode: TelegramAssistantRenderingMode,
36
+ ) => Promise<void>;
30
37
  setVoiceReplyMode: (
31
38
  mode: TelegramVoiceReplyMode | undefined,
32
39
  ) => Promise<void>;
@@ -118,8 +125,9 @@ export interface TelegramSettingsMenuRuntimeDeps<
118
125
 
119
126
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
120
127
  export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
121
- export const RICH_DRAFT_PREVIEWS_SETTINGS_TITLE =
122
- "<b>📝 Rich draft previews:</b>";
128
+ export const DRAFT_PREVIEWS_SETTINGS_TITLE = "<b>📝 Draft previews:</b>";
129
+ export const ASSISTANT_RENDERING_SETTINGS_TITLE =
130
+ "<b>🧾 Assistant rendering:</b>";
123
131
  export const TIME_INJECTION_MODE_SETTINGS_TITLE =
124
132
  "<b>🕒 Time injection mode:</b>";
125
133
  export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
@@ -155,14 +163,27 @@ export function buildProactivePushSettingsText(
155
163
  ].join("\n");
156
164
  }
157
165
 
158
- export function buildRichDraftPreviewsSettingsText(enabled: boolean): string {
166
+ export function buildDraftPreviewsSettingsText(enabled: boolean): string {
167
+ return [
168
+ `${DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
169
+ "",
170
+ "Show live answer drafts while the model is answering.",
171
+ "",
172
+ "<code>-</code> <code>off</code> (default): show native active status, then send one final answer.",
173
+ "<code>-</code> <code>on</code>: stream safe Telegram Rich Draft frames before the final answer.",
174
+ ].join("\n");
175
+ }
176
+
177
+ export function buildAssistantRenderingSettingsText(
178
+ mode: TelegramAssistantRenderingMode,
179
+ ): string {
159
180
  return [
160
- `${RICH_DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
181
+ `${ASSISTANT_RENDERING_SETTINGS_TITLE} <code>${mode}</code>`,
161
182
  "",
162
- "Show progressive Rich Markdown draft previews while the model is answering.",
183
+ "Choose how final assistant Markdown answers are delivered.",
163
184
  "",
164
- "<code>-</code> <code>off</code> (default): show native active status, then send the final Rich Markdown answer.",
165
- "<code>-</code> <code>on</code>: also stream safe Rich Markdown draft previews before the final answer.",
185
+ "<code>-</code> <code>rich</code> (default): use Telegram Native Rich Markdown.",
186
+ "<code>-</code> <code>html</code>: use the legacy Markdown-to-HTML renderer.",
166
187
  ].join("\n");
167
188
  }
168
189
 
@@ -200,12 +221,35 @@ export function buildTimeInjectionModeSettingsText(
200
221
 
201
222
  export function buildTelegramSettingsMenuReplyMarkup(
202
223
  proactivePushEnabled: boolean,
203
- richDraftPreviewsEnabled: boolean,
204
- voiceReplyMode: TelegramVoiceReplyMode,
205
- timeInjectionMode: TelegramTimeMode,
206
- sectionRegistry?: TelegramSectionRegistry,
224
+ draftPreviewsEnabled: boolean,
225
+ assistantRenderingModeOrVoiceReplyMode: TelegramAssistantRenderingMode | TelegramVoiceReplyMode,
226
+ voiceReplyModeOrTimeInjectionMode: TelegramVoiceReplyMode | TelegramTimeMode,
227
+ timeInjectionModeOrSectionRegistry?: TelegramTimeMode | TelegramSectionRegistry,
228
+ sectionRegistryOrVoiceReplyModeConfigured?: TelegramSectionRegistry | boolean,
207
229
  voiceReplyModeConfigured = true,
208
230
  ): TelegramSettingsMenuReplyMarkup {
231
+ const hasRenderingMode =
232
+ assistantRenderingModeOrVoiceReplyMode === "rich" ||
233
+ assistantRenderingModeOrVoiceReplyMode === "html";
234
+ const assistantRenderingMode: TelegramAssistantRenderingMode = hasRenderingMode
235
+ ? assistantRenderingModeOrVoiceReplyMode
236
+ : "rich";
237
+ const voiceReplyMode = hasRenderingMode
238
+ ? (voiceReplyModeOrTimeInjectionMode as TelegramVoiceReplyMode)
239
+ : (assistantRenderingModeOrVoiceReplyMode as TelegramVoiceReplyMode);
240
+ const timeInjectionMode = hasRenderingMode
241
+ ? (timeInjectionModeOrSectionRegistry as TelegramTimeMode)
242
+ : (voiceReplyModeOrTimeInjectionMode as TelegramTimeMode);
243
+ const sectionRegistry = hasRenderingMode
244
+ ? (sectionRegistryOrVoiceReplyModeConfigured as
245
+ | TelegramSectionRegistry
246
+ | undefined)
247
+ : (timeInjectionModeOrSectionRegistry as TelegramSectionRegistry | undefined);
248
+ const effectiveVoiceReplyModeConfigured = hasRenderingMode
249
+ ? voiceReplyModeConfigured
250
+ : typeof sectionRegistryOrVoiceReplyModeConfigured === "boolean"
251
+ ? sectionRegistryOrVoiceReplyModeConfigured
252
+ : true;
209
253
  const rows: Array<Array<{ text: string; callback_data: string }>> = [
210
254
  [{ text: "⬆️ Main menu", callback_data: "menu:back" }],
211
255
  ];
@@ -221,7 +265,7 @@ export function buildTelegramSettingsMenuReplyMarkup(
221
265
  {
222
266
  text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
223
267
  getVoiceReplyModeLabel(
224
- getVoiceReplyModeSetting(voiceReplyMode, voiceReplyModeConfigured),
268
+ getVoiceReplyModeSetting(voiceReplyMode, effectiveVoiceReplyModeConfigured),
225
269
  ),
226
270
  )}`,
227
271
  callback_data: "settings:open:voice-reply",
@@ -235,8 +279,14 @@ export function buildTelegramSettingsMenuReplyMarkup(
235
279
  ],
236
280
  [
237
281
  {
238
- text: `📝 Rich drafts: ${richDraftPreviewsEnabled ? "on" : "off"}`,
239
- callback_data: "settings:open:rich-drafts",
282
+ text: `📝 Draft previews: ${draftPreviewsEnabled ? "on" : "off"}`,
283
+ callback_data: "settings:open:draft-previews",
284
+ },
285
+ ],
286
+ [
287
+ {
288
+ text: `🧾 Rendering: ${assistantRenderingMode}`,
289
+ callback_data: "settings:open:assistant-rendering",
240
290
  },
241
291
  ],
242
292
  [
@@ -262,6 +312,7 @@ export async function openTelegramSettingsMenu<
262
312
  buildTelegramSettingsMenuReplyMarkup(
263
313
  deps.isProactivePushEnabled(),
264
314
  deps.areRichDraftPreviewsEnabled(),
315
+ deps.getAssistantRenderingMode(),
265
316
  deps.getVoiceReplyMode(),
266
317
  deps.getTimeInjectionMode(),
267
318
  sectionRegistry,
@@ -294,7 +345,7 @@ export function buildProactivePushSettingsReplyMarkup(
294
345
  };
295
346
  }
296
347
 
297
- export function buildRichDraftPreviewsSettingsReplyMarkup(
348
+ export function buildDraftPreviewsSettingsReplyMarkup(
298
349
  enabled: boolean,
299
350
  ): TelegramSettingsMenuReplyMarkup {
300
351
  return {
@@ -303,17 +354,34 @@ export function buildRichDraftPreviewsSettingsReplyMarkup(
303
354
  [
304
355
  {
305
356
  text: enabled ? "🟢 On" : "⚫️ On",
306
- callback_data: "settings:set:rich-drafts:on",
357
+ callback_data: "settings:set:draft-previews:on",
307
358
  },
308
359
  {
309
360
  text: enabled ? "⚫️ Off" : "🟡 Off",
310
- callback_data: "settings:set:rich-drafts:off",
361
+ callback_data: "settings:set:draft-previews:off"
311
362
  },
312
363
  ],
313
364
  ],
314
365
  };
315
366
  }
316
367
 
368
+ export function buildAssistantRenderingSettingsReplyMarkup(
369
+ mode: TelegramAssistantRenderingMode,
370
+ ): TelegramSettingsMenuReplyMarkup {
371
+ const modes: TelegramAssistantRenderingMode[] = ["rich", "html"];
372
+ return {
373
+ inline_keyboard: [
374
+ [{ text: "⬆️ Back", callback_data: "settings:list" }],
375
+ ...modes.map((value) => [
376
+ {
377
+ text: `${value === mode ? "🟢 " : ""}${value}`,
378
+ callback_data: `settings:set:assistant-rendering:${value}`,
379
+ },
380
+ ]),
381
+ ],
382
+ };
383
+ }
384
+
317
385
  export function buildTimeInjectionModeSettingsReplyMarkup(
318
386
  mode: TelegramTimeMode,
319
387
  ): TelegramSettingsMenuReplyMarkup {
@@ -364,6 +432,7 @@ export async function updateTelegramSettingsMenuMessage(
364
432
  buildTelegramSettingsMenuReplyMarkup(
365
433
  deps.isProactivePushEnabled(),
366
434
  deps.areRichDraftPreviewsEnabled(),
435
+ deps.getAssistantRenderingMode(),
367
436
  deps.getVoiceReplyMode(),
368
437
  deps.getTimeInjectionMode(),
369
438
  sectionRegistry,
@@ -382,13 +451,23 @@ export async function updateProactivePushSettingsMessage(
382
451
  );
383
452
  }
384
453
 
385
- export async function updateRichDraftPreviewsSettingsMessage(
454
+ export async function updateDraftPreviewsSettingsMessage(
386
455
  deps: TelegramSettingsMenuCallbackDeps,
387
456
  ): Promise<void> {
388
457
  const enabled = deps.areRichDraftPreviewsEnabled();
389
458
  await deps.updateSettingsMessage(
390
- buildRichDraftPreviewsSettingsText(enabled),
391
- buildRichDraftPreviewsSettingsReplyMarkup(enabled),
459
+ buildDraftPreviewsSettingsText(enabled),
460
+ buildDraftPreviewsSettingsReplyMarkup(enabled),
461
+ );
462
+ }
463
+
464
+ export async function updateAssistantRenderingSettingsMessage(
465
+ deps: TelegramSettingsMenuCallbackDeps,
466
+ ): Promise<void> {
467
+ const mode = deps.getAssistantRenderingMode();
468
+ await deps.updateSettingsMessage(
469
+ buildAssistantRenderingSettingsText(mode),
470
+ buildAssistantRenderingSettingsReplyMarkup(mode),
392
471
  );
393
472
  }
394
473
 
@@ -429,8 +508,13 @@ export async function handleTelegramSettingsMenuCallbackAction(
429
508
  await deps.answerCallbackQuery(callbackQueryId);
430
509
  return true;
431
510
  }
432
- if (data === "settings:open:rich-drafts") {
433
- await updateRichDraftPreviewsSettingsMessage(deps);
511
+ if (data === "settings:open:draft-previews" || data === "settings:open:rich-drafts") {
512
+ await updateDraftPreviewsSettingsMessage(deps);
513
+ await deps.answerCallbackQuery(callbackQueryId);
514
+ return true;
515
+ }
516
+ if (data === "settings:open:assistant-rendering") {
517
+ await updateAssistantRenderingSettingsMessage(deps);
434
518
  await deps.answerCallbackQuery(callbackQueryId);
435
519
  return true;
436
520
  }
@@ -487,18 +571,29 @@ export async function handleTelegramSettingsMenuCallbackAction(
487
571
  }
488
572
  }
489
573
  if (
574
+ data === "settings:set:draft-previews:on" ||
575
+ data === "settings:set:draft-previews:off" ||
490
576
  data === "settings:set:rich-drafts:on" ||
491
577
  data === "settings:set:rich-drafts:off"
492
578
  ) {
493
579
  const enabled = data.endsWith(":on");
494
580
  await deps.setRichDraftPreviewsEnabled(enabled);
495
- await updateRichDraftPreviewsSettingsMessage(deps);
581
+ await updateDraftPreviewsSettingsMessage(deps);
496
582
  await deps.answerCallbackQuery(
497
583
  callbackQueryId,
498
- `Rich draft previews ${enabled ? "enabled" : "disabled"}`,
584
+ `Draft previews ${enabled ? "enabled" : "disabled"}`,
499
585
  );
500
586
  return true;
501
587
  }
588
+ if (data.startsWith("settings:set:assistant-rendering:")) {
589
+ const mode = data.slice("settings:set:assistant-rendering:".length);
590
+ if (mode === "rich" || mode === "html") {
591
+ await deps.setAssistantRenderingMode(mode);
592
+ await updateAssistantRenderingSettingsMessage(deps);
593
+ await deps.answerCallbackQuery(callbackQueryId, `Rendering: ${mode}`);
594
+ return true;
595
+ }
596
+ }
502
597
  if (
503
598
  data === "settings:set:proactive:on" ||
504
599
  data === "settings:set:proactive:off"
@@ -530,6 +625,7 @@ export function createTelegramSettingsMenuRuntime<
530
625
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
531
626
  isProactivePushEnabled: deps.isProactivePushEnabled,
532
627
  areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
628
+ getAssistantRenderingMode: deps.getAssistantRenderingMode,
533
629
  getVoiceReplyMode: deps.getVoiceReplyMode,
534
630
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
535
631
  getTimeInjectionMode: deps.getTimeInjectionMode,
@@ -549,6 +645,7 @@ export function createTelegramSettingsMenuRuntime<
549
645
  {
550
646
  isProactivePushEnabled: deps.isProactivePushEnabled,
551
647
  areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
648
+ getAssistantRenderingMode: deps.getAssistantRenderingMode,
552
649
  getVoiceReplyMode: deps.getVoiceReplyMode,
553
650
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
554
651
  getTimeInjectionMode: deps.getTimeInjectionMode,
@@ -588,6 +685,8 @@ export function createTelegramSettingsMenuRuntime<
588
685
  return true;
589
686
  }
590
687
  if (
688
+ query.data === "settings:set:draft-previews:on" ||
689
+ query.data === "settings:set:draft-previews:off" ||
591
690
  query.data === "settings:set:rich-drafts:on" ||
592
691
  query.data === "settings:set:rich-drafts:off"
593
692
  ) {
@@ -595,10 +694,18 @@ export function createTelegramSettingsMenuRuntime<
595
694
  await deps.setRichDraftPreviewsEnabled(enabled);
596
695
  await deps.answerCallbackQuery(
597
696
  query.id,
598
- `Rich draft previews ${enabled ? "enabled" : "disabled"}`,
697
+ `Draft previews ${enabled ? "enabled" : "disabled"}`,
599
698
  );
600
699
  return true;
601
700
  }
701
+ if (query.data.startsWith("settings:set:assistant-rendering:")) {
702
+ const mode = query.data.slice("settings:set:assistant-rendering:".length);
703
+ if (mode === "rich" || mode === "html") {
704
+ await deps.setAssistantRenderingMode(mode);
705
+ await deps.answerCallbackQuery(query.id, `Rendering: ${mode}`);
706
+ return true;
707
+ }
708
+ }
602
709
  const hasTimeInjectionPrefix = query.data.startsWith(
603
710
  "settings:set:time-injection:",
604
711
  );
@@ -630,11 +737,13 @@ export function createTelegramSettingsMenuRuntime<
630
737
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
631
738
  isProactivePushEnabled: deps.isProactivePushEnabled,
632
739
  areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
740
+ getAssistantRenderingMode: deps.getAssistantRenderingMode,
633
741
  getVoiceReplyMode: deps.getVoiceReplyMode,
634
742
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
635
743
  getTimeInjectionMode: deps.getTimeInjectionMode,
636
744
  setProactivePushEnabled: deps.setProactivePushEnabled,
637
745
  setRichDraftPreviewsEnabled: deps.setRichDraftPreviewsEnabled,
746
+ setAssistantRenderingMode: deps.setAssistantRenderingMode,
638
747
  setVoiceReplyMode: deps.setVoiceReplyMode,
639
748
  setTimeInjectionMode: deps.setTimeInjectionMode,
640
749
  updateSettingsMessage: (text, replyMarkup) =>
package/lib/replies.ts CHANGED
@@ -678,6 +678,8 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
678
678
  // UI/compat regular-message runtime for bridge-owned text and interactive
679
679
  // surfaces. Assistant and guest Markdown delivery bypass this path and use
680
680
  // native Rich Message helpers above.
681
+ export type TelegramAssistantRenderingMode = "rich" | "html";
682
+
681
683
  export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
682
684
  renderTelegramMessage: (
683
685
  text: string,
@@ -685,6 +687,7 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
685
687
  ) => TelegramRenderedChunk[];
686
688
  replyTransport: TelegramReplyTransport<TReplyMarkup>;
687
689
  recordOwnership?: TelegramReplyOwnershipRecorder["record"];
690
+ getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
688
691
  sendRichMessage: (
689
692
  body: TelegramSendRichMessageBody,
690
693
  ) => Promise<TelegramSentMessage>;
@@ -732,6 +735,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
732
735
  text: string,
733
736
  options?: { mode?: TelegramRenderMode },
734
737
  ) => TelegramRenderedChunk[];
738
+ getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
735
739
  sendRichMessage: (
736
740
  body: TelegramSendRichMessageBody,
737
741
  ) => Promise<TelegramSentMessage>;
@@ -752,6 +756,7 @@ export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
752
756
  deps.renderTelegramMessage ?? renderTelegramMessage,
753
757
  replyTransport,
754
758
  recordOwnership: deps.recordOwnership,
759
+ getAssistantRenderingMode: deps.getAssistantRenderingMode,
755
760
  sendRichMessage: deps.sendRichMessage,
756
761
  }),
757
762
  };
@@ -777,13 +782,19 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
777
782
  );
778
783
  },
779
784
  sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
780
- if (typeof options?.target?.threadId === "number" && replyToMessageId && replyToMessageId > 0) {
785
+ const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
786
+ if (
787
+ renderingMode === "html" ||
788
+ (typeof options?.target?.threadId === "number" &&
789
+ replyToMessageId !== undefined &&
790
+ replyToMessageId > 0)
791
+ ) {
781
792
  return deps.replyTransport.sendRenderedChunks(
782
793
  chatId,
783
794
  deps.renderTelegramMessage(markdown, { mode: "markdown" }),
784
795
  {
785
- replyMarkup: options.replyMarkup,
786
- target: options.target,
796
+ replyMarkup: options?.replyMarkup,
797
+ target: options?.target,
787
798
  replyToMessageId,
788
799
  },
789
800
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.19.0",
3
+ "version": "0.19.1",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"