@llblab/pi-telegram 0.18.6 → 0.19.0

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 CHANGED
@@ -37,7 +37,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
37
37
  - `/.agents/skills/`: Project-local agent skills that support this repository. Keep each skill's durable operating guidance in its own `SKILL.md`; use `AGENTS.md` only for topology and maintenance rules.
38
38
  - `telegram-bot`: Agent-facing Telegram Bot API lookup skill. Keep the vendored `api.md` reference intact and put navigation, freshness, line-range, task, risk, and synonym indexes in `SKILL.md`.
39
39
  - `domain-dag`: Agent-facing architecture skill and validator for this repository's flat Domain DAG, composition-root, header, cycle, and shared-bucket invariants.
40
- - `/README.md`: User-facing project entry point and product hyperindex. It is the public representation of the extension, not agent-maintenance context. Keep its rhythm as identity → mental model/lensesinstallconnectusecore features → docs, with vivid examples that explain the runtime adapter/operator-console model without duplicating full docs. It should describe the extension through several durable lenses — operator companion, Telegram UI harness, Pi runtime adapter, multi-instance/thread organism, companion-extension platform, delivery/media surface, safety boundary, and release maturity — so a reader can understand what the extension can become without wading into minor implementation detail.
40
+ - `/README.md`: User-facing project entry point and product hyperindex. It is the public representation of the extension, not agent-maintenance context. Keep its rhythm as identity → install/connectlived examples product model compact feature showcase controls/surfaces safety boundaries → docs. Preserve both layers: strong product positioning plus a practical feature catalogue. Do not let the README collapse into abstract positioning that hides capabilities, and do not let it regress into an implementation dump that duplicates full docs. It should describe the extension through several durable lenses — operator companion, Telegram UI harness, Pi runtime adapter, multi-instance/thread organism, companion-extension platform, delivery/media surface, safety boundary, and release maturity — so a reader can understand practical value without wading into minor implementation detail.
41
41
  - `/AGENTS.md`: Durable engineering and runtime conventions
42
42
  - `/BACKLOG.md`: Canonical open work. Keep only open top-level tasks; when all subtasks under a top-level task are complete, remove that task from the backlog and record completed delivery in `CHANGELOG.md` if user-visible. Put detailed decomposition under the single owning top-level task with nested checkboxes and explicit done criteria instead of promoting completed slices into separate top-level backlog items.
43
43
  - `/CHANGELOG.md`: Completed delivery history focused on the final released behavior and user/operator/developer impact. Prefer multiple domain-scoped bullets in the form `[Domain]`: change + impact instead of accumulating unrelated changes into one long entry. Do not record transient implementation churn such as "added then removed" mechanics, internal reversions, or cleanup of an abandoned intermediate path unless the final product surface exposes that as a meaningful migration/breaking change.
@@ -103,7 +103,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
103
103
 
104
104
  ## 5.3 Telegram Delivery Semantics
105
105
 
106
- - Assistant and guest replies use Telegram-native Rich Markdown via Rich Message APIs, not Markdown→HTML conversion. Bridge-owned UI surfaces such as commands, menus, status, queue controls, and sections should keep explicit Telegram HTML/plain rendering by default because readability and maintainability are higher there. Companion sections may explicitly choose Markdown, HTML, or plain text per view. Keep native Rich Markdown source close to model-authored Markdown, but normalize Bot-API-fragile equivalents when evidence shows a Telegram parser/client edge, such as space-after-marker blockquotes (`> quote` -> `>quote`) and dollar-prefixed ticker atoms (`$BLDR` -> `\$BLDR`) outside code fences/spans
106
+ - Rich Markdown is the model-answer membrane: use Telegram-native Rich Message APIs for complete assistant/guest model replies only, not for tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, or other harness-owned technical surfaces. Those bridge-owned surfaces should keep explicit Telegram HTML/plain rendering by default because readability, operational predictability, and surface ownership are higher there. Companion sections may explicitly choose Markdown, HTML, or plain text per view. Keep native Rich Markdown source close to model-authored Markdown, but normalize Bot-API-fragile equivalents when evidence shows a Telegram parser/client edge, such as space-after-marker blockquotes (`> quote` -> `>quote`) and dollar-prefixed ticker atoms (`$BLDR` -> `\$BLDR`) outside code fences/spans
107
107
  - Use `.agents/skills/telegram-bot/SKILL.md` and its `api.md` reference for native Rich Markdown, Bot API topic transport, and transport capability checks
108
108
  - Formula guidance belongs in the Telegram-turn prompt contract: use `$...$` for inline math and `$$...$$` for block math; backticks intentionally render formulas as literal code
109
109
  - Real code blocks must stay literal and escaped
package/BACKLOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Project Backlog
2
2
 
3
- _Current deterministic status: Threaded Mode implementation, native typing/activity status, regression coverage, docs/context reconciliation, typecheck, full tests, pack check, audit, Domain DAG validation, context validation, native Windows classic↔Threaded Mode upgrade/downgrade smoke, and live post-reload leader/follower prompt routing, follower Active parity, and unbound reroute/restore smoke are green. This backlog intentionally tracks only release-relevant remaining work: evidence-gated Telegram client/runtime follow-ups and upstream Pi API blockers._
3
+ _Current deterministic status: Threaded Mode implementation, native typing/activity status, regression coverage, docs/context reconciliation, typecheck, full tests, pack check, audit, Domain DAG validation, context validation, native Windows classic↔Threaded Mode upgrade/downgrade smoke, and live post-reload leader/follower prompt routing, follower Active parity, and unbound reroute/restore smoke are green. This backlog intentionally tracks only release-relevant remaining work: 0.19.0 release preparation, evidence-gated Telegram client/runtime follow-ups, and upstream Pi API blockers._
4
4
 
5
5
  ## P0 — Promoted Follower Reload Smoke
6
6
 
package/CHANGELOG.md CHANGED
@@ -1,6 +1,13 @@
1
1
  # Changelog
2
2
 
