@llblab/pi-telegram 0.2.7 → 0.2.9
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/AGENTS.md +2 -1
- package/CHANGELOG.md +5 -0
- package/README.md +36 -120
- package/docs/architecture.md +13 -6
- package/index.ts +13 -12
- package/lib/polling.ts +2 -0
- package/lib/preview.ts +212 -0
- package/lib/rendering.ts +616 -59
- package/lib/replies.ts +2 -181
- package/lib/updates.ts +0 -8
- package/package.json +1 -1
- package/tests/menu.test.ts +46 -15
- package/tests/preview.test.ts +441 -0
- package/tests/queue.test.ts +3 -0
- package/tests/rendering.test.ts +167 -0
- package/tests/replies.test.ts +2 -222
- package/tests/updates.test.ts +15 -24
package/AGENTS.md
CHANGED
|
@@ -57,7 +57,8 @@
|
|
|
57
57
|
- Keep comments and user-facing docs in English unless the surrounding file already follows another convention
|
|
58
58
|
- Each project `.ts` file should start with a short multi-line responsibility header comment that explains the file boundary to future maintainers
|
|
59
59
|
- Name extracted `/lib` modules and mirrored `/tests` suites by bare domain when the repository already supplies the Telegram scope; prefer `api.ts`, `queue.ts`, `updates.ts`, and `queue.test.ts` over redundant `telegram-*` filename prefixes
|
|
60
|
-
- Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction; current extracted domains include queueing/runtime decisions, replies, polling, updates, attachments, registration and lifecycle-hook binding, Telegram API/config support, turn-building, media extraction, setup, rendering, status rendering, menu/model-resolution/UI support, and model-switch support
|
|
60
|
+
- Prefer targeted edits, keeping `index.ts` as the orchestration layer and moving reusable logic into flat `/lib` domain modules when a subsystem becomes large enough to earn extraction; current extracted domains include queueing/runtime decisions, preview streaming, replies, polling, updates, attachments, registration and lifecycle-hook binding, Telegram API/config support, turn-building, media extraction, setup, rendering, status rendering, menu/model-resolution/UI support, and model-switch support
|
|
61
|
+
- Keep preview appearance logic in the rendering domain and preview transport/lifecycle logic in the preview domain so richer streaming strategies can evolve without entangling Telegram delivery state with Markdown formatting rules
|
|
61
62
|
|
|
62
63
|
## 7. Operational Conventions
|
|
63
64
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Current
|
|
4
4
|
|
|
5
|
+
- `[Rendering]` Hardened link rendering so absolute links stay clickable, markdown-heavy link labels reduce to plain clickable labels, tooltip titles are ignored safely, balanced-parenthesis URLs stay intact, and unsupported link forms degrade without broken anchors. Impact: Telegram replies now keep more links usable while avoiding malformed output for relative, reference-style, or footnote-like link syntax.
|
|
6
|
+
- `[Preview]` Evolved rich streaming from first-chunk snapshots to stable-block previews with a conservative plain tail fallback, while preserving original blank-line spacing between rendered blocks and keeping headings visually separated from following blocks. Impact: closed top-level Markdown blocks now stream as rich Telegram HTML before finalization, incomplete fences, quotes, lists, and other trailing work remain readable without producing broken rich formatting, preview/final block spacing no longer collapses extra empty lines, and headings no longer visually merge into following code blocks when source Markdown omits a blank line.
|
|
7
|
+
- `[Refactor]` Split preview concerns so runtime transport and finalization live in the preview domain while preview snapshot derivation lives in the rendering domain. Impact: rich streaming can evolve independently from final reply delivery while keeping preview appearance decisions closer to the Telegram renderer.
|
|
8
|
+
- `[Streaming]` Switched Telegram previews from plain draft-first text to rich first-chunk message editing, so formatting appears during generation instead of only after finalization. Impact: users now see richer streamed output earlier, while final replies still replace the preview with fully rendered Telegram HTML.
|
|
9
|
+
- `[Rendering]` Preserved leading indentation on the first Markdown line, kept numeric markers for ordered task lists in both preview and final Telegram rendering, and stopped reinterpreting standalone `[x]` or `[ ]` prose as inline checkboxes. Impact: nested content no longer flattens when a message starts with indentation, numbered checklists keep their ordered semantics, and literal checklist-like prose stays literal.
|
|
5
10
|
- `[Queue UI]` Marked liked high-priority queued Telegram turns with `⬆` in the pi status-bar queue preview. Impact: operators can now distinguish reaction-promoted turns from normal queued prompts at a glance.
|
|
6
11
|
- `[Docs]` Added short responsibility header comments to every project `.ts` file. Impact: file boundaries are easier to understand while navigating the growing `/lib` split.
|
|
7
12
|
- `[Naming]` Renamed extracted domain modules and mirrored regression suites to use repo-scoped bare domain filenames such as `api.ts`, `queue.ts`, and `queue.test.ts` instead of repeating `telegram-*` in every path. Impact: the internal topology is easier to scan and stays aligned with the repository-level Telegram scope.
|
package/README.md
CHANGED
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|

