@llblab/pi-telegram 0.19.0 → 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 +15 -0
- package/README.md +58 -58
- package/docs/architecture.md +3 -3
- package/index.ts +5 -2
- package/lib/config.ts +63 -8
- package/lib/menu-settings.ts +146 -37
- package/lib/preview.ts +11 -0
- package/lib/replies.ts +15 -3
- package/lib/telegram-api.ts +18 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.19.1: Settings Layer Hotfix
|
|
12
|
+
|
|
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.
|
|
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.
|
|
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.
|
|
16
|
+
- `[Validation]` Updated menu/settings/reply regressions for the two-axis configuration model.
|
|
17
|
+
|
|
3
18
|
## 0.19.0: Telegram Companion Hub
|
|
4
19
|
|
|
5
20
|
- `[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
|
|
76
|
-
|
|
|
77
|
-
| Operator companion
|
|
78
|
-
| Runtime adapter
|
|
79
|
-
| Telegram UI harness
|
|
80
|
-
| Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads
|
|
81
|
-
| Extension platform
|
|
82
|
-
| Safety boundary
|
|
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
|
|
89
|
-
|
|
|
90
|
-
| Prompt intake
|
|
91
|
-
| Queue control
|
|
92
|
-
| Operator menu
|
|
93
|
-
| Prompt templates
|
|
94
|
-
| Model and thinking
|
|
95
|
-
| Compaction
|
|
96
|
-
|
|
|
97
|
-
|
|
|
98
|
-
| Bridge UI rendering
|
|
99
|
-
| Inbound files
|
|
100
|
-
| Outbound artifacts
|
|
101
|
-
| Voice input
|
|
102
|
-
| Voice output
|
|
103
|
-
| Buttons
|
|
104
|
-
| Callback routing
|
|
105
|
-
| Threaded Mode
|
|
106
|
-
| Reroute and restore
|
|
107
|
-
| Extension sections
|
|
108
|
-
| Runtime diagnostics
|
|
109
|
-
| Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior.
|
|
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
|
|
130
|
-
|
|
|
131
|
-
| `/start`
|
|
132
|
-
| `/compact`
|
|
133
|
-
| `/next`
|
|
134
|
-
| `/continue` | Enqueue a priority continuation prompt
|
|
135
|
-
| `/abort`
|
|
136
|
-
| `/stop`
|
|
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
|
|
145
|
-
|
|
|
146
|
-
| `/telegram-setup`
|
|
147
|
-
| `/telegram-connect`
|
|
148
|
-
| `/telegram-disconnect` | Stop polling and release ownership
|
|
149
|
-
| `/telegram-status`
|
|
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.
|
|
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
|
|
188
|
-
|
|
|
189
|
-
| Classic DM
|
|
190
|
-
| Threaded Mode | Several visible Pi terminals sharing one bot
|
|
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
|
|
197
|
-
|
|
|
198
|
-
| Bot token bootstrap
|
|
199
|
-
| HTTP proxy
|
|
200
|
-
| Telegram network family
|
|
201
|
-
| Agent data root
|
|
202
|
-
| Inbound file limit
|
|
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`,
|
|
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
|
|
package/docs/architecture.md
CHANGED
|
@@ -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
|
|
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.
|
|
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.
|
|
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 } =
|
|
@@ -477,7 +478,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
477
478
|
recordRuntimeEvent,
|
|
478
479
|
});
|
|
479
480
|
const nativeMarkdownDraftSender =
|
|
480
|
-
TelegramApi.
|
|
481
|
+
TelegramApi.createTelegramAssistantDraftSender({
|
|
482
|
+
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
|
|
483
|
+
renderMarkdownToHtmlDraft: Replies.renderTelegramMarkdownToHtmlDraft,
|
|
481
484
|
sendMessageDraft,
|
|
482
485
|
sendRichMessageDraft,
|
|
483
486
|
});
|
|
@@ -487,7 +490,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
487
490
|
getMessageText: Replies.getAgentMessageText,
|
|
488
491
|
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
|
|
489
492
|
sendDraft: nativeMarkdownDraftSender,
|
|
490
|
-
canSend: configControls.
|
|
493
|
+
canSend: configControls.areDraftPreviewsEnabled,
|
|
491
494
|
sendMarkdownReply,
|
|
492
495
|
recordRuntimeEvent,
|
|
493
496
|
...replyTransport,
|
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;
|
|
@@ -57,7 +59,16 @@ export interface TelegramConfig {
|
|
|
57
59
|
attachmentHandlers?: TelegramInboundHandlerConfig[];
|
|
58
60
|
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
59
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 */
|
|
60
69
|
richDraftPreviews?: boolean;
|
|
70
|
+
/** @deprecated use assistant.rendering */
|
|
71
|
+
assistantRendering?: TelegramAssistantRenderingMode;
|
|
61
72
|
voice?: {
|
|
62
73
|
replyMode?: "manual" | "mirror" | "always";
|
|
63
74
|
/** Whether to attach the provider's transcriptText as caption on voice messages */
|
|
@@ -260,18 +271,60 @@ export function createTelegramProactivePushSetter(
|
|
|
260
271
|
};
|
|
261
272
|
}
|
|
262
273
|
|
|
263
|
-
export function
|
|
274
|
+
export function createTelegramDraftPreviewsChecker(
|
|
264
275
|
configStore: Pick<TelegramConfigStore, "get">,
|
|
265
276
|
): () => boolean {
|
|
266
|
-
return () =>
|
|
277
|
+
return () => {
|
|
278
|
+
const config = configStore.get();
|
|
279
|
+
return (
|
|
280
|
+
config.assistant?.draftPreviews ??
|
|
281
|
+
config.draftPreviews ??
|
|
282
|
+
config.richDraftPreviews ??
|
|
283
|
+
false
|
|
284
|
+
);
|
|
285
|
+
};
|
|
267
286
|
}
|
|
268
287
|
|
|
269
|
-
export function
|
|
288
|
+
export function createTelegramDraftPreviewsSetter(
|
|
270
289
|
configStore: TelegramMutableConfigStore,
|
|
271
290
|
): (enabled: boolean) => Promise<void> {
|
|
272
291
|
return async (enabled) => {
|
|
273
292
|
await loadLatestTelegramConfig(configStore);
|
|
274
|
-
const
|
|
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
|
+
};
|
|
302
|
+
configStore.set(config);
|
|
303
|
+
await configStore.persist(config);
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function createTelegramAssistantRenderingModeGetter(
|
|
308
|
+
configStore: Pick<TelegramConfigStore, "get">,
|
|
309
|
+
): () => TelegramAssistantRenderingMode {
|
|
310
|
+
return () => {
|
|
311
|
+
const config = configStore.get();
|
|
312
|
+
const mode = config.assistant?.rendering ?? config.assistantRendering;
|
|
313
|
+
return mode === "html" ? "html" : "rich";
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function createTelegramAssistantRenderingModeSetter(
|
|
318
|
+
configStore: TelegramMutableConfigStore,
|
|
319
|
+
): (mode: TelegramAssistantRenderingMode) => Promise<void> {
|
|
320
|
+
return async (mode) => {
|
|
321
|
+
await loadLatestTelegramConfig(configStore);
|
|
322
|
+
const { assistantRendering: _legacyAssistantRendering, ...current } =
|
|
323
|
+
configStore.get();
|
|
324
|
+
const config = {
|
|
325
|
+
...current,
|
|
326
|
+
assistant: { ...current.assistant, rendering: mode },
|
|
327
|
+
};
|
|
275
328
|
configStore.set(config);
|
|
276
329
|
await configStore.persist(config);
|
|
277
330
|
};
|
|
@@ -412,10 +465,12 @@ export function createTelegramConfigControls(
|
|
|
412
465
|
return {
|
|
413
466
|
isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
|
|
414
467
|
setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
468
|
+
areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
|
|
469
|
+
setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
|
|
470
|
+
getAssistantRenderingMode:
|
|
471
|
+
createTelegramAssistantRenderingModeGetter(configStore),
|
|
472
|
+
setAssistantRenderingMode:
|
|
473
|
+
createTelegramAssistantRenderingModeSetter(configStore),
|
|
419
474
|
getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
|
|
420
475
|
isVoiceReplyModeConfigured:
|
|
421
476
|
createTelegramVoiceReplyModeConfiguredChecker(configStore),
|
package/lib/menu-settings.ts
CHANGED
|
@@ -8,7 +8,10 @@ import {
|
|
|
8
8
|
getTelegramExtensionSettingsRows,
|
|
9
9
|
type TelegramSectionRegistry,
|
|
10
10
|
} from "./sections.ts";
|
|
11
|
-
import type {
|
|
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";
|
|
@@ -18,7 +21,8 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
|
18
21
|
|
|
19
22
|
export interface TelegramSettingsStateDeps {
|
|
20
23
|
isProactivePushEnabled: () => boolean;
|
|
21
|
-
|
|
24
|
+
areDraftPreviewsEnabled: () => boolean;
|
|
25
|
+
getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
|
|
22
26
|
getTimeInjectionMode: () => TelegramTimeMode;
|
|
23
27
|
getVoiceReplyMode: () => TelegramVoiceReplyMode;
|
|
24
28
|
isVoiceReplyModeConfigured: () => boolean;
|
|
@@ -26,7 +30,10 @@ export interface TelegramSettingsStateDeps {
|
|
|
26
30
|
|
|
27
31
|
export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
|
|
28
32
|
setProactivePushEnabled: (enabled: boolean) => Promise<void>;
|
|
29
|
-
|
|
33
|
+
setDraftPreviewsEnabled: (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
|
|
122
|
-
|
|
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
|
|
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
|
-
`${
|
|
181
|
+
`${ASSISTANT_RENDERING_SETTINGS_TITLE} <code>${mode}</code>`,
|
|
161
182
|
"",
|
|
162
|
-
"
|
|
183
|
+
"Choose how final assistant Markdown answers are delivered.",
|
|
163
184
|
"",
|
|
164
|
-
"<code>-</code> <code>
|
|
165
|
-
"<code>-</code> <code>
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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,
|
|
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: `📝
|
|
239
|
-
callback_data: "settings:open:
|
|
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
|
[
|
|
@@ -261,7 +311,8 @@ export async function openTelegramSettingsMenu<
|
|
|
261
311
|
buildTelegramSettingsMenuText(),
|
|
262
312
|
buildTelegramSettingsMenuReplyMarkup(
|
|
263
313
|
deps.isProactivePushEnabled(),
|
|
264
|
-
deps.
|
|
314
|
+
deps.areDraftPreviewsEnabled(),
|
|
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
|
|
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:
|
|
357
|
+
callback_data: "settings:set:draft-previews:on",
|
|
307
358
|
},
|
|
308
359
|
{
|
|
309
360
|
text: enabled ? "⚫️ Off" : "🟡 Off",
|
|
310
|
-
callback_data: "settings:set:
|
|
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 {
|
|
@@ -363,7 +431,8 @@ export async function updateTelegramSettingsMenuMessage(
|
|
|
363
431
|
buildTelegramSettingsMenuText(),
|
|
364
432
|
buildTelegramSettingsMenuReplyMarkup(
|
|
365
433
|
deps.isProactivePushEnabled(),
|
|
366
|
-
deps.
|
|
434
|
+
deps.areDraftPreviewsEnabled(),
|
|
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
|
|
454
|
+
export async function updateDraftPreviewsSettingsMessage(
|
|
386
455
|
deps: TelegramSettingsMenuCallbackDeps,
|
|
387
456
|
): Promise<void> {
|
|
388
|
-
const enabled = deps.
|
|
457
|
+
const enabled = deps.areDraftPreviewsEnabled();
|
|
389
458
|
await deps.updateSettingsMessage(
|
|
390
|
-
|
|
391
|
-
|
|
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
|
|
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
|
-
await deps.
|
|
495
|
-
await
|
|
580
|
+
await deps.setDraftPreviewsEnabled(enabled);
|
|
581
|
+
await updateDraftPreviewsSettingsMessage(deps);
|
|
496
582
|
await deps.answerCallbackQuery(
|
|
497
583
|
callbackQueryId,
|
|
498
|
-
`
|
|
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"
|
|
@@ -529,7 +624,8 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
529
624
|
{
|
|
530
625
|
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
|
|
531
626
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
532
|
-
|
|
627
|
+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
|
|
628
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
533
629
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
534
630
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
535
631
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
@@ -548,7 +644,8 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
548
644
|
updateTelegramSettingsMenuMessage(
|
|
549
645
|
{
|
|
550
646
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
551
|
-
|
|
647
|
+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
|
|
648
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
552
649
|
getVoiceReplyMode: deps.getVoiceReplyMode,
|
|
553
650
|
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
|
|
554
651
|
getTimeInjectionMode: deps.getTimeInjectionMode,
|
|
@@ -588,17 +685,27 @@ 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
|
) {
|
|
594
693
|
const enabled = query.data.endsWith(":on");
|
|
595
|
-
await deps.
|
|
694
|
+
await deps.setDraftPreviewsEnabled(enabled);
|
|
596
695
|
await deps.answerCallbackQuery(
|
|
597
696
|
query.id,
|
|
598
|
-
`
|
|
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
|
);
|
|
@@ -629,12 +736,14 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
629
736
|
}
|
|
630
737
|
return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
|
|
631
738
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
|
632
|
-
|
|
739
|
+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
|
|
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
|
+
setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
|
|
746
|
+
setAssistantRenderingMode: deps.setAssistantRenderingMode,
|
|
638
747
|
setVoiceReplyMode: deps.setVoiceReplyMode,
|
|
639
748
|
setTimeInjectionMode: deps.setTimeInjectionMode,
|
|
640
749
|
updateSettingsMessage: (text, replyMarkup) =>
|
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
|
|
|
@@ -678,6 +684,8 @@ export async function sendTelegramNativeMarkdownReply<TReplyMarkup = unknown>(
|
|
|
678
684
|
// UI/compat regular-message runtime for bridge-owned text and interactive
|
|
679
685
|
// surfaces. Assistant and guest Markdown delivery bypass this path and use
|
|
680
686
|
// native Rich Message helpers above.
|
|
687
|
+
export type TelegramAssistantRenderingMode = "rich" | "html";
|
|
688
|
+
|
|
681
689
|
export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
|
|
682
690
|
renderTelegramMessage: (
|
|
683
691
|
text: string,
|
|
@@ -685,6 +693,7 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
|
|
|
685
693
|
) => TelegramRenderedChunk[];
|
|
686
694
|
replyTransport: TelegramReplyTransport<TReplyMarkup>;
|
|
687
695
|
recordOwnership?: TelegramReplyOwnershipRecorder["record"];
|
|
696
|
+
getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
|
|
688
697
|
sendRichMessage: (
|
|
689
698
|
body: TelegramSendRichMessageBody,
|
|
690
699
|
) => Promise<TelegramSentMessage>;
|
|
@@ -732,6 +741,7 @@ export interface TelegramRenderedMessageDeliveryRuntimeDeps<
|
|
|
732
741
|
text: string,
|
|
733
742
|
options?: { mode?: TelegramRenderMode },
|
|
734
743
|
) => TelegramRenderedChunk[];
|
|
744
|
+
getAssistantRenderingMode?: () => TelegramAssistantRenderingMode;
|
|
735
745
|
sendRichMessage: (
|
|
736
746
|
body: TelegramSendRichMessageBody,
|
|
737
747
|
) => Promise<TelegramSentMessage>;
|
|
@@ -752,6 +762,7 @@ export function createTelegramRenderedMessageDeliveryRuntime<TReplyMarkup>(
|
|
|
752
762
|
deps.renderTelegramMessage ?? renderTelegramMessage,
|
|
753
763
|
replyTransport,
|
|
754
764
|
recordOwnership: deps.recordOwnership,
|
|
765
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
755
766
|
sendRichMessage: deps.sendRichMessage,
|
|
756
767
|
}),
|
|
757
768
|
};
|
|
@@ -777,13 +788,14 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
|
|
|
777
788
|
);
|
|
778
789
|
},
|
|
779
790
|
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
780
|
-
|
|
791
|
+
const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
|
|
792
|
+
if (renderingMode === "html") {
|
|
781
793
|
return deps.replyTransport.sendRenderedChunks(
|
|
782
794
|
chatId,
|
|
783
795
|
deps.renderTelegramMessage(markdown, { mode: "markdown" }),
|
|
784
796
|
{
|
|
785
|
-
replyMarkup: options
|
|
786
|
-
target: options
|
|
797
|
+
replyMarkup: options?.replyMarkup,
|
|
798
|
+
target: options?.target,
|
|
787
799
|
replyToMessageId,
|
|
788
800
|
},
|
|
789
801
|
);
|
package/lib/telegram-api.ts
CHANGED
|
@@ -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"];
|