3
- ## Unreleased
3
+ ## 0.19.0: Telegram Companion Hub
4
+
5
+ - `[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.
6
+ - `[Settings]` Added an opt-in `richDraftPreviews` setting, exposed it in Telegram Settings, and gated `sendRichMessageDraft` preview frames behind it while preserving final native Rich Markdown replies. Impact: fresh installs default to final-only Rich Markdown plus native active status, and operators can explicitly enable progressive draft drawing when desired.
7
+ - `[Backlog]` Opened 0.19.0 release preparation and closed the rich draft preview setting task, making final-only Rich Markdown replies the intended baseline and draft previews an opt-in progressive enhancement.
8
+ - `[Context]` Clarified the README standard: keep the root entrypoint balanced between product positioning and a compact practical feature showcase, avoiding both over-abstract marketing copy and duplicated implementation docs. Impact: future README changes preserve the feature-catalogue value while keeping the surface coherent.
9
+ - `[Docs]` Reworked the root README as a product-oriented RhythmE entrypoint with clearer hero positioning, install/connect flow, operating model, expanded feature showcase, classic-vs-Threaded Mode comparison, safety boundaries, extension platform summary, and documentation map. Impact: the public entrypoint now explains `pi-telegram` as a Telegram companion console while preserving a practical catalogue of user-facing capabilities.
10
+ - `[Guest Mode]` Unauthorized guest-query replies now include the standard denied-action emoji. Impact: the compact `Access denied` message is easier to recognize in Telegram.
4
11
 
5
12
  ## 0.18.6: Threaded Mode parity hotfix
6
13
 
package/README.md CHANGED
@@ -1,25 +1,15 @@
1
1
  # pi-telegram
2
2
 
3
- ![pi-telegram screenshot](screenshot.png)
3
+ ![pi-telegram banner](banner.png)
4
4
 
5
- **Telegram runtime adapter for Pi.**
5
+ **A Telegram companion hub for live Pi sessions.**
6
6
 
7
- `pi-telegram` turns a private Telegram DM into a session-local operator console for Pi. It admits work, preserves context, streams readable replies, keeps busy sessions usable through queues, and turns assistant-authored intent into native Telegram artifacts.
7
+ `pi-telegram` turns a private Telegram DM into a mobile operator surface for Pi. It accepts prompts, queues work, streams readable previews, delivers final replies and files, exposes safe controls, and lets companion extensions add Telegram-native capabilities without owning a second bot loop.
8
8
 
9
- The product shape is a mobile companion for a live Pi session: start work in the terminal, then continue from Telegram on the couch or outside. It is not a remote terminal, PTY supervisor, or process launcher. Companion extensions can add commands, sections, status rows, handlers, and voice providers while `pi-telegram` keeps ownership of transport, queueing, and reply policy.
9
+ It is a **runtime adapter**, not a remote terminal. Start or supervise work in the Pi TUI, then continue from Telegram while away from the keyboard. The bridge preserves Pi session semantics instead of pretending Telegram is a PTY, shell, or process launcher. That boundary is the product: Telegram gets safe runtime handles, not raw terminal power.
10
10
 
11
11
  This repository is an actively maintained fork of [`badlogic/pi-telegram`](https://github.com/badlogic/pi-telegram). It started from upstream commit [`cb34008`](https://github.com/badlogic/pi-telegram/commit/cb34008460b6c1ca036d92322f69d87f626be0fc) and has since diverged substantially.
12
12
 
13
- ## What this gives you
14
-
15
- - **Mobile supervision**: continue a live Pi session from Telegram without turning Telegram into a fake terminal.
16
- - **Telegram-native controls**: menus, settings, queue controls, native active status, Rich Markdown replies, drafts, buttons, voice, files, and artifacts.
17
- - **Safe runtime mapping**: Telegram turns map into Pi lifecycle, queueing, model switching, compaction, previews, final replies, and ownership rules.
18
- - **Optional Threaded Mode**: one leader and visible follower Pi processes can share one bot through named Telegram threads.
19
- - **Extension platform**: companion extensions can add Telegram-native commands, sections, status rows, update handlers, handlers, and voice providers without owning polling.
20
-
21
- Use this README for the product shape. Follow the docs for exact contracts.
22
-
23
13
  ## Install
24
14
 
25
15
  From npm:
@@ -34,24 +24,24 @@ From git:
34
24
  pi install git:github.com/llblab/pi-telegram
35
25
  ```
36
26
 
37
- ## Connect
27
+ ## Quick Start
38
28
 
39
29
  ### 1. Create a Telegram bot
40
30
 