|
|
4
4
|
|
|
5
|
-
Telegram DM bridge for pi.
|
|
5
|
+
Better Telegram DM bridge for pi.
|
|
6
6
|
|
|
7
|
-
This repository is
|
|
8
|
-
It started from upstream commit [`cb34008460b6c1ca036d92322f69d87f626be0fc`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
7
|
+
This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008460b6c1ca036d92322f69d87f626be0fc`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
|
|
9
8
|
|
|
10
9
|
## Start Here
|
|
11
10
|
|
|
@@ -14,19 +13,15 @@ It started from upstream commit [`cb34008460b6c1ca036d92322f69d87f626be0fc`](htt
|
|
|
14
13
|
- [Changelog](./CHANGELOG.md)
|
|
15
14
|
- [Documentation](./docs/README.md)
|
|
16
15
|
|
|
17
|
-
##
|
|
16
|
+
## Key Features
|
|
18
17
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
24
|
-
-
|
|
25
|
-
- Streaming
|
|
26
|
-
- General runtime polish, bug fixes, and refactors across pairing, command handling, and Telegram session behavior
|
|
27
|
-
- Cleaner internal domain layout, with flat `/lib/*.ts` modules and mirrored `/tests/*.test.ts` suites that use repo-scoped domain names
|
|
28
|
-
|
|
29
|
-
In short: this fork is no longer just a repackaged copy of upstream; it is a feature-expanded and bug-fixed Telegram frontend for pi.
|
|
18
|
+
- **Priority Command Queue**: Control commands such as `/status` and `/model` use a high-priority control queue, so they do not get stuck behind normal queued prompts when pi is busy.
|
|
19
|
+
- **Interactive UI**: Manage your session directly from Telegram. Inline buttons allow you to switch models and adjust reasoning (thinking) levels on the fly.
|
|
20
|
+
- **In-flight Model Switching**: Change the active model mid-generation. The agent gracefully pauses, applies the new model, and restarts its response without losing context.
|
|
21
|
+
- **Smart Message Queue**: Messages sent while the agent is busy are queued and previewed in the pi status bar, and queued turns can be reprioritized or removed with Telegram reactions.
|
|
22
|
+
- **Mobile-Optimized Rendering**: Tables and lists are formatted for narrow screens. Markdown is correctly parsed and split to fit Telegram's limits without breaking HTML structures or code blocks, block spacing stays faithful to the original Markdown with readable heading separation, supported absolute links stay clickable, and unsupported link forms degrade safely.
|
|
23
|
+
- **File Handling & Attachments**: Send images and files to the agent, or ask it to generate and return artifacts. Outbound files are delivered automatically via the `telegram_attach` tool.
|
|
24
|
+
- **Streaming Responses**: Closed Markdown blocks stream back as rich Telegram HTML while pi is generating, and the still-growing tail stays readable until the final fully rendered reply lands.
|
|
30
25
|
|
|
31
26
|
## Install
|
|
32
27
|
|
|
@@ -42,22 +37,16 @@ From git:
|
|
|
42
37
|
pi install git:github.com/llblab/pi-telegram
|
|
43
38
|
```
|
|
44
39
|
|
|
45
|
-
Or for a single run:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
48
|
-
pi -e @llblab/pi-telegram
|
|
49
|
-
```
|
|
50
|
-
|
|
51
40
|
## Configure
|
|
52
41
|
|
|
53
|
-
### Telegram
|
|
42
|
+
### 1. Telegram Bot
|
|
54
43
|
|
|
55
44
|
1. Open [@BotFather](https://t.me/BotFather)
|
|
56
45
|
2. Run `/newbot`
|
|
57
46
|
3. Pick a name and username
|
|
58
47
|
4. Copy the bot token
|
|
59
48
|
|
|
60
|
-
### pi
|
|
49
|
+
### 2. Connect to pi
|
|
61
50
|
|
|
62
51
|
Start pi, then run:
|
|
63
52
|
|
|
@@ -65,132 +54,59 @@ Start pi, then run:
|
|
|
65
54
|
/telegram-setup
|
|
66
55
|
```
|
|
67
56
|
|
|
68
|
-
Paste
|
|
69
|
-
If a bot token is already saved in `~/.pi/agent/telegram.json`, `/telegram-setup` shows that stored value by default. Otherwise it pre-fills from the first configured environment variable in `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`.
|
|
57
|
+
Paste your bot token when prompted. If a bot token is already saved in `~/.pi/agent/telegram.json`, `/telegram-setup` shows that stored value by default. Otherwise it prefills from the first configured environment variable in `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`.
|
|
70
58
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
```text
|
|
74
|
-
~/.pi/agent/telegram.json
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Connect a pi session
|
|
78
|
-
|
|
79
|
-
The Telegram bridge is session-local. Connect it only in the pi session that should own the bot:
|
|
59
|
+
Link the bridge to your current pi session:
|
|
80
60
|
|
|
81
61
|
```bash
|
|
82
62
|
/telegram-connect
|
|
83
63
|
```
|
|
84
64
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
```bash
|
|
88
|
-
/telegram-disconnect
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
Check status:
|
|
92
|
-
|
|
93
|
-
```bash
|
|
94
|
-
/telegram-status
|
|
95
|
-
```
|
|
65
|
+
_(Note: The bridge is session-local. Only one pi session can be connected to the bot at a time.)_
|
|
96
66
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
After token setup and `/telegram-connect`:
|
|
67
|
+
### 3. Pair your account
|
|
100
68
|
|
|
101
69
|
1. Open the DM with your bot in Telegram
|
|
102
70
|
2. Send `/start`
|
|
103
71
|
|
|
104
|
-
The first
|
|
72
|
+
The first user to message the bot becomes the exclusive owner of the bridge. The extension will only accept messages from this user.
|
|
105
73
|
|
|
106
74
|
## Usage
|
|
107
75
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
Additional fork-specific controls:
|
|
111
|
-
|
|
112
|
-
- `/status` now has a richer view with inline buttons for model and thinking controls, and joins the high-priority control queue when pi is busy
|
|
113
|
-
- `/model` opens the interactive model selector, applies idle selections immediately, joins the high-priority control queue when pi is busy, and can restart the active Telegram-owned run on the newly selected model, waiting for the current tool call to finish when needed
|
|
114
|
-
- `/compact` starts session compaction when pi and the Telegram queue are idle
|
|
115
|
-
- Queue reactions: `👍` prioritizes a waiting turn, `👎` removes it
|
|
116
|
-
|
|
117
|
-
### Send text
|
|
118
|
-
|
|
119
|
-
Send any message in the bot DM. It is forwarded into pi with a `[telegram]` prefix.
|
|
76
|
+
Once paired, simply chat with your bot in Telegram. All text, images, and files are forwarded to pi.
|
|
120
77
|
|
|
121
|
-
###
|
|
78
|
+
### Commands & Controls
|
|
122
79
|
|
|
123
|
-
|
|
80
|
+
- **`/status`**: View session stats, cost, and use inline buttons to change models.
|
|
81
|
+
- **`/model`**: Open the interactive model selector.
|
|
82
|
+
- **`/compact`**: Start session compaction (only works when the session is idle).
|
|
83
|
+
- **`/stop`**: Abort the active run.
|
|
84
|
+
- **`/telegram-disconnect`** (in pi): Stop polling in the current session.
|
|
85
|
+
- **`/telegram-status`** (in pi): Check bridge status.
|
|
124
86
|
|
|
125
|
-
|
|
87
|
+
### Queue, Reactions, and Media
|
|
126
88
|
|
|
127
|
-
-
|
|
128
|
-
-
|
|
129
|
-
-
|
|
89
|
+
- If you send more Telegram messages while pi is busy, they are queued and processed in order.
|
|
90
|
+
- `👍` moves a waiting turn into the priority block. Removing `👍` sends it back to its normal queue position, and adding `👍` again gives it a fresh priority position.
|
|
91
|
+
- `👎` removes a waiting turn from the queue. Telegram Bot API does not expose ordinary DM message-deletion events through the polling path used here, so queue removal is bound to the dislike reaction.
|
|
92
|
+
- For media groups, a reaction on any message in the group applies to the whole queued turn.
|
|
93
|
+
- Inbound images, albums, and files are downloaded to `~/.pi/agent/tmp/telegram`, local file paths are included in the prompt, and inbound images are forwarded to pi as image inputs.
|
|
94
|
+
- Queue reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
|
|
130
95
|
|
|
131
|
-
###
|
|
96
|
+
### Requesting Files
|
|
132
97
|
|
|
133
|
-
If you ask pi for a file or generated artifact, pi
|
|
98
|
+
If you ask pi for a file or generated artifact (e.g., _"generate a shell script and attach it"_), pi will call the `telegram_attach` tool, and the extension will send the file alongside its next Telegram reply.
|
|
134
99
|
|
|
135
100
|
Examples:
|
|
136
101
|
|
|
137
102
|
- `summarize this image`
|
|
138
|
-
- `read this README and summarize it`
|
|
139
|
-
- `write me a markdown file with the plan and send it back`
|
|
140
103
|
- `generate a shell script and attach it`
|
|
141
104
|
|
|
142
|
-
### Stop a run
|
|
143
|
-
|
|
144
|
-
In Telegram, send:
|
|
145
|
-
|
|
146
|
-
```text
|
|
147
|
-
stop
|
|
148
|
-
```
|
|
149
|
-
|
|
150
|
-
or:
|
|
151
|
-
|
|
152
|
-
```text
|
|
153
|
-
/stop
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
That aborts the active pi turn.
|
|
157
|
-
|
|
158
|
-
### Queue follow-ups
|
|
159
|
-
|
|
160
|
-
If you send more Telegram messages while pi is busy, they are queued and processed in order.
|
|
161
|
-
|
|
162
|
-
The pi status bar shows queued Telegram turns as compact previews, for example:
|
|
163
|
-
|
|
164
|
-
```text
|
|
165
|
-
+3: [⬆ write a shell script…, summarize this image…, 📎 2 attachments]
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Priority turns promoted with 👍 are marked with `⬆` in that queue preview.
|
|
169
|
-
|
|
170
|
-
Each preview is limited to at most 4 words or 32 characters.
|
|
171
|
-
|
|
172
|
-
### Reprioritize or discard queued messages
|
|
173
|
-
|
|
174
|
-
While a message is still waiting in the queue:
|
|
175
|
-
|
|
176
|
-
- React with 👍 to move it into the priority block
|
|
177
|
-
- React with 👎 to remove it from the queue
|
|
178
|
-
|
|
179
|
-
Priority is stable:
|
|
180
|
-
|
|
181
|
-
- The first liked queued message stays ahead of later liked messages
|
|
182
|
-
- Removing 👍 sends the message back to its normal queue position
|
|
183
|
-
- Adding 👍 again gives it a fresh priority position
|
|
184
|
-
|
|
185
|
-
For media groups, a reaction on any message in the group applies to the whole queued turn.
|
|
186
|
-
|
|
187
|
-
Message reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
|
|
188
|
-
|
|
189
105
|
## Streaming
|
|
190
106
|
|
|
191
|
-
The extension streams assistant
|
|
107
|
+
The extension streams assistant previews back to Telegram while pi is generating.
|
|
192
108
|
|
|
193
|
-
|
|
109
|
+
Rich previews are sent through editable messages because Telegram drafts are text-only. Closed top-level Markdown blocks can appear with formatting before the answer finishes, while the still-growing tail remains conservative and readable until the preview is replaced with the fully rendered Telegram HTML reply.
|
|
194
110
|
|
|
195
111
|
## Notes
|
|
196
112
|
|
package/docs/architecture.md
CHANGED
|
@@ -21,7 +21,9 @@ Current runtime areas include:
|
|
|
21
21
|
|
|
22
22
|
- Telegram API types and local bridge state in `index.ts`
|
|
23
23
|
- Queueing and queue-runtime helpers in `/lib/queue.ts`
|
|
24
|
-
-
|
|
24
|
+
- Preview transport-selection, preview-finalization, and preview-runtime helpers in `/lib/preview.ts`
|
|
25
|
+
- Reply-transport and rendered-message delivery helpers in `/lib/replies.ts`
|
|
26
|
+
- Preview appearance and snapshot derivation stay in `/lib/rendering.ts`, while `/lib/preview.ts` owns transport and lifecycle decisions, so richer preview strategies can evolve without entangling Markdown formatting with Telegram delivery state
|
|
25
27
|
- Polling request, stop-condition, and long-poll loop helpers in `/lib/polling.ts`
|
|
26
28
|
- Telegram API/config helpers and lazy bot-token client wrappers in `/lib/api.ts`
|
|
27
29
|
- Telegram turn-building helpers in `/lib/turns.ts`
|
|
@@ -30,7 +32,7 @@ Current runtime areas include:
|
|
|
30
32
|
- Telegram attachment queueing and delivery helpers in `/lib/attachments.ts`
|
|
31
33
|
- Telegram tool, command, and lifecycle-hook registration helpers in `/lib/registration.ts`
|
|
32
34
|
- Setup/token prompt helpers in `/lib/setup.ts`
|
|
33
|
-
- Markdown and Telegram message rendering helpers in `/lib/rendering.ts`
|
|
35
|
+
- Markdown, preview-snapshot, and Telegram message rendering helpers in `/lib/rendering.ts`
|
|
34
36
|
- Status rendering helpers in `/lib/status.ts`
|
|
35
37
|
- Menu/model-resolution, menu-state construction, pure menu-page derivation, pure menu render-payload builders, menu-message runtime, callback parsing, callback entry handling, callback mutation helpers, full model-callback planning and execution, interface-polished callback effect ports, status-thinking callback handling, and UI helpers in `/lib/menu.ts`
|
|
36
38
|
- Model-switch guard, continuation, and restart helpers in `/lib/model-switch.ts`
|
|
@@ -94,12 +96,15 @@ Key rules:
|
|
|
94
96
|
|
|
95
97
|
- Rich text should render cleanly in Telegram chats
|
|
96
98
|
- Real code blocks must remain literal and escaped
|
|
99
|
+
- Supported absolute HTTP(S) and mailto links should stay clickable, while unsupported link forms such as unresolved references, footnotes, or relative links without a known base should degrade safely instead of producing broken Telegram anchors
|
|
97
100
|
- Markdown tables should keep their internal separators but drop the outer left and right borders when rendered as monospace blocks so narrow Telegram clients keep more usable width
|
|
98
101
|
- Unordered Markdown lists should render with a monospace `-` marker and ordered Markdown lists should render with monospace numeric markers so list indentation stays more predictable on narrow Telegram clients
|
|
102
|
+
- Real Markdown task-list items should render with checkbox markers, while standalone `[x]` and `[ ]` prose should stay literal instead of being reinterpreted as checklists
|
|
99
103
|
- Nested Markdown quotes should flatten into one Telegram blockquote with added non-breaking-space indentation because Telegram does not render nested blockquotes reliably
|
|
104
|
+
- Original blank-line spacing between Markdown blocks should stay intact in both preview and final rendering instead of being collapsed to one generic block separator, while headings should still keep readable separation from following blocks such as code fences even when source Markdown omits a blank line
|
|
100
105
|
- Long replies must be split below Telegram's 4096-character limit
|
|
101
106
|
- Chunking should avoid breaking HTML structure where possible
|
|
102
|
-
- Preview rendering
|
|
107
|
+
- Preview rendering uses stable top-level Markdown blocks for rich Telegram HTML and appends the still-growing tail conservatively as readable plain text so the preview stays valid even when the answer is incomplete
|
|
103
108
|
|
|
104
109
|
The renderer is a Telegram-specific formatter, not a general Markdown engine, so rendering changes should be treated as regression-prone.
|
|
105
110
|
|
|
@@ -109,10 +114,12 @@ During generation, the bridge streams previews back to Telegram.
|
|
|
109
114
|
|
|
110
115
|
Preferred order:
|
|
111
116
|
|
|
112
|
-
1.
|
|
113
|
-
2.
|
|
117
|
+
1. Re-render the current Markdown buffer into a preview snapshot that renders closed top-level blocks as rich Telegram HTML and keeps the unstable tail conservative and readable
|
|
118
|
+
2. Send or update that preview through `sendMessage` plus `editMessageText`, because `sendMessageDraft` is text-only for rich previews
|
|
114
119
|
3. Replace the preview with the final rendered reply when generation ends
|
|
115
120
|
|
|
121
|
+
Draft streaming can remain as a plain-text fallback path, but rich Telegram previews are driven through editable messages and stable-block snapshot selection.
|
|
122
|
+
|
|
116
123
|
Outbound files are sent only after the active Telegram turn completes and must be staged through the `telegram_attach` tool.
|
|
117
124
|
|
|
118
125
|
## Interactive Controls
|
|
@@ -125,7 +132,7 @@ Current operator controls include:
|
|
|
125
132
|
- Inline status buttons for model and thinking adjustments, applying idle selections immediately while still respecting busy-run restart rules
|
|
126
133
|
- `/model` for interactive model selection, queued as a high-priority control item when needed and supporting in-flight restart of the active Telegram-owned run on a newly selected model
|
|
127
134
|
- `/compact` for Telegram-triggered pi session compaction when the bridge is idle
|
|
128
|
-
- Queue reactions using `👍` and `👎`
|
|
135
|
+
- Queue reactions using `👍` and `👎`, with `👎` acting as the canonical queue-removal path because ordinary Telegram DM message deletions are not exposed through the Bot API polling path this bridge uses
|
|
129
136
|
|
|
130
137
|
## In-Flight Model Switching
|
|
131
138
|
|
package/index.ts
CHANGED
|
@@ -87,11 +87,14 @@ import {
|
|
|
87
87
|
type TelegramRenderMode,
|
|
88
88
|
} from "./lib/rendering.ts";
|
|
89
89
|
import {
|
|
90
|
-
buildTelegramReplyTransport,
|
|
91
90
|
clearTelegramPreview,
|
|
92
91
|
finalizeTelegramMarkdownPreview,
|
|
93
92
|
finalizeTelegramPreview,
|
|
94
93
|
flushTelegramPreview,
|
|
94
|
+
type TelegramPreviewRuntimeState,
|
|
95
|
+
} from "./lib/preview.ts";
|
|
96
|
+
import {
|
|
97
|
+
buildTelegramReplyTransport,
|
|
95
98
|
sendTelegramMarkdownReply,
|
|
96
99
|
sendTelegramPlainReply,
|
|
97
100
|
} from "./lib/replies.ts";
|
|
@@ -235,14 +238,12 @@ interface TelegramMessageReactionUpdated {
|
|
|
235
238
|
}
|
|
236
239
|
|
|
237
240
|
interface TelegramUpdate {
|
|
238
|
-
_: string;
|
|
239
241
|
update_id: number;
|
|
240
242
|
message?: TelegramMessage;
|
|
241
243
|
edited_message?: TelegramMessage;
|
|
242
244
|
callback_query?: TelegramCallbackQuery;
|
|
243
245
|
message_reaction?: TelegramMessageReactionUpdated;
|
|
244
246
|
deleted_business_messages?: { message_ids?: unknown };
|
|
245
|
-
messages?: unknown;
|
|
246
247
|
}
|
|
247
248
|
|
|
248
249
|
interface TelegramGetFileResult {
|
|
@@ -269,14 +270,7 @@ interface DownloadedTelegramFile {
|
|
|
269
270
|
|
|
270
271
|
type ActiveTelegramTurn = PendingTelegramTurn;
|
|
271
272
|
|
|
272
|
-
|
|
273
|
-
mode: "draft" | "message";
|
|
274
|
-
draftId?: number;
|
|
275
|
-
messageId?: number;
|
|
276
|
-
pendingText: string;
|
|
277
|
-
lastSentText: string;
|
|
278
|
-
flushTimer?: ReturnType<typeof setTimeout>;
|
|
279
|
-
}
|
|
273
|
+
type TelegramPreviewState = TelegramPreviewRuntimeState;
|
|
280
274
|
|
|
281
275
|
interface TelegramMediaGroupState {
|
|
282
276
|
messages: TelegramMessage[];
|
|
@@ -693,21 +687,28 @@ export default function (pi: ExtensionAPI) {
|
|
|
693
687
|
text,
|
|
694
688
|
});
|
|
695
689
|
},
|
|
696
|
-
sendMessage: async (
|
|
690
|
+
sendMessage: async (
|
|
691
|
+
chatId: number,
|
|
692
|
+
text: string,
|
|
693
|
+
options?: { parseMode?: "HTML" },
|
|
694
|
+
) => {
|
|
697
695
|
return callTelegramApi<TelegramSentMessage>("sendMessage", {
|
|
698
696
|
chat_id: chatId,
|
|
699
697
|
text,
|
|
698
|
+
parse_mode: options?.parseMode,
|
|
700
699
|
});
|
|
701
700
|
},
|
|
702
701
|
editMessageText: async (
|
|
703
702
|
chatId: number,
|
|
704
703
|
messageId: number,
|
|
705
704
|
text: string,
|
|
705
|
+
options?: { parseMode?: "HTML" },
|
|
706
706
|
) => {
|
|
707
707
|
await editTelegramMessageText({
|
|
708
708
|
chat_id: chatId,
|
|
709
709
|
message_id: messageId,
|
|
710
710
|
text,
|
|
711
|
+
parse_mode: options?.parseMode,
|
|
711
712
|
});
|
|
712
713
|
},
|
|
713
714
|
renderTelegramMessage,
|
package/lib/polling.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface TelegramUpdateLike {
|
|
|
11
11
|
update_id: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
// Standard Telegram DM polling does not expose ordinary message-deletion events,
|
|
15
|
+
// so queue removal stays reaction-driven while delete-like business updates remain defensive-only.
|
|
14
16
|
export const TELEGRAM_ALLOWED_UPDATES = [
|
|
15
17
|
"message",
|
|
16
18
|
"edited_message",
|
package/lib/preview.ts
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram preview streaming helpers
|
|
3
|
+
* Owns preview transport selection, runtime updates, and preview finalization
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
buildTelegramPreviewSnapshot,
|
|
8
|
+
type TelegramPreviewRenderStrategy,
|
|
9
|
+
type TelegramPreviewSnapshot,
|
|
10
|
+
type TelegramRenderedChunk,
|
|
11
|
+
type TelegramRenderMode,
|
|
12
|
+
} from "./rendering.ts";
|
|
13
|
+
|
|
14
|
+
export interface TelegramPreviewStateLike {
|
|
15
|
+
mode: "draft" | "message";
|
|
16
|
+
draftId?: number;
|
|
17
|
+
messageId?: number;
|
|
18
|
+
pendingText: string;
|
|
19
|
+
lastSentText: string;
|
|
20
|
+
lastSentParseMode?: "HTML";
|
|
21
|
+
lastSentStrategy?: TelegramPreviewRenderStrategy;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TelegramPreviewRuntimeState extends TelegramPreviewStateLike {
|
|
25
|
+
flushTimer?: ReturnType<typeof setTimeout>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TelegramSentPreviewMessageLike {
|
|
29
|
+
message_id: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface TelegramPreviewRuntimeDeps {
|
|
33
|
+
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
34
|
+
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
35
|
+
clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
|
|
36
|
+
maxMessageLength: number;
|
|
37
|
+
renderPreviewText: (markdown: string) => string;
|
|
38
|
+
getDraftSupport: () => "unknown" | "supported" | "unsupported";
|
|
39
|
+
setDraftSupport: (support: "unknown" | "supported" | "unsupported") => void;
|
|
40
|
+
allocateDraftId: () => number;
|
|
41
|
+
sendDraft: (chatId: number, draftId: number, text: string) => Promise<void>;
|
|
42
|
+
sendMessage: (
|
|
43
|
+
chatId: number,
|
|
44
|
+
text: string,
|
|
45
|
+
options?: { parseMode?: "HTML" },
|
|
46
|
+
) => Promise<TelegramSentPreviewMessageLike>;
|
|
47
|
+
editMessageText: (
|
|
48
|
+
chatId: number,
|
|
49
|
+
messageId: number,
|
|
50
|
+
text: string,
|
|
51
|
+
options?: { parseMode?: "HTML" },
|
|
52
|
+
) => Promise<void>;
|
|
53
|
+
renderTelegramMessage: (
|
|
54
|
+
text: string,
|
|
55
|
+
options?: { mode?: TelegramRenderMode },
|
|
56
|
+
) => TelegramRenderedChunk[];
|
|
57
|
+
sendRenderedChunks: (
|
|
58
|
+
chatId: number,
|
|
59
|
+
chunks: TelegramRenderedChunk[],
|
|
60
|
+
) => Promise<number | undefined>;
|
|
61
|
+
editRenderedMessage: (
|
|
62
|
+
chatId: number,
|
|
63
|
+
messageId: number,
|
|
64
|
+
chunks: TelegramRenderedChunk[],
|
|
65
|
+
) => Promise<number | undefined>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildTelegramPreviewFinalText(
|
|
69
|
+
state: TelegramPreviewStateLike,
|
|
70
|
+
): string | undefined {
|
|
71
|
+
const finalText = state.pendingText.trim();
|
|
72
|
+
if (finalText) return finalText;
|
|
73
|
+
if (
|
|
74
|
+
state.lastSentStrategy === "rich-stable-blocks" ||
|
|
75
|
+
state.lastSentParseMode === "HTML"
|
|
76
|
+
) {
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
return state.lastSentText.trim() || undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function shouldUseTelegramDraftPreview(options: {
|
|
83
|
+
draftSupport: "unknown" | "supported" | "unsupported";
|
|
84
|
+
snapshot?: TelegramPreviewSnapshot;
|
|
85
|
+
}): boolean {
|
|
86
|
+
return (
|
|
87
|
+
options.draftSupport !== "unsupported" &&
|
|
88
|
+
(options.snapshot === undefined || options.snapshot.strategy === "plain")
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export async function clearTelegramPreview(
|
|
93
|
+
chatId: number,
|
|
94
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const state = deps.getState();
|
|
97
|
+
if (!state) return;
|
|
98
|
+
deps.clearScheduledFlush(state);
|
|
99
|
+
deps.setState(undefined);
|
|
100
|
+
if (state.mode !== "draft" || state.draftId === undefined) return;
|
|
101
|
+
try {
|
|
102
|
+
await deps.sendDraft(chatId, state.draftId, "");
|
|
103
|
+
} catch {
|
|
104
|
+
// ignore
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function flushTelegramPreview(
|
|
109
|
+
chatId: number,
|
|
110
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const state = deps.getState();
|
|
113
|
+
if (!state) return;
|
|
114
|
+
state.flushTimer = undefined;
|
|
115
|
+
const snapshot = buildTelegramPreviewSnapshot({
|
|
116
|
+
state,
|
|
117
|
+
maxMessageLength: deps.maxMessageLength,
|
|
118
|
+
renderPreviewText: deps.renderPreviewText,
|
|
119
|
+
renderTelegramMessage: deps.renderTelegramMessage,
|
|
120
|
+
});
|
|
121
|
+
if (!snapshot) return;
|
|
122
|
+
if (
|
|
123
|
+
shouldUseTelegramDraftPreview({
|
|
124
|
+
draftSupport: deps.getDraftSupport(),
|
|
125
|
+
snapshot,
|
|
126
|
+
})
|
|
127
|
+
) {
|
|
128
|
+
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
129
|
+
state.draftId = draftId;
|
|
130
|
+
try {
|
|
131
|
+
await deps.sendDraft(chatId, draftId, snapshot.text);
|
|
132
|
+
deps.setDraftSupport("supported");
|
|
133
|
+
state.mode = "draft";
|
|
134
|
+
state.lastSentText = snapshot.text;
|
|
135
|
+
state.lastSentParseMode = snapshot.parseMode;
|
|
136
|
+
state.lastSentStrategy = snapshot.strategy;
|
|
137
|
+
return;
|
|
138
|
+
} catch {
|
|
139
|
+
deps.setDraftSupport("unsupported");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
143
|
+
try {
|
|
144
|
+
await deps.sendDraft(chatId, state.draftId, "");
|
|
145
|
+
} catch {
|
|
146
|
+
// ignore
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (state.messageId === undefined) {
|
|
150
|
+
const sent = await deps.sendMessage(chatId, snapshot.text, {
|
|
151
|
+
parseMode: snapshot.parseMode,
|
|
152
|
+
});
|
|
153
|
+
state.messageId = sent.message_id;
|
|
154
|
+
state.mode = "message";
|
|
155
|
+
state.lastSentText = snapshot.text;
|
|
156
|
+
state.lastSentParseMode = snapshot.parseMode;
|
|
157
|
+
state.lastSentStrategy = snapshot.strategy;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
await deps.editMessageText(chatId, state.messageId, snapshot.text, {
|
|
161
|
+
parseMode: snapshot.parseMode,
|
|
162
|
+
});
|
|
163
|
+
state.mode = "message";
|
|
164
|
+
state.lastSentText = snapshot.text;
|
|
165
|
+
state.lastSentParseMode = snapshot.parseMode;
|
|
166
|
+
state.lastSentStrategy = snapshot.strategy;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export async function finalizeTelegramPreview(
|
|
170
|
+
chatId: number,
|
|
171
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
172
|
+
): Promise<boolean> {
|
|
173
|
+
const state = deps.getState();
|
|
174
|
+
if (!state) return false;
|
|
175
|
+
await flushTelegramPreview(chatId, deps);
|
|
176
|
+
const finalText = buildTelegramPreviewFinalText(state);
|
|
177
|
+
if (!finalText) {
|
|
178
|
+
await clearTelegramPreview(chatId, deps);
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
if (state.mode === "draft") {
|
|
182
|
+
await deps.sendMessage(chatId, finalText);
|
|
183
|
+
await clearTelegramPreview(chatId, deps);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
deps.setState(undefined);
|
|
187
|
+
return state.messageId !== undefined;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function finalizeTelegramMarkdownPreview(
|
|
191
|
+
chatId: number,
|
|
192
|
+
markdown: string,
|
|
193
|
+
deps: TelegramPreviewRuntimeDeps,
|
|
194
|
+
): Promise<boolean> {
|
|
195
|
+
const state = deps.getState();
|
|
196
|
+
if (!state) return false;
|
|
197
|
+
await flushTelegramPreview(chatId, deps);
|
|
198
|
+
const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
|
|
199
|
+
if (chunks.length === 0) {
|
|
200
|
+
await clearTelegramPreview(chatId, deps);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
if (state.mode === "draft") {
|
|
204
|
+
await deps.sendRenderedChunks(chatId, chunks);
|
|
205
|
+
await clearTelegramPreview(chatId, deps);
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
if (state.messageId === undefined) return false;
|
|
209
|
+
await deps.editRenderedMessage(chatId, state.messageId, chunks);
|
|
210
|
+
deps.setState(undefined);
|
|
211
|
+
return true;
|
|
212
|
+
}
|