@llblab/pi-telegram 0.2.6 → 0.2.8
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 +2 -0
- package/README.md +33 -115
- package/docs/architecture.md +3 -2
- package/index.ts +0 -2
- package/lib/polling.ts +2 -0
- package/lib/queue.ts +8 -1
- package/lib/rendering.ts +33 -13
- package/lib/updates.ts +0 -8
- package/package.json +3 -2
- package/screenshot.png +0 -0
- package/tests/queue.test.ts +52 -0
- package/tests/rendering.test.ts +45 -0
- package/tests/updates.test.ts +5 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
## Current
|
|
4
4
|
|
|
5
|
+
- `[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.
|
|
6
|
+
- `[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.
|
|
5
7
|
- `[Docs]` Added short responsibility header comments to every project `.ts` file. Impact: file boundaries are easier to understand while navigating the growing `/lib` split.
|
|
6
8
|
- `[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.
|
|
7
9
|
- `[Controls]` Expanded Telegram session controls with a richer `/status` view, inline model selection, and thinking-level controls, and fixed the callback-selection path so idle model and thinking picks apply immediately instead of only becoming visible after a later Telegram interaction. Impact: more bridge configuration can be managed directly from Telegram with more predictable immediate feedback.
|
package/README.md
CHANGED
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
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 instead of redundant `telegram-*` filename prefixes
|
|
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.
|
|
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**: Smooth, real-time typing effect using Telegram Drafts (or message edits as a fallback).
|
|
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,125 +54,54 @@ 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`** (or just **`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: [summarize this image…, write a shell script…, 📎 2 attachments]
|
|
166
|
-
```
|
|
167
|
-
|
|
168
|
-
Each preview is limited to at most 4 words or 32 characters.
|
|
169
|
-
|
|
170
|
-
### Reprioritize or discard queued messages
|
|
171
|
-
|
|
172
|
-
While a message is still waiting in the queue:
|
|
173
|
-
|
|
174
|
-
- React with 👍 to move it into the priority block
|
|
175
|
-
- React with 👎 to remove it from the queue
|
|
176
|
-
|
|
177
|
-
Priority is stable:
|
|
178
|
-
|
|
179
|
-
- The first liked queued message stays ahead of later liked messages
|
|
180
|
-
- Removing 👍 sends the message back to its normal queue position
|
|
181
|
-
- Adding 👍 again gives it a fresh priority position
|
|
182
|
-
|
|
183
|
-
For media groups, a reaction on any message in the group applies to the whole queued turn.
|
|
184
|
-
|
|
185
|
-
Message reactions depend on Telegram delivering `message_reaction` updates for your bot and chat type.
|
|
186
|
-
|
|
187
105
|
## Streaming
|
|
188
106
|
|
|
189
107
|
The extension streams assistant text previews back to Telegram while pi is generating.
|
package/docs/architecture.md
CHANGED
|
@@ -68,7 +68,7 @@ Queued items now use two explicit dimensions:
|
|
|
68
68
|
- `kind`: prompt vs control
|
|
69
69
|
- `queueLane`: control vs priority vs default
|
|
70
70
|
|
|
71
|
-
This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output.
|
|
71
|
+
This lets synthetic control actions and Telegram prompts share one stable ordering model while still rendering distinctly in status output. In the pi status bar queue preview, priority prompts are marked with `⬆` while control items keep their own control-specific summary markers such as `⚡`.
|
|
72
72
|
|
|
73
73
|
A dispatched prompt remains in the queue until `agent_start` consumes it. That keeps the active Telegram turn bound correctly for previews, attachments, abort handling, and final reply delivery.
|
|
74
74
|
|
|
@@ -96,6 +96,7 @@ Key rules:
|
|
|
96
96
|
- Real code blocks must remain literal and escaped
|
|
97
97
|
- 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
98
|
- 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
|
|
99
|
+
- 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
100
|
- Nested Markdown quotes should flatten into one Telegram blockquote with added non-breaking-space indentation because Telegram does not render nested blockquotes reliably
|
|
100
101
|
- Long replies must be split below Telegram's 4096-character limit
|
|
101
102
|
- Chunking should avoid breaking HTML structure where possible
|
|
@@ -125,7 +126,7 @@ Current operator controls include:
|
|
|
125
126
|
- Inline status buttons for model and thinking adjustments, applying idle selections immediately while still respecting busy-run restart rules
|
|
126
127
|
- `/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
128
|
- `/compact` for Telegram-triggered pi session compaction when the bridge is idle
|
|
128
|
-
- Queue reactions using `👍` and `👎`
|
|
129
|
+
- 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
130
|
|
|
130
131
|
## In-Flight Model Switching
|
|
131
132
|
|
package/index.ts
CHANGED
|
@@ -235,14 +235,12 @@ interface TelegramMessageReactionUpdated {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
interface TelegramUpdate {
|
|
238
|
-
_: string;
|
|
239
238
|
update_id: number;
|
|
240
239
|
message?: TelegramMessage;
|
|
241
240
|
edited_message?: TelegramMessage;
|
|
242
241
|
callback_query?: TelegramCallbackQuery;
|
|
243
242
|
message_reaction?: TelegramMessageReactionUpdated;
|
|
244
243
|
deleted_business_messages?: { message_ids?: unknown };
|
|
245
|
-
messages?: unknown;
|
|
246
244
|
}
|
|
247
245
|
|
|
248
246
|
interface TelegramGetFileResult {
|
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/queue.ts
CHANGED
|
@@ -189,6 +189,13 @@ export function consumeDispatchedTelegramPrompt(
|
|
|
189
189
|
return { activeTurn: nextItem, remainingItems: items.slice(1) };
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
+
function formatTelegramQueueItemStatusSummary(item: TelegramQueueItem): string {
|
|
193
|
+
if (item.queueLane === "priority") {
|
|
194
|
+
return `⬆ ${item.statusSummary}`;
|
|
195
|
+
}
|
|
196
|
+
return item.statusSummary;
|
|
197
|
+
}
|
|
198
|
+
|
|
192
199
|
export function formatQueuedTelegramItemsStatus(
|
|
193
200
|
items: TelegramQueueItem[],
|
|
194
201
|
): string {
|
|
@@ -196,7 +203,7 @@ export function formatQueuedTelegramItemsStatus(
|
|
|
196
203
|
const previewCount = 4;
|
|
197
204
|
const summaries = items
|
|
198
205
|
.slice(0, previewCount)
|
|
199
|
-
.map(
|
|
206
|
+
.map(formatTelegramQueueItemStatusSummary)
|
|
200
207
|
.filter(Boolean);
|
|
201
208
|
if (summaries.length === 0) return ` +${items.length}`;
|
|
202
209
|
const suffix = items.length > summaries.length ? ", …" : "";
|
package/lib/rendering.ts
CHANGED
|
@@ -129,8 +129,25 @@ function stripIndentedCodePrefix(line: string): string {
|
|
|
129
129
|
return line;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
function normalizeMarkdownDocument(markdown: string): string {
|
|
133
|
+
const lines = markdown.replace(/\r\n/g, "\n").split("\n");
|
|
134
|
+
let start = 0;
|
|
135
|
+
while (start < lines.length && (lines[start] ?? "").trim().length === 0) {
|
|
136
|
+
start += 1;
|
|
137
|
+
}
|
|
138
|
+
let end = lines.length;
|
|
139
|
+
while (end > start && (lines[end - 1] ?? "").trim().length === 0) {
|
|
140
|
+
end -= 1;
|
|
141
|
+
}
|
|
142
|
+
return lines.slice(start, end).join("\n");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isMarkdownNumberedListMarker(marker: string): boolean {
|
|
146
|
+
return /^\d+\.$/.test(marker);
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
export function renderMarkdownPreviewText(markdown: string): string {
|
|
133
|
-
const normalized = markdown
|
|
150
|
+
const normalized = normalizeMarkdownDocument(markdown);
|
|
134
151
|
if (normalized.length === 0) return "";
|
|
135
152
|
const output: string[] = [];
|
|
136
153
|
const lines = normalized.split("\n");
|
|
@@ -169,9 +186,14 @@ export function renderMarkdownPreviewText(markdown: string): string {
|
|
|
169
186
|
const task = line.match(/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/);
|
|
170
187
|
if (task) {
|
|
171
188
|
const indent = " ".repeat((task[1] ?? "").length);
|
|
172
|
-
const
|
|
189
|
+
const listMarker = task[2] ?? "-";
|
|
190
|
+
const checkboxMarker =
|
|
191
|
+
(task[3] ?? " ").toLowerCase() === "x" ? "[x]" : "[ ]";
|
|
192
|
+
const taskPrefix = isMarkdownNumberedListMarker(listMarker)
|
|
193
|
+
? `${listMarker} ${checkboxMarker}`
|
|
194
|
+
: checkboxMarker;
|
|
173
195
|
output.push(
|
|
174
|
-
`${indent}${
|
|
196
|
+
`${indent}${taskPrefix} ${stripInlineMarkdownToPlainText(task[4] ?? "")}`,
|
|
175
197
|
);
|
|
176
198
|
continue;
|
|
177
199
|
}
|
|
@@ -275,13 +297,6 @@ function renderInlineMarkdown(text: string): string {
|
|
|
275
297
|
result = renderDelimitedInlineStyle(result, "_", (content) => {
|
|
276
298
|
return `<i>${content}</i>`;
|
|
277
299
|
});
|
|
278
|
-
result = result.replace(
|
|
279
|
-
/(^|[\s>(])(\[(?: |x|X)\])(?=($|[\s<).,:;!?]))/g,
|
|
280
|
-
(_match, prefix: string, checkbox: string) => {
|
|
281
|
-
const normalized = checkbox.toLowerCase() === "[x]" ? "[x]" : "[ ]";
|
|
282
|
-
return `${prefix}<code>${normalized}</code>`;
|
|
283
|
-
},
|
|
284
|
-
);
|
|
285
300
|
result = result.replace(/\\([\\`*_{}\[\]()#+\-.!>~|])/g, "$1");
|
|
286
301
|
return result.replace(
|
|
287
302
|
/\uE000(\d+)\uE001/g,
|
|
@@ -330,9 +345,14 @@ function renderMarkdownTextLines(block: string): string[] {
|
|
|
330
345
|
const task = piece.match(/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/);
|
|
331
346
|
if (task) {
|
|
332
347
|
const indent = buildListIndent(Math.floor((task[1] ?? "").length / 2));
|
|
333
|
-
const
|
|
348
|
+
const listMarker = task[2] ?? "-";
|
|
349
|
+
const checkboxMarker =
|
|
350
|
+
(task[3] ?? " ").toLowerCase() === "x" ? "[x]" : "[ ]";
|
|
351
|
+
const taskPrefix = isMarkdownNumberedListMarker(listMarker)
|
|
352
|
+
? `<code>${listMarker}</code> <code>${checkboxMarker}</code>`
|
|
353
|
+
: `<code>${checkboxMarker}</code>`;
|
|
334
354
|
rendered.push(
|
|
335
|
-
`${indent}
|
|
355
|
+
`${indent}${taskPrefix} ${renderInlineMarkdown(task[4] ?? "")}`,
|
|
336
356
|
);
|
|
337
357
|
continue;
|
|
338
358
|
}
|
|
@@ -498,7 +518,7 @@ function renderMarkdownQuoteBlock(lines: string[]): string[] {
|
|
|
498
518
|
}
|
|
499
519
|
|
|
500
520
|
function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] {
|
|
501
|
-
const normalized = markdown
|
|
521
|
+
const normalized = normalizeMarkdownDocument(markdown);
|
|
502
522
|
if (normalized.length === 0) return [];
|
|
503
523
|
const renderedBlocks: string[] = [];
|
|
504
524
|
const lines = normalized.split("\n");
|
package/lib/updates.ts
CHANGED
|
@@ -22,8 +22,6 @@ export type TelegramReactionTypeLike =
|
|
|
22
22
|
|
|
23
23
|
export interface TelegramUpdateLike {
|
|
24
24
|
deleted_business_messages?: { message_ids?: unknown };
|
|
25
|
-
_: string;
|
|
26
|
-
messages?: unknown;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
27
|
function isTelegramMessageIdList(value: unknown): value is number[] {
|
|
@@ -55,12 +53,6 @@ export function extractDeletedTelegramMessageIds(
|
|
|
55
53
|
if (isTelegramMessageIdList(deletedBusinessMessageIds)) {
|
|
56
54
|
return deletedBusinessMessageIds;
|
|
57
55
|
}
|
|
58
|
-
if (
|
|
59
|
-
update._ === "updateDeleteMessages" &&
|
|
60
|
-
isTelegramMessageIdList(update.messages)
|
|
61
|
-
) {
|
|
62
|
-
return update.messages;
|
|
63
|
-
}
|
|
64
56
|
return [];
|
|
65
57
|
}
|
|
66
58
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Better Telegram DM bridge extension for pi",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"pi": {
|
|
30
30
|
"extensions": [
|
|
31
31
|
"./index.ts"
|
|
32
|
-
]
|
|
32
|
+
],
|
|
33
|
+
"image": "https://github.com/llblab/pi-telegram/raw/main/screenshot.png"
|
|
33
34
|
},
|
|
34
35
|
"peerDependencies": {
|
|
35
36
|
"@mariozechner/pi-ai": "*",
|
package/screenshot.png
CHANGED
|
Binary file
|
package/tests/queue.test.ts
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
buildTelegramSessionStartState,
|
|
17
17
|
executeTelegramControlItemRuntime,
|
|
18
18
|
executeTelegramQueueDispatchPlan,
|
|
19
|
+
formatQueuedTelegramItemsStatus,
|
|
19
20
|
getNextTelegramToolExecutionCount,
|
|
20
21
|
shouldStartTelegramPolling,
|
|
21
22
|
} from "../lib/queue.ts";
|
|
@@ -169,6 +170,57 @@ test("Queue mutation helpers apply and clear prompt priority without touching co
|
|
|
169
170
|
assert.equal(cleared.items[1]?.queueLane, "control");
|
|
170
171
|
});
|
|
171
172
|
|
|
173
|
+
test("Queued status formatting marks priority prompts in the pi status bar", () => {
|
|
174
|
+
const queueItemType = undefined as
|
|
175
|
+
| Parameters<typeof formatQueuedTelegramItemsStatus>[0][number]
|
|
176
|
+
| undefined;
|
|
177
|
+
const priorityPrompt: typeof queueItemType = {
|
|
178
|
+
kind: "prompt",
|
|
179
|
+
chatId: 1,
|
|
180
|
+
replyToMessageId: 1,
|
|
181
|
+
sourceMessageIds: [11],
|
|
182
|
+
queueOrder: 4,
|
|
183
|
+
queueLane: "priority",
|
|
184
|
+
laneOrder: 0,
|
|
185
|
+
queuedAttachments: [],
|
|
186
|
+
content: [{ type: "text", text: "prompt" }],
|
|
187
|
+
historyText: "prompt history",
|
|
188
|
+
statusSummary: "prompt",
|
|
189
|
+
};
|
|
190
|
+
const defaultPrompt: typeof queueItemType = {
|
|
191
|
+
kind: "prompt",
|
|
192
|
+
chatId: 1,
|
|
193
|
+
replyToMessageId: 2,
|
|
194
|
+
sourceMessageIds: [12],
|
|
195
|
+
queueOrder: 5,
|
|
196
|
+
queueLane: "default",
|
|
197
|
+
laneOrder: 5,
|
|
198
|
+
queuedAttachments: [],
|
|
199
|
+
content: [{ type: "text", text: "default" }],
|
|
200
|
+
historyText: "default history",
|
|
201
|
+
statusSummary: "default",
|
|
202
|
+
};
|
|
203
|
+
const controlItem: typeof queueItemType = {
|
|
204
|
+
kind: "control",
|
|
205
|
+
controlType: "status",
|
|
206
|
+
chatId: 1,
|
|
207
|
+
replyToMessageId: 3,
|
|
208
|
+
queueOrder: 6,
|
|
209
|
+
queueLane: "control",
|
|
210
|
+
laneOrder: 0,
|
|
211
|
+
statusSummary: "⚡ status",
|
|
212
|
+
execute: async () => {},
|
|
213
|
+
};
|
|
214
|
+
assert.equal(
|
|
215
|
+
formatQueuedTelegramItemsStatus([
|
|
216
|
+
controlItem,
|
|
217
|
+
priorityPrompt,
|
|
218
|
+
defaultPrompt,
|
|
219
|
+
]),
|
|
220
|
+
" +3: [⚡ status, ⬆ prompt, default]",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
172
224
|
test("History partition keeps control items queued and extracts prompt items", () => {
|
|
173
225
|
const queueItemType = undefined as
|
|
174
226
|
| Parameters<
|
package/tests/rendering.test.ts
CHANGED
|
@@ -7,6 +7,7 @@ import assert from "node:assert/strict";
|
|
|
7
7
|
import test from "node:test";
|
|
8
8
|
|
|
9
9
|
import { __telegramTestUtils } from "../index.ts";
|
|
10
|
+
import { renderMarkdownPreviewText } from "../lib/rendering.ts";
|
|
10
11
|
|
|
11
12
|
test("Nested lists stay out of code blocks", () => {
|
|
12
13
|
const chunks = __telegramTestUtils.renderTelegramMessage(
|
|
@@ -68,6 +69,50 @@ test("Numbered lists use monospace numeric markers", () => {
|
|
|
68
69
|
assert.match(chunks[0]?.text ?? "", /<code>2\.<\/code> second/);
|
|
69
70
|
});
|
|
70
71
|
|
|
72
|
+
test("Ordered task lists preserve numeric markers in previews and final rendering", () => {
|
|
73
|
+
const markdown = "1. [x] first\n2. [ ] second";
|
|
74
|
+
assert.equal(renderMarkdownPreviewText(markdown), markdown);
|
|
75
|
+
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
|
76
|
+
mode: "markdown",
|
|
77
|
+
});
|
|
78
|
+
assert.equal(chunks.length, 1);
|
|
79
|
+
assert.match(
|
|
80
|
+
chunks[0]?.text ?? "",
|
|
81
|
+
/<code>1\.<\/code> <code>\[x\]<\/code> first/,
|
|
82
|
+
);
|
|
83
|
+
assert.match(
|
|
84
|
+
chunks[0]?.text ?? "",
|
|
85
|
+
/<code>2\.<\/code> <code>\[ \]<\/code> second/,
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("Leading indentation on the first markdown line stays intact", () => {
|
|
90
|
+
const markdown = " - nested bullet\n - nested child";
|
|
91
|
+
assert.equal(renderMarkdownPreviewText(markdown), markdown);
|
|
92
|
+
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
|
93
|
+
mode: "markdown",
|
|
94
|
+
});
|
|
95
|
+
assert.equal(chunks.length, 1);
|
|
96
|
+
assert.match(chunks[0]?.text ?? "", /^\u00A0\u00A0<code>-<\/code> nested bullet/m);
|
|
97
|
+
assert.match(
|
|
98
|
+
chunks[0]?.text ?? "",
|
|
99
|
+
/^\u00A0\u00A0\u00A0\u00A0<code>-<\/code> nested child/m,
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("Standalone checkbox-looking prose stays literal outside task lists", () => {
|
|
104
|
+
const markdown = "Use [ ] as a placeholder and keep [x] literal";
|
|
105
|
+
assert.equal(renderMarkdownPreviewText(markdown), markdown);
|
|
106
|
+
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
|
107
|
+
mode: "markdown",
|
|
108
|
+
});
|
|
109
|
+
assert.equal(chunks.length, 1);
|
|
110
|
+
assert.equal((chunks[0]?.text ?? "").includes("<code>[ ]</code>"), false);
|
|
111
|
+
assert.equal((chunks[0]?.text ?? "").includes("<code>[x]</code>"), false);
|
|
112
|
+
assert.match(chunks[0]?.text ?? "", /Use \[ \] as a placeholder/);
|
|
113
|
+
assert.match(chunks[0]?.text ?? "", /keep \[x\] literal/);
|
|
114
|
+
});
|
|
115
|
+
|
|
71
116
|
test("Nested blockquotes flatten into one Telegram blockquote with indentation", () => {
|
|
72
117
|
const chunks = __telegramTestUtils.renderTelegramMessage(
|
|
73
118
|
"> outer\n>> inner\n>>> deepest",
|
package/tests/updates.test.ts
CHANGED
|
@@ -30,28 +30,20 @@ test("Update helpers normalize emoji reactions and collect emoji-only entries",
|
|
|
30
30
|
assert.deepEqual([...emojis], ["👍", "👎"]);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
test("Update helpers extract deleted message ids from
|
|
33
|
+
test("Update helpers extract deleted business-message ids only from Bot API shapes", () => {
|
|
34
34
|
assert.deepEqual(
|
|
35
35
|
extractDeletedTelegramMessageIds({
|
|
36
|
-
_: "other",
|
|
37
36
|
deleted_business_messages: { message_ids: [1, 2] },
|
|
38
37
|
}),
|
|
39
38
|
[1, 2],
|
|
40
39
|
);
|
|
41
40
|
assert.deepEqual(
|
|
42
41
|
extractDeletedTelegramMessageIds({
|
|
43
|
-
|
|
44
|
-
messages: [3, 4],
|
|
45
|
-
}),
|
|
46
|
-
[3, 4],
|
|
47
|
-
);
|
|
48
|
-
assert.deepEqual(
|
|
49
|
-
extractDeletedTelegramMessageIds({
|
|
50
|
-
_: "updateDeleteMessages",
|
|
51
|
-
messages: [3, "bad"],
|
|
42
|
+
deleted_business_messages: { message_ids: [3, "bad"] },
|
|
52
43
|
}),
|
|
53
44
|
[],
|
|
54
45
|
);
|
|
46
|
+
assert.deepEqual(extractDeletedTelegramMessageIds({}), []);
|
|
55
47
|
});
|
|
56
48
|
|
|
57
49
|
test("Update routing classifies authorization state for pair, allow, and deny", () => {
|
|
@@ -101,11 +93,10 @@ test("Update routing extracts private human messages from message or edited_mess
|
|
|
101
93
|
assert.ok(directMessage);
|
|
102
94
|
});
|
|
103
95
|
|
|
104
|
-
test("Update flow prioritizes deleted-message handling over other update kinds", () => {
|
|
96
|
+
test("Update flow prioritizes deleted business-message handling over other update kinds", () => {
|
|
105
97
|
const action = buildTelegramUpdateFlowAction(
|
|
106
98
|
{
|
|
107
|
-
|
|
108
|
-
messages: [1, 2],
|
|
99
|
+
deleted_business_messages: { message_ids: [1, 2] },
|
|
109
100
|
message_reaction: {
|
|
110
101
|
chat: { type: "private" },
|
|
111
102
|
user: { id: 1, is_bot: false },
|
|
@@ -119,7 +110,6 @@ test("Update flow prioritizes deleted-message handling over other update kinds",
|
|
|
119
110
|
test("Update flow returns authorized callback and message actions", () => {
|
|
120
111
|
const callbackAction = buildTelegramUpdateFlowAction(
|
|
121
112
|
{
|
|
122
|
-
_: "other",
|
|
123
113
|
callback_query: {
|
|
124
114
|
from: { id: 7, is_bot: false },
|
|
125
115
|
message: { chat: { type: "private" } },
|
|
@@ -133,7 +123,6 @@ test("Update flow returns authorized callback and message actions", () => {
|
|
|
133
123
|
{ kind: "allow" },
|
|
134
124
|
);
|
|
135
125
|
const messageAction = buildTelegramUpdateFlowAction({
|
|
136
|
-
_: "other",
|
|
137
126
|
message: {
|
|
138
127
|
chat: { type: "private" },
|
|
139
128
|
from: { id: 9, is_bot: false },
|
|
@@ -148,7 +137,6 @@ test("Update flow returns authorized callback and message actions", () => {
|
|
|
148
137
|
|
|
149
138
|
test("Update flow ignores unauthorized transport shapes and preserves reaction events", () => {
|
|
150
139
|
const reactionAction = buildTelegramUpdateFlowAction({
|
|
151
|
-
_: "other",
|
|
152
140
|
message_reaction: {
|
|
153
141
|
chat: { type: "private" },
|
|
154
142
|
user: { id: 1, is_bot: false },
|
|
@@ -156,7 +144,6 @@ test("Update flow ignores unauthorized transport shapes and preserves reaction e
|
|
|
156
144
|
});
|
|
157
145
|
assert.equal(reactionAction.kind, "reaction");
|
|
158
146
|
const ignored = buildTelegramUpdateFlowAction({
|
|
159
|
-
_: "other",
|
|
160
147
|
callback_query: {
|
|
161
148
|
from: { id: 1, is_bot: true },
|
|
162
149
|
message: { chat: { type: "private" } },
|
|
@@ -218,7 +205,6 @@ test("Update execution plan preserves deleted and reaction actions", () => {
|
|
|
218
205
|
test("Update execution plan can be built directly from updates", () => {
|
|
219
206
|
const plan = buildTelegramUpdateExecutionPlanFromUpdate(
|
|
220
207
|
{
|
|
221
|
-
_: "other",
|
|
222
208
|
callback_query: {
|
|
223
209
|
from: { id: 4, is_bot: false },
|
|
224
210
|
message: { chat: { type: "private" } },
|
|
@@ -260,7 +246,6 @@ test("Update runtime can execute directly from raw updates", async () => {
|
|
|
260
246
|
const events: string[] = [];
|
|
261
247
|
await executeTelegramUpdate(
|
|
262
248
|
{
|
|
263
|
-
_: "other",
|
|
264
249
|
message: {
|
|
265
250
|
chat: { id: 10, type: "private" },
|
|
266
251
|
message_id: 20,
|