41
- 1. Open [@BotFather](https://t.me/BotFather)
42
- 2. Run `/newbot`
43
- 3. Pick a name and username
44
- 4. Copy the bot token
31
+ 1. Open [@BotFather](https://t.me/BotFather).
32
+ 2. Run `/newbot`.
33
+ 3. Pick a name and username.
34
+ 4. Copy the bot token.
45
35
 
46
- ### 2. Configure the bot token in Pi
36
+ ### 2. Configure Pi
47
37
 
48
- Start Pi, then run:
38
+ Run this inside Pi:
49
39
 
50
40
  ```bash
51
41
  /telegram-setup
52
42
  ```
53
43
 
54
- Paste your bot token when prompted. If a bot token is already saved in `~/.pi/agent/telegram.json`, the setup prompt 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`. The saved config file is written atomically with private `0600` permissions.
44
+ Paste the bot token. If `~/.pi/agent/telegram.json` already contains a saved token, setup offers it as the default. If no saved token exists, setup can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`.
55
45
 
56
46
  ### 3. Connect this Pi session
57
47
 
@@ -59,262 +49,222 @@ Paste your bot token when prompted. If a bot token is already saved in `~/.pi/ag
59
49
  /telegram-connect
60
50
  ```
61
51
 
62
- The adapter is session-local: only one Pi instance polls Telegram at a time. In classic mode, `/telegram-connect` records external control/polling ownership in `~/.pi/agent/locks.json`. When Telegram private-chat Threaded Mode is available for the bot, `/telegram-connect` uses the local Telegram organism automatically: the first live instance becomes leader, later live instances register as followers instead of taking over while the leader heartbeat is healthy. Local queue and reply state stay per Pi instance, so an instance that loses Telegram control still finishes work it already accepted.
52
+ The connected Pi instance owns Telegram polling. In classic mode this is a singleton lock. When Telegram private-chat Threaded Mode is available, one live instance becomes the leader and later visible Pi instances register as followers.
63
53
 
64
54
  ### 4. Pair your Telegram account
65
55
 
66
- 1. Open the DM with your bot in Telegram
67
- 2. Send `/start`
68
-
69
- The first user to message the bot becomes the exclusive owner of the adapter. Messages from other users are ignored.
70
-
71
- ### Environment-only configuration
72
-
73
- Most day-to-day controls live in the Telegram menu or Pi commands. A few important runtime knobs intentionally stay in environment variables because they affect bootstrap, networking, or transport limits before a menu can help:
74
-
75
- - **Bot token bootstrap**: `/telegram-setup` can prefill from `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY` when no token is already saved.
76
- - **HTTP/HTTPS proxy**: native `fetch` can use `HTTP_PROXY`, `HTTPS_PROXY`, and `NO_PROXY` when Node's environment proxy mode is enabled. Use `NODE_USE_ENV_PROXY=1` or start Node with `--use-env-proxy`. SOCKS5 is not part of the zero-dependency core. If you need it, run a local HTTP-to-SOCKS bridge or system tunnel and point `HTTP_PROXY` / `HTTPS_PROXY` at the HTTP endpoint.
77
- - **Telegram network family**: `PI_TELEGRAM_NETWORK_FAMILY=auto|ipv4|ipv6|ipv4-fallback` controls Bot API transport only. The default is `ipv4-fallback`: try native `fetch` first, then retry transport-level failures through IPv4-only HTTPS. Use `auto` to force native `fetch` only, or `ipv4`/`ipv6` to force a family.
78
- - **Agent data root / temp location**: `PI_CODING_AGENT_DIR` changes the base agent directory used for `telegram.json`, locks, generated outbound-handler artifacts, and Telegram temp files. When unset, the adapter uses `~/.pi/agent`, so inbound Telegram files land in `~/.pi/agent/tmp/telegram`.
79
- - **Inbound file limit**: `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES` or `TELEGRAM_MAX_FILE_SIZE_BYTES` changes the default 50 MiB Telegram download limit.
80
- - **Outbound attachment limit**: `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES` or `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` changes the default 50 MiB `telegram_attach` delivery limit.
81
-
82
- Assistant Markdown is delivered through Telegram's native Rich Message API. There is no `telegram.json` rendering toggle: final replies use `sendRichMessage`, and streaming previews use `sendRichMessageDraft` when Telegram drafts are available.
83
-
84
- Long-running Telegram turns use Telegram's native active status as the activity indicator: technically Bot API `sendChatAction(typing)`, presented in product language as `...active` even when Telegram clients render it as typing dots. Active status is the only automatic in-chat work signal before the final reply.
85
-
86
- Set these variables before launching Pi. Some transport defaults (notably Telegram temp directory and inbound/outbound byte-limit constants) are intentionally captured when the extension modules load, while setup-token defaults and agent-dir lookups used by config/locks are read through their runtime helpers.
87
-
88
- ## Use
89
-
90
- Once paired, chat with your bot in Telegram. Text, images, files, replies, edits, media groups, and configured handler output are forwarded into Pi as Telegram-originated turns.
91
-
92
- What it feels like:
93
-
94
- - Start work in the terminal, walk away, and keep supervising the same live Pi session from Telegram.
95
- - Open `/start` and get a Telegram control panel for the running Pi session: status, prompt templates, model, thinking, settings, and queue.
96
- - Fire off three tasks while Pi is busy. They become visible queue items instead of terminal noise.
97
- - Open Queue from the menu, inspect waiting work, delete stale prompts, or move important work forward.
98
- - Switch models from Telegram mid-run; the adapter schedules a safe continuation instead of tearing state apart.
99
- - Send a voice note; a configured inbound handler or registered STT provider transcribes it; Pi answers in the same chat.
100
- - Drop a screenshot and ask, "what is broken here?" The image payload reaches Pi with the local file context.
101
- - Ask for a generated file; when Pi calls `telegram_attach`, the artifact returns with the active Telegram reply or is sent directly to the paired/default chat from local work.
102
-
103
- ### Telegram controls
104
-
105
- Use these inside the Telegram DM with your bot. The main entrypoint is `/start`: it opens the operator menu and exposes many of the important agent controls that can be safely adapted through Pi's extension APIs. The bot does not forward arbitrary terminal slash commands or emulate TUI-only session controls.
106
-
107
- - **`/start`**: Pair the first Telegram user when needed, register bot commands, and open the inline application menu with command help, prompt-template commands, status rows, model controls, thinking controls, settings, and queue controls.
108
- - **`/compact`**: Ask for inline confirmation, then start session compaction when the session is idle; Telegram shows the native active indicator while manual or automatic compaction is running.
109
- - **`/next`**: Dispatch the next queued turn, aborting Pi first if needed.
110
- - **`/continue`**: Enqueue a priority `continue` prompt.
111
- - **`/abort`**: Abort the active run without touching the queue. Abort-history applies only to Telegram-owned active turns; later local prompts do not make the next Telegram prompt absorb older queue items.
112
- - **`/stop`**: Abort the active run and clear waiting Telegram queue items.
113
-
114
- Hidden compatibility shortcuts: `/help` and `/status` open the main application menu, `/model` opens model controls, `/thinking` opens reasoning controls, `/queue` opens queue controls, and `/settings` opens bridge settings.
115
-
116
- Prompt-template commands are discovered from Pi prompt templates, mapped to Telegram-safe aliases (`fix-tests.md` becomes `/fix_tests`), shown in `/start`, and expanded before queueing.
117
-
118
- ### Pi commands
119
-
120
- Run these inside Pi, not Telegram:
121
-
122
- - **`/telegram-setup`**: Configure or update the Telegram bot token.
123
- - **`/telegram-connect`**: Start polling Telegram updates in the current Pi session and acquire the singleton lock.
124
- - **`/telegram-disconnect`**: Stop polling in the current Pi session and release the singleton lock.
125
- - **`/telegram-status`**: Inspect adapter status, connection, polling, execution, queue, and recent redacted runtime/API failure events.
126
-
127
- ### Files and artifacts
56
+ Open the bot DM and send:
128
57
 
129
- Send files or images directly to the bot. Inbound downloads are saved under `<agent-dir>/tmp/telegram` and default to a 50 MiB limit. The agent dir is `~/.pi/agent` unless `PI_CODING_AGENT_DIR` overrides it.
130
-
131
- If you ask Pi for a generated file, Pi can call `telegram_attach`: during a Telegram-originated turn the adapter sends it with the next Telegram reply, and during local/TUI work it sends directly to the paired/default chat, a registered follower's assigned thread, or explicit `chat_id` plus optional `thread_id`. Local work can also use `telegram_message` when you explicitly ask the agent to push a Markdown text message to Telegram; embedded `telegram_button` comments are parsed and attached to that message. Direct local/TUI delivery requires the current Pi instance to own `/telegram-connect`, or to be registered with an explicitly enabled multi-instance bus so it can route through the leader; if neither is true, take over or enable/register with the bus before sending. Outbound attachments default to a 50 MiB limit. Environment variables for both limits are listed in [Environment-only configuration](#environment-only-configuration).
132
-
133
- ### Telegram Threaded Mode and multi-instance bus
134
-
135
- Telegram private-chat Threaded Mode is the switch. Classic single-DM polling is the base mode. When Telegram reports private-chat threads are available for the bot, the adapter enables the local leader/follower bus automatically; when threads are unavailable or later disabled, it uses classic single-DM polling as the ordinary private-bot mode.
136
-
137
- Only the leader calls `getUpdates`; followers authenticate to the local bus and route allowlisted, target-scoped Telegram work through the leader. When private-chat threads are available, they become the UI targets:
138
-
139
- - The leader owns one thread;
140
- - Each explicitly connected follower gets one visible thread;
141
- - Telegram never launches hidden Pi follower processes;
142
- - New thread names are assigned by the bridge from a compact curated palette, while existing human names are preserved;
143
- - Unknown owner-created threads preserve the original prompt and offer a target-thread chooser instead of spawning work invisibly;
144
- - Stale follower tabs receive compact lifecycle notices before cleanup when the leader can prove ownership.
145
-
146
- Thread input is still authorized by `allowedUserId`. There is no separate public `telegram.json` switch for the bus: Telegram capability detection is the runtime source of truth. Native Windows Threaded Mode smoke remains tracked in `BACKLOG.md`; the intended transport is the same local bus over Windows named pipes instead of Unix sockets.
147
-
148
- ## Core features
149
-
150
- ### Operator menu and controls
151
-
152
- The inline application menu is the primary operator surface. It exposes status, prompt-template commands, companion-extension Telegram commands, model selection, thinking level selection, settings, and queue inspection/mutation: a Telegram-shaped subset of the important handles normally available from the CLI. A typical control loop stays inside Telegram: open `/start`, inspect status, jump into Queue, delete stale work, switch model, return to the main menu, and keep the Pi session running without touching the terminal.
153
-
154
- ### Queue runtime
155
-
156
- Messages sent while Pi is busy enter the prompt queue and are processed in order. Control actions and model-switch continuation turns use higher-priority lanes. Queue processing and reply delivery stay local to the Pi instance that accepted the work, even if `/telegram-connect` later moves elsewhere.
157
-
158
- The menu is the primary way to inspect and mutate the queue. Reactions are an extra shortcut when Telegram delivers `message_reaction` updates for the chat. The same rules apply to text, voice, files, images, and media groups:
58
+ ```text
59
+ /start
60
+ ```
159
61
 
160
- - Priority shortcuts: `👍`, `⚡️`, `❤️`, `🕊`, and `🔥` promote waiting work.
161
- - Removal shortcuts: `👎`, `👻`, `💔`, `💩`, and `🗑` remove waiting work from the queue.
62
+ The first Telegram user to message the bot becomes the allowed owner. Other users are ignored.
63
+
64
+ ## What It Feels Like
65
+
66
+ - Start a task in the terminal, walk away, and keep supervising it from your phone.
67
+ - Send another prompt while Pi is busy; it becomes a queued Telegram turn instead of interrupting the active run.
68
+ - Open `/start` to inspect status, model, thinking, settings, prompt templates, and queue controls.
69
+ - Send voice, images, files, replies, edits, or media groups; the bridge turns them into Pi context.
70
+ - Ask for an artifact; `telegram_attach` returns it through the active reply or direct Telegram delivery.
71
+ - In Threaded Mode, run multiple visible Pi instances through one bot, each with its own Telegram thread.
72
+
73
+ ## Product Model
74
+
75
+ | Lens | What `pi-telegram` owns |
76
+ | ----------------------- | -------------------------------------------------------------------------------------------------- |
77
+ | Operator companion | A phone-width control surface for a live Pi session |
78
+ | Runtime adapter | Telegram turns mapped into Pi lifecycle, queueing, previews, final replies, and artifacts |
79
+ | Telegram UI harness | Menus, settings, callbacks, Rich Markdown, drafts, active status, buttons, voice, and files |
80
+ | Multi-instance organism | One leader plus explicit visible followers routed through Telegram private-chat threads |
81
+ | Extension platform | Commands, sections, status rows, update handlers, inbound/outbound handlers, and voice providers |
82
+ | Safety boundary | No hidden Pi processes, no fake terminal, no PTY tricks, no arbitrary TUI slash-command forwarding |
83
+
84
+ ## Feature Showcase
85
+
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
+
88
+ | Surface | What you can do | Why it matters |
89
+ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
90
+ | Prompt intake | Send text, replies, edits, images, files, albums, voice notes, and handler output into Pi. | Telegram becomes a real mobile input surface with file/context references, not just a text tunnel. |
91
+ | Queue control | Inspect waiting turns, delete stale work, promote important prompts, continue, abort, stop, or force the next queued item. | Long Pi tasks keep running while new mobile prompts stay visible and controllable instead of interrupting or disappearing. |
92
+ | Operator menu | Use `/start` for status, prompt templates, model, thinking, settings, queue, extension sections, and diagnostics. | The bot is an operator panel, not a command cheat sheet. |
93
+ | Prompt templates | Run Pi prompt templates as Telegram-safe commands such as `/fix_tests`. | Reusable local workflows become phone-accessible without exposing arbitrary terminal commands. |
94
+ | Model and thinking | Switch model or reasoning level from Telegram through safe continuation flows. | Mobile control can adjust execution strategy without tearing down the current session. |
95
+ | Compaction | Confirm `/compact`, show native active status during compaction, and preserve Telegram-owned turn semantics. | Context maintenance is visible and safe from the phone. |
96
+ | Streaming previews | Show native active status and, when enabled, Rich Markdown drafts while the agent is still working. | The operator can see progress without making draft rendering the baseline. |
97
+ | Final Rich Markdown | Deliver complete assistant/guest model replies through Telegram Rich Message APIs with safe splitting. | Meaningful model answers stay readable on narrow mobile screens without forcing generic HTML conversion. |
98
+ | Bridge UI rendering | Render tool rows, reasoning/technical steps, menus, queue controls, status, settings, diagnostics, and sections through explicit Telegram HTML/plain UI. | Harness-owned surfaces remain operationally predictable and visually distinct from model-authored answers. |
99
+ | Inbound files | Download inbound files to the Pi agent temp directory with size limits. | Screenshots, PDFs, datasets, and artifacts enter Pi as inspectable local files. |
100
+ | Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
101
+ | Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
102
+ | Voice output | Use `telegram_voice`, reply modes, configured voice handlers, and TTS providers. | Replies can become Telegram voice messages when the workflow calls for it. |
103
+ | Buttons | Turn top-level `telegram_button` comments into inline buttons. | Assistant-authored choices become native Telegram interactions. |
104
+ | Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
105
+ | Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
106
+ | Reroute and restore | Preserve unknown threads and offer explicit target choices. | Telegram client state can be repaired without silently deleting or hijacking prompts. |
107
+ | Extension sections | Add menu sections, commands, status rows, settings, callbacks, and delivery helpers from companion extensions. | `pi-telegram` becomes a platform surface for other Pi extensions. |
108
+ | Runtime diagnostics | Use `/telegram-status` and recent runtime events for connection, role, queue, transport, and failure evidence. | Debugging lives in the operator surface instead of hidden logs only. |
109
+ | Safety and ownership | Pair one owner, lock transport, scope targets, and reject fake terminal behavior. | Remote access remains explicit, bounded, and understandable. |
110
+
111
+ ## Core Loop
112
+
113
+ ```text
114
+ Telegram message
115
+ -> Telegram turn
116
+ -> queue or active dispatch
117
+ -> Pi agent lifecycle
118
+ -> streaming preview / native active status
119
+ -> final Rich Markdown reply
120
+ -> optional files, voice, buttons, or callback actions
121
+ ```
162
122
 
163
- ### Streaming and native Rich Markdown
123
+ The bridge keeps Telegram responsive without stealing Pi's runtime model. Queueing, model changes, compaction, aborts, final delivery, and direct artifact sends all stay scoped to the Pi instance that accepted the work.
164
124
 
165
- Assistant Markdown is sent to Telegram as native Rich Markdown. Streaming previews use Telegram rich-message drafts when a structurally closed Markdown prefix is available, and final replies persist the complete Markdown through `sendRichMessage`. The bridge still strips top-level hidden action comments before delivery and splits only when Telegram transport limits require it.
125
+ ## Telegram Controls
166
126
 
167
- Telegram HTML rendering remains the default for bridge-owned UI surfaces such as commands, menus, status messages, queue controls, and extension sections, where explicit markup is clearer and easier to maintain. Native Rich Markdown is reserved for model-authored Markdown replies and guest replies that naturally arrive as Markdown.
127
+ Use these in the bot DM.
168
128
 
169
- ### Media, replies, edits, and split text
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 |
170
137
 
171
- Telegram replies to earlier text or caption messages are forwarded as `[reply]` context for normal prompts, while slash commands still parse from the new message text only. If a Telegram message is edited while still waiting in the queue, the queued turn is updated instead of duplicated. Very long text messages that Telegram appears to split automatically are coalesced through a conservative debounce when the first chunk is near Telegram's text limit.
138
+ Hidden compatibility shortcuts: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings` jump into the same menu system.
172
139
 
173
- ### Inbound handlers and STT providers
140
+ ## Pi Commands
174
141
 
175
- `telegram.json` can define ordered `inboundHandlers` for Telegram → Pi preprocessing: text translation, voice transcription, OCR, PDF extraction, or any command-template pipeline. Matching handlers run before the turn enters the queue; failed handlers record diagnostics and fall back safely. Legacy `attachmentHandlers` still work as a deprecated compatibility alias appended after `inboundHandlers`.
142
+ Run these inside Pi.
176
143
 
177
- A practical voice setup is simple: Telegram `.ogg` arrives, STT runs locally or through your chosen command, stdout is injected as `[outputs]`, and Pi receives the result as usable prompt context. Extensions can also register programmatic inbound handlers; full voice extensions can register transcription providers. Explicit `inboundHandlers` and legacy `attachmentHandlers` run first, then programmatic inbound handlers, then registered STT providers as fallback for voice/audio files.
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 |
178
150
 
179
- ```json
180
- {
181
- "inboundHandlers": [
182
- {
183
- "type": "text",
184
- "template": "/path/to/translate --lang {lang=en} --text \"{text}\""
185
- },
186
- {
187
- "type": "voice",
188
- "template": [
189
- "/path/to/stt --file {file} --lang {lang=ru}",
190
- "/path/to/translate-stdin --lang {lang=en}"
191
- ]
192
- },
193
- {
194
- "mime": "audio/*",
195
- "template": [
196
- "/path/to/stt-fallback --file {file} --lang {lang=ru}",
197
- "/path/to/translate-stdin --lang {lang=en}"
198
- ]
199
- }
200
- ]
201
- }
202
- ```
151
+ ## Main Surfaces
203
152
 
204
- ### Outbound handlers, voice synthesis providers, and buttons
153
+ ### Operator Menu
205
154
 
206
- Assistant replies can include hidden outbound blocks. `telegram_voice` and `telegram_button` are not Pi tools; they are assistant-authored HTML comments that the adapter removes from Telegram text and handles after `agent_end`. Recognized blocks must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples. Do not use JSON button specs, inline comments after visible text, or standalone button tool calls; write normal Markdown plus hidden comments, and add visible parent text if buttons would otherwise be the only output.
155
+ `/start` opens the Telegram-native control panel: status, prompt-template commands, model selection, thinking level, settings, queue controls, and extension sections. It is the primary Telegram UI; reaction shortcuts are secondary queue affordances.
207
156
 
208
- Prompt guidance is context-aware: unconfigured sessions receive no Telegram suffix, local/TUI prompts only get explicit direct-delivery guidance, and Telegram-originated turns get the full phone-width output and action-comment contract.
157
+ ### Queue Runtime
209
158
 
210
- ```md
211
- Full technical answer stays readable as text.
159
+ Messages sent while Pi is busy become queued turns. Priority lanes support control actions and model-switch continuations. Queue controls let you inspect, delete, promote, and dispatch work from Telegram without touching the terminal.
212
160
 
213
- <!-- telegram_voice lang=ru rate=+30%
214
- Text to synthesize as a Telegram voice message.
215
- -->
161
+ ### Native Rich Markdown
216
162
 
217
- <!-- telegram_button label="Show risks"
218
- List the main risks first.
219
- -->
220
- ```
163
+ Rich Markdown is the model-answer membrane. Complete assistant and guest model replies use Telegram's native Rich Message APIs, while tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces use explicit Telegram HTML/plain rendering. This keeps meaningful model-authored answers visually distinct from bridge-owned operational UI. Streaming previews may use rich-message drafts when enabled; final replies persist through `sendRichMessage`.
221
164
 
222
- Outbound `type: "text"` handlers can transform final text/Markdown before native Rich Markdown delivery. Voice output can be handled either by configured `outboundHandlers` with `type: "voice"` or by registered voice synthesis provider extensions: the bridge extracts `telegram_voice` text or intercepts text by reply mode, asks the voice pipeline for a `.ogg`/`.opus` artifact, and uploads it through Telegram `sendVoice`. Explicit configured voice handlers run before zero-config providers, so operator-owned `telegram.json` pipelines stay authoritative.
165
+ ### Files And Artifacts
223
166
 
224
- The agent writes intent; providers or voice handlers own TTS and format conversion, the adapter owns Telegram transport, and buttons route back as queued prompts.
167
+ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limit. `telegram_attach` is the canonical outbound file path. During Telegram-originated turns it attaches to the active reply; during explicit local/TUI delivery it can send to the paired/default chat or routed Threaded Mode target.
225
168
 
226
- ### Voice reply policies
169
+ ### Voice And Media
227
170
 
228
- The bridge can automatically convert agent text replies into Telegram voice messages without requiring explicit `<!-- telegram_voice -->` markup in every response. Configure this from Settings `Voice reply` or by setting `voice.replyMode` in `telegram.json`:
171
+ Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis.
229
172
 
230
- - `hidden` (default): no `voice.replyMode` is stored. Behavior is manual, but prompt context stays silent.
231
- - `manual`: agent-authored `<!-- telegram_voice -->` markup is required for voice replies; no automatic conversion. Unlike `hidden`, this explicit mode adds `[voice] reply mode: manual` context.
232
- - `mirror`: when the user sends a voice message, the next reply is converted to voice and text preview is suppressed. Text-originated turns stay on the normal/manual text path, so agent-authored `<!-- telegram_voice -->` markup still works explicitly.
233
- - `always`: every reply is converted to voice and text preview is suppressed.
173
+ ### Buttons And Callbacks
234
174
 
235
- If `telegram.json` explicitly sets a valid `voice.replyMode`, prompts include compact `[voice] reply mode: ...` context after handler outputs. When the field is missing or invalid, behavior still defaults to manual/hidden and the prompt context stays silent.
175
+ Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions.
236
176
 
237
- In `mirror` and `always` modes, the bridge transparently intercepts agent text responses and routes them through the outbound voice pipeline. Configured `outboundHandlers` with `type: "voice"` run first in their configured order; zero-config registered synthesis providers run after them as progressive fallbacks. If several synthesis providers are installed, they are tried in registration order and the first one that returns a valid `.ogg`/`.opus` artifact handles the reply; `undefined`, errors, or invalid output fall through to the next provider. If every voice generator fails, the bridge falls back to sending the text reply instead.
177
+ ### Threaded Mode And Multi-Instance Bus
238
178
 
239
- Voice synthesis provider extensions register TTS backends at runtime through public API domain subpaths. Providers use durable ids, receive the text plus optional `lang`/`rate` hints, and must return `.ogg` or `.opus` artifacts. The bridge tries configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers in registration order.
179
+ Classic private DM mode is the base product mode. When Telegram private-chat Threaded Mode is available, the bridge enables a local leader/follower bus automatically:
240
180
 
241
- Provider examples and diagnostics live in [Voice Integration](./docs/voice.md) and [Public API](./docs/public-api.md). Boundary: providers own TTS; `pi-telegram` owns reply policy, prompt context, fallback ordering, and Telegram transport.
181
+ - One live leader owns `getUpdates`.
182
+ - Followers are visible Pi processes started by the operator.
183
+ - Each connected instance gets a Telegram thread target.
184
+ - Unknown threads are preserved and offered explicit reroute/restore choices.
185
+ - Telegram never launches hidden Pi processes.
242
186
 
243
- ### Extension interop
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 |
244
191
 
245
- Unknown inline-button callbacks are forwarded to Pi as `[callback] <data>` when they do not belong to pi-telegram, so other extensions can namespace and handle Telegram buttons without polling the bot themselves. Layered extensions that need synchronous update handling can register a handler on the shared update registry.
192
+ ## Environment Configuration
246
193
 
247
- ### Extension Sections
194
+ Most controls live in Pi commands or the Telegram menu. Environment variables remain for bootstrap and transport boundaries:
248
195
 
249
- Ordinary pi extensions can register Telegram-native slash commands, structured UI sections, and compact status lines without owning a second polling loop. Slash commands use explicit opt-in registration from `@llblab/pi-telegram/commands`, so workflow-specific commands can live in companion extensions instead of expanding the core bridge command set. UI sections appear in the main Telegram menu and Settings submenu, default to explicit Telegram HTML UI markup, and may explicitly choose Markdown or plain text when that better matches their content; status lines allow widgets such as quota indicators to appear beside Status, Usage, Cost, and Context only when relevant to the active model. Each section gets a narrow typed context with `edit`, `open`, `enqueuePrompt`, `answerCallback`, and `callbackData()` — enough to build interactive Telegram-native surfaces while `pi-telegram` owns transport, callback routing, navigation hierarchy, and diagnostics.
196
+ | Area | Variables |
197
+ | ------------------------- | ---------------------------------------------------------------------------------------------- |
198
+ | Bot token bootstrap | `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, `TELEGRAM_KEY` |
199
+ | HTTP proxy | `HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`, plus `NODE_USE_ENV_PROXY=1` or Node `--use-env-proxy` |
200
+ | Telegram network family | `PI_TELEGRAM_NETWORK_FAMILY`=auto | ipv4 | ipv6 | ipv4-fallback` |
201
+ | Agent data root | `PI_CODING_AGENT_DIR` |
202
+ | Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
203
+ | Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
250
204
 
251
- Import `registerTelegramSection()` from `@llblab/pi-telegram/sections` and return a disposer on shutdown. Sections can send interactive messages directly into the chat via `ctx.open()` — confirmation dialogs, approve/deny gates, and multi-step forms live outside the menu hierarchy while callbacks route through the same typed handler. See [`@llblab/pi-telegram-extension-demo`](https://github.com/llblab/pi-telegram-extension-demo) for a working reference and the [Extension Sections Standard](./docs/sections.md) for the full contract.
205
+ Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, native Rich Markdown replies, and native Telegram active status for long-running turns.
252
206
 
253
- ### Proactive push
207
+ ## Extension Platform
254
208
 
255
- `telegram.json` can set `proactivePush: true` to send successful local non-Telegram final replies to Telegram when no Telegram turn is active and this Pi instance currently owns `/telegram-connect` or is registered as a Threaded Mode follower. Non-owners skip proactive delivery instead of pushing unrelated local/headless results through a bot they no longer control. Local prompt text is not mirrored because the bot does not own terminal user messages. The mode is off by default and can be toggled from settings.
209
+ Companion extensions can integrate with Telegram without owning polling or transport:
256
210
 
257
- ### Time context
211
+ - Register Telegram slash commands.
212
+ - Add menu sections and settings surfaces.
213
+ - Add compact status rows.
214
+ - Handle update/callback namespaces.
215
+ - Provide inbound preprocessing handlers.
216
+ - Provide outbound voice synthesis.
217
+ - Use direct delivery helpers for explicit local/TUI sends.
258
218
 
259
- `telegram.json` can opt into a compact `[time]` line in Telegram-originated prompts so Pi has a wall-clock reference for requests such as "today", "now", or scheduling. It is hidden by default and uses the system timezone; the mode can also be changed from Settings → `🕒 Time injection`.
219
+ Stable public entrypoints are documented in [Public API](./docs/public-api.md), [Extension Sections](./docs/sections.md), [Inbound Handlers](./docs/inbound.md), [Outbound Handlers](./docs/outbound.md), [Updates](./docs/updates.md), and [Voice Integration](./docs/voice.md).
260
220
 
261
- ```json
262
- {
263
- "time": {
264
- "injectionMode": "interval",
265
- "interval": 3600000
266
- }
267
- }
268
- ```
221
+ ## Safety Boundaries
269
222
 
270
- Modes are `hidden`, `always`, and `interval`. `hidden` means no time line is added to prompt context. `interval` is measured in milliseconds and rate-limits the time line per chat in memory, so back-to-back messages do not repeatedly spend context on the same timestamp. When present, `[time]` is the final prompt-context section after attachments, handler outputs, and voice policy.
223
+ `pi-telegram` intentionally does not:
271
224
 
272
- ## Docs
225
+ - Spawn hidden Pi follower processes.
226
+ - Pretend Telegram is a terminal or PTY.
227
+ - Forward arbitrary Telegram slash commands into the Pi TUI.
228
+ - Inject raw TTY input or terminal-control sequences.
229
+ - Replace Pi session lifecycle without an official Pi API.
230
+ - Let non-owner Telegram users control the bridge.
273
231
 
274
- - [Project Context](./AGENTS.md): durable engineering conventions and architecture constraints.
275
- - [Open Backlog](./BACKLOG.md): planned work and known follow-ups.
276
- - [Changelog](./CHANGELOG.md): completed delivery history.
277
- - [Documentation Index](./docs/README.md): technical docs hub.
278
- - [Architecture](./docs/architecture.md): runtime and subsystem overview.
279
- - [Public API](./docs/public-api.md): stable commands, config, package entrypoints, assistant markup, and extension APIs.
280
- - [Inbound Handlers](./docs/inbound.md): Telegram → Pi preprocessing.
281
- - [Outbound Handlers](./docs/outbound.md): final text, voice, and artifact pipelines.
282
- - [Command Templates](./docs/command-templates.md): portable command-template contract.
283
- - [Callback Namespaces](./docs/callback-namespaces.md): callback interop for layered extensions.
284
- - [Updates](./docs/updates.md): shared update interception.
285
- - [Extension Sections](./docs/sections.md): Telegram extension sections platform for loading extensions that register UI surfaces.
286
- - [Voice Integration](./docs/voice.md): voice reply policies, transparent interception, and provider extension API.
287
- - [Locks](./docs/locks.md): singleton polling ownership.
288
- - [UI Style](./docs/ui-style.md): inline button, toggle, tab, option-list, card, and dialog style guide.
232
+ Telegram is a companion surface around a live Pi runtime, not a second runtime.
289
233
 
290
- ## Notes
234
+ ## Documentation Map
291
235
 
292
- - The extension intentionally keeps rich visual/TUI configuration minimal for now. For advanced setup, ask an agent to read this README and the docs, then update `~/.pi/agent/telegram.json` for your workflow.
293
- - Replies to Telegram prompts are sent as Telegram replies to the source message when possible; if the source message is unavailable, delivery falls back to a normal message.
294
- - Temporary inbound Telegram files are cleaned up on later session starts.
236
+ - [Architecture](./docs/architecture.md) runtime, domains, queue, transport, and Threaded Mode overview.
237
+ - [Public API](./docs/public-api.md) package entrypoints and stable companion-extension contracts.
238
+ - [Inbound Handlers](./docs/inbound.md) Telegram-to-Pi preprocessing pipelines.
239
+ - [Outbound Handlers](./docs/outbound.md) — final text/voice/file transformation and delivery.
240
+ - [Voice Integration](./docs/voice.md) — STT/TTS provider model and reply policies.
241
+ - [Extension Sections](./docs/sections.md) — Telegram-native companion UI surfaces.
242
+ - [Updates](./docs/updates.md) — update handler registry and callback interop.
243
+ - [Multi-Instance Bus](./docs/multi-instance-bus.md) — leader/follower routing in Threaded Mode.
244
+ - [Locks](./docs/locks.md) — singleton ownership and shared lock conventions.
245
+ - [UI Style](./docs/ui-style.md) — menu, emoji, labels, dialogs, and inline keyboard standards.
246
+ - [Callback Namespaces](./docs/callback-namespaces.md) — callback ownership and routing.
247
+ - [Command Templates](./docs/command-templates.md) — handler command-template conventions.
295
248
 
296
- ## Companion Extensions
297
-
298
- Third-party extensions that integrate with `pi-telegram`:
299
-
300
- - [`pi-codex-usage`](https://github.com/llblab/pi-codex-usage) — Compact Codex subscription quota/status widget for the Pi statusline and the inline menu status text opened by `/start`.
301
-
302
- ```bash
303
- pi install npm:@llblab/pi-codex-usage
304
- ```
249
+ The docs index lives at [docs/README.md](./docs/README.md).
305
250
 
306
- - [`pi-telegram-tool-status`](https://github.com/Timur00Kh/pi-telegram-tool-status) — Live-updating service messages that list tools used by the agent. It keeps one message per Telegram prompt and edits it in place as tools execute.
251
+ ## Development
307
252
 
308
253
  ```bash
309
- pi install npm:pi-telegram-tool-status
254
+ npm run typecheck
255
+ npm test
256
+ npm run audit
257
+ npm run pack:check
310
258
  ```
311
259
 
312
- - [`pi-xai-voice`](https://github.com/luxus/pi-xai-voice) — Companion extension that adds xAI-powered TTS for voice reply policies and `telegram_voice` markup.
260
+ Full validation:
313
261
 
314
262
  ```bash
315
- pi install npm:pi-xai-voice
263
+ npm run validate
316
264
  ```
317
265
 
318
- ## License
266
+ Project context:
319
267
 
320
- MIT
268
+ - [AGENTS.md](./AGENTS.md) — engineering and runtime conventions.
269
+ - [BACKLOG.md](./BACKLOG.md) — release-relevant open work.
270
+ - [CHANGELOG.md](./CHANGELOG.md) — completed delivery history.
@@ -223,7 +223,7 @@ During active Telegram-owned turns, assistant message start/update hooks re-arm
223
223
 
224
224
  ### Rendering And Delivery
225
225
 
226
- Assistant replies use Telegram-native Rich Markdown. Final Markdown is sent directly as `InputRichMessage.markdown` through `sendRichMessage`, and streaming previews use `sendRichMessageDraft` when draft delivery succeeds. Guest replies also use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
226
+ Rich Markdown is the model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage`, and guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering. Streaming previews may use `sendRichMessageDraft` when enabled and when draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
227
227
 
228
228
  Assistant delivery guarantees:
229
229
 
@@ -231,12 +231,12 @@ Assistant delivery guarantees:
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
- - 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 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:
238
238
 
239
- - Bridge-owned UI surfaces such as commands, menus, status messages, queue controls, and interactive sections use Telegram HTML/plain rendering helpers by default. These texts are authored for Telegram UI rather than model output, so explicit HTML markup remains clearer and easier to maintain.
239
+ - Bridge-owned UI surfaces such as tool rows, reasoning/thinking blocks, commands, menus, status messages, queue controls, diagnostics, settings, and interactive sections use Telegram HTML/plain rendering helpers by default. These texts are authored for operational UI rather than model output, so explicit HTML/plain markup remains clearer, safer, and easier to maintain.
240
240
  - In those UI/compat surfaces, real code blocks stay literal and escaped, supported absolute links stay clickable, unsupported links degrade safely, tables use compact monospace rendering with grapheme/display-width accounting, and list/quote/heading spacing stays Telegram-safe.
241
241
 
242
242
  Final delivery attaches reply metadata only where requested. Reply parameters apply only to the first chunk of split messages; continuation chunks are adjacent normal messages. Media-group turns reply to the representative message id.
package/index.ts CHANGED
@@ -487,6 +487,7 @@ export default function (pi: Pi.ExtensionAPI) {
487
487
  getMessageText: Replies.getAgentMessageText,
488
488
  getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
489
489
  sendDraft: nativeMarkdownDraftSender,
490
+ canSend: configControls.areRichDraftPreviewsEnabled,
490
491
  sendMarkdownReply,
491
492
  recordRuntimeEvent,
492
493
  ...replyTransport,
package/lib/config.ts CHANGED
@@ -57,6 +57,7 @@ export interface TelegramConfig {
57
57
  attachmentHandlers?: TelegramInboundHandlerConfig[];
58
58
  outboundHandlers?: TelegramOutboundHandlerConfig[];
59
59
  proactivePush?: boolean;
60
+ richDraftPreviews?: boolean;
60
61
  voice?: {
61
62
  replyMode?: "manual" | "mirror" | "always";
62
63
  /** Whether to attach the provider's transcriptText as caption on voice messages */
@@ -259,6 +260,23 @@ export function createTelegramProactivePushSetter(
259
260
  };
260
261
  }
261
262
 
263
+ export function createTelegramRichDraftPreviewsChecker(
264
+ configStore: Pick<TelegramConfigStore, "get">,
265
+ ): () => boolean {
266
+ return () => configStore.get().richDraftPreviews ?? false;
267
+ }
268
+
269
+ export function createTelegramRichDraftPreviewsSetter(
270
+ configStore: TelegramMutableConfigStore,
271
+ ): (enabled: boolean) => Promise<void> {
272
+ return async (enabled) => {
273
+ await loadLatestTelegramConfig(configStore);
274
+ const config = { ...configStore.get(), richDraftPreviews: enabled };
275
+ configStore.set(config);
276
+ await configStore.persist(config);
277
+ };
278
+ }
279
+
262
280
  export function createTelegramVoiceReplyModeGetter(
263
281
  configStore: Pick<TelegramConfigStore, "get">,
264
282
  ): () => "manual" | "mirror" | "always" {
@@ -394,6 +412,10 @@ export function createTelegramConfigControls(
394
412
  return {
395
413
  isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
396
414
  setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
415
+ areRichDraftPreviewsEnabled:
416
+ createTelegramRichDraftPreviewsChecker(configStore),
417
+ setRichDraftPreviewsEnabled:
418
+ createTelegramRichDraftPreviewsSetter(configStore),
397
419
  getVoiceReplyMode: createTelegramVoiceReplyModeGetter(configStore),
398
420
  isVoiceReplyModeConfigured:
399
421
  createTelegramVoiceReplyModeConfiguredChecker(configStore),
@@ -18,6 +18,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
18
18
 
19
19
  export interface TelegramSettingsStateDeps {
20
20
  isProactivePushEnabled: () => boolean;
21
+ areRichDraftPreviewsEnabled: () => boolean;
21
22
  getTimeInjectionMode: () => TelegramTimeMode;
22
23
  getVoiceReplyMode: () => TelegramVoiceReplyMode;
23
24
  isVoiceReplyModeConfigured: () => boolean;
@@ -25,6 +26,7 @@ export interface TelegramSettingsStateDeps {
25
26
 
26
27
  export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
27
28
  setProactivePushEnabled: (enabled: boolean) => Promise<void>;
29
+ setRichDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
28
30
  setVoiceReplyMode: (
29
31
  mode: TelegramVoiceReplyMode | undefined,
30
32
  ) => Promise<void>;
@@ -116,6 +118,8 @@ export interface TelegramSettingsMenuRuntimeDeps<
116
118
 
117
119
  export const SETTINGS_MENU_TITLE = "<b>⚙️ Settings:</b>";
118
120
  export const PROACTIVE_PUSH_SETTINGS_TITLE = "<b>📌 Proactive push:</b>";
121
+ export const RICH_DRAFT_PREVIEWS_SETTINGS_TITLE =
122
+ "<b>📝 Rich draft previews:</b>";
119
123
  export const TIME_INJECTION_MODE_SETTINGS_TITLE =
120
124
  "<b>🕒 Time injection mode:</b>";
121
125
  export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
@@ -151,6 +155,17 @@ export function buildProactivePushSettingsText(
151
155
  ].join("\n");
152
156
  }
153
157
 
158
+ export function buildRichDraftPreviewsSettingsText(enabled: boolean): string {
159
+ return [
160
+ `${RICH_DRAFT_PREVIEWS_SETTINGS_TITLE} <code>${enabled ? "on" : "off"}</code>`,
161
+ "",
162
+ "Show progressive Rich Markdown draft previews while the model is answering.",
163
+ "",
164
+ "<code>-</code> <code>off</code> (default): show native active status, then send the final Rich Markdown answer.",
165
+ "<code>-</code> <code>on</code>: also stream safe Rich Markdown draft previews before the final answer.",
166
+ ].join("\n");
167
+ }
168
+
154
169
  export function buildVoiceReplyModeSettingsText(
155
170
  mode: TelegramVoiceReplyMode,
156
171
  configured = true,
@@ -185,6 +200,7 @@ export function buildTimeInjectionModeSettingsText(
185
200
 
186
201
  export function buildTelegramSettingsMenuReplyMarkup(
187
202
  proactivePushEnabled: boolean,
203
+ richDraftPreviewsEnabled: boolean,
188
204
  voiceReplyMode: TelegramVoiceReplyMode,
189
205
  timeInjectionMode: TelegramTimeMode,
190
206
  sectionRegistry?: TelegramSectionRegistry,
@@ -217,6 +233,12 @@ export function buildTelegramSettingsMenuReplyMarkup(
217
233
  callback_data: "settings:open:time-injection",
218
234
  },
219
235
  ],
236
+ [
237
+ {
238
+ text: `📝 Rich drafts: ${richDraftPreviewsEnabled ? "on" : "off"}`,
239
+ callback_data: "settings:open:rich-drafts",
240
+ },
241
+ ],
220
242
  [
221
243
  {
222
244
  text: `📌 Proactive push: ${proactivePushEnabled ? "on" : "off"}`,
@@ -239,6 +261,7 @@ export async function openTelegramSettingsMenu<
239
261
  buildTelegramSettingsMenuText(),
240
262
  buildTelegramSettingsMenuReplyMarkup(
241
263
  deps.isProactivePushEnabled(),
264
+ deps.areRichDraftPreviewsEnabled(),
242
265
  deps.getVoiceReplyMode(),
243
266
  deps.getTimeInjectionMode(),
244
267
  sectionRegistry,
@@ -271,6 +294,26 @@ export function buildProactivePushSettingsReplyMarkup(
271
294
  };
272
295
  }
273
296
 
297
+ export function buildRichDraftPreviewsSettingsReplyMarkup(
298
+ enabled: boolean,
299
+ ): TelegramSettingsMenuReplyMarkup {
300
+ return {
301
+ inline_keyboard: [
302
+ [{ text: "⬆️ Back", callback_data: "settings:list" }],
303
+ [
304
+ {
305
+ text: enabled ? "🟢 On" : "⚫️ On",
306
+ callback_data: "settings:set:rich-drafts:on",
307
+ },
308
+ {
309
+ text: enabled ? "⚫️ Off" : "🟡 Off",
310
+ callback_data: "settings:set:rich-drafts:off",
311
+ },
312
+ ],
313
+ ],
314
+ };
315
+ }
316
+
274
317
  export function buildTimeInjectionModeSettingsReplyMarkup(
275
318
  mode: TelegramTimeMode,
276
319
  ): TelegramSettingsMenuReplyMarkup {
@@ -320,6 +363,7 @@ export async function updateTelegramSettingsMenuMessage(
320
363
  buildTelegramSettingsMenuText(),
321
364
  buildTelegramSettingsMenuReplyMarkup(
322
365
  deps.isProactivePushEnabled(),
366
+ deps.areRichDraftPreviewsEnabled(),
323
367
  deps.getVoiceReplyMode(),
324
368
  deps.getTimeInjectionMode(),
325
369
  sectionRegistry,
@@ -338,6 +382,16 @@ export async function updateProactivePushSettingsMessage(
338
382
  );
339
383
  }
340
384
 
385
+ export async function updateRichDraftPreviewsSettingsMessage(
386
+ deps: TelegramSettingsMenuCallbackDeps,
387
+ ): Promise<void> {
388
+ const enabled = deps.areRichDraftPreviewsEnabled();
389
+ await deps.updateSettingsMessage(
390
+ buildRichDraftPreviewsSettingsText(enabled),
391
+ buildRichDraftPreviewsSettingsReplyMarkup(enabled),
392
+ );
393
+ }
394
+
341
395
  export async function updateTimeInjectionModeSettingsMessage(
342
396
  deps: TelegramSettingsMenuCallbackDeps,
343
397
  ): Promise<void> {
@@ -375,6 +429,11 @@ export async function handleTelegramSettingsMenuCallbackAction(
375
429
  await deps.answerCallbackQuery(callbackQueryId);
376
430
  return true;
377
431
  }
432
+ if (data === "settings:open:rich-drafts") {
433
+ await updateRichDraftPreviewsSettingsMessage(deps);
434
+ await deps.answerCallbackQuery(callbackQueryId);
435
+ return true;
436
+ }
378
437
  if (data === "settings:open:voice-reply") {
379
438
  await updateVoiceReplyModeSettingsMessage(deps);
380
439
  await deps.answerCallbackQuery(callbackQueryId);
@@ -427,6 +486,19 @@ export async function handleTelegramSettingsMenuCallbackAction(
427
486
  return true;
428
487
  }
429
488
  }
489
+ if (
490
+ data === "settings:set:rich-drafts:on" ||
491
+ data === "settings:set:rich-drafts:off"
492
+ ) {
493
+ const enabled = data.endsWith(":on");
494
+ await deps.setRichDraftPreviewsEnabled(enabled);
495
+ await updateRichDraftPreviewsSettingsMessage(deps);
496
+ await deps.answerCallbackQuery(
497
+ callbackQueryId,
498
+ `Rich draft previews ${enabled ? "enabled" : "disabled"}`,
499
+ );
500
+ return true;
501
+ }
430
502
  if (
431
503
  data === "settings:set:proactive:on" ||
432
504
  data === "settings:set:proactive:off"
@@ -457,6 +529,7 @@ export function createTelegramSettingsMenuRuntime<
457
529
  {
458
530
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
459
531
  isProactivePushEnabled: deps.isProactivePushEnabled,
532
+ areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
460
533
  getVoiceReplyMode: deps.getVoiceReplyMode,
461
534
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
462
535
  getTimeInjectionMode: deps.getTimeInjectionMode,
@@ -475,6 +548,7 @@ export function createTelegramSettingsMenuRuntime<
475
548
  updateTelegramSettingsMenuMessage(
476
549
  {
477
550
  isProactivePushEnabled: deps.isProactivePushEnabled,
551
+ areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
478
552
  getVoiceReplyMode: deps.getVoiceReplyMode,
479
553
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
480
554
  getTimeInjectionMode: deps.getTimeInjectionMode,
@@ -513,6 +587,18 @@ export function createTelegramSettingsMenuRuntime<
513
587
  );
514
588
  return true;
515
589
  }
590
+ if (
591
+ query.data === "settings:set:rich-drafts:on" ||
592
+ query.data === "settings:set:rich-drafts:off"
593
+ ) {
594
+ const enabled = query.data.endsWith(":on");
595
+ await deps.setRichDraftPreviewsEnabled(enabled);
596
+ await deps.answerCallbackQuery(
597
+ query.id,
598
+ `Rich draft previews ${enabled ? "enabled" : "disabled"}`,
599
+ );
600
+ return true;
601
+ }
516
602
  const hasTimeInjectionPrefix = query.data.startsWith(
517
603
  "settings:set:time-injection:",
518
604
  );
@@ -543,10 +629,12 @@ export function createTelegramSettingsMenuRuntime<
543
629
  }
544
630
  return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
545
631
  isProactivePushEnabled: deps.isProactivePushEnabled,
632
+ areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
546
633
  getVoiceReplyMode: deps.getVoiceReplyMode,
547
634
  isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
548
635
  getTimeInjectionMode: deps.getTimeInjectionMode,
549
636
  setProactivePushEnabled: deps.setProactivePushEnabled,
637
+ setRichDraftPreviewsEnabled: deps.setRichDraftPreviewsEnabled,
550
638
  setVoiceReplyMode: deps.setVoiceReplyMode,
551
639
  setTimeInjectionMode: deps.setTimeInjectionMode,
552
640
  updateSettingsMessage: (text, replyMarkup) =>
package/lib/updates.ts CHANGED
@@ -1110,7 +1110,7 @@ export async function executeTelegramUpdatePlan<
1110
1110
  if (plan.shouldDeny) {
1111
1111
  await deps.answerGuestQuery(
1112
1112
  plan.guestMessage.guest_query_id,
1113
- "Access denied.",
1113
+ "🚫 Access denied.",
1114
1114
  );
1115
1115
  return;
1116
1116
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.18.6",
3
+ "version": "0.19.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -42,7 +42,7 @@
42
42
  "BACKLOG.md",
43
43
  "CHANGELOG.md",
44
44
  "docs/",
45
- "screenshot.png"
45
+ "banner.png"
46
46
  ],
47
47
  "exports": {
48
48
  ".": "./index.ts",
File without changes