@llblab/pi-telegram 0.11.2 → 0.12.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 +15 -11
- package/BACKLOG.md +0 -10
- package/CHANGELOG.md +18 -0
- package/README.md +18 -14
- package/api/inbound.ts +14 -0
- package/api/keyboard.ts +10 -0
- package/api/outbound.ts +11 -0
- package/api/sections.ts +17 -0
- package/api/updates.ts +11 -0
- package/api/voice.ts +24 -0
- package/docs/README.md +7 -5
- package/docs/architecture.md +160 -225
- package/docs/callback-namespaces.md +3 -3
- package/docs/{inbound-handlers.md → inbound.md} +13 -10
- package/docs/locks.md +3 -3
- package/docs/{outbound-handlers.md → outbound.md} +13 -10
- package/docs/public-api.md +266 -0
- package/docs/{extension-sections.md → sections.md} +31 -27
- package/docs/ui-style.md +165 -0
- package/docs/{external-handlers.md → updates.md} +33 -31
- package/docs/voice.md +17 -14
- package/index.ts +84 -238
- package/lib/bindings.ts +301 -0
- package/lib/commands.ts +114 -1
- package/lib/config.ts +43 -2
- package/lib/{inbound-handlers.ts → inbound.ts} +5 -4
- package/lib/lifecycle.ts +41 -6
- package/lib/menu-model.ts +3 -3
- package/lib/menu-queue.ts +1 -1
- package/lib/menu-settings.ts +21 -10
- package/lib/menu-status.ts +1 -1
- package/lib/menu.ts +1 -1
- package/lib/{outbound-handlers.ts → outbound.ts} +21 -11
- package/lib/polling.ts +4 -3
- package/lib/preview.ts +1 -1
- package/lib/routing.ts +44 -3
- package/lib/{extension-sections.ts → sections.ts} +37 -8
- package/lib/updates.ts +121 -1
- package/lib/voice.ts +33 -14
- package/package.json +11 -1
- package/lib/external-handlers.ts +0 -166
|
@@ -13,7 +13,7 @@ An outbound handler is selected by `type`. Text replies and assistant markup map
|
|
|
13
13
|
| Source | Handler | Action |
|
|
14
14
|
| ----------------- | ----------------------------- | ----------------------- |
|
|
15
15
|
| Final text | `outboundHandlers[type=text]` | Transform before render |
|
|
16
|
-
| `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice`
|
|
16
|
+
| `telegram_voice` | Voice pipeline | OGG/Opus `sendVoice` |
|
|
17
17
|
| `telegram_button` | Built-in | Attach inline button |
|
|
18
18
|
|
|
19
19
|
The voice pipeline is detailed below: configured `type: "voice"` handlers first, then programmatic handlers, then registered synthesis providers.
|
|
@@ -60,24 +60,27 @@ Voice replies use one fallback pipeline:
|
|
|
60
60
|
|
|
61
61
|
1. configured `outboundHandlers` with `type: "voice"` in `telegram.json` order
|
|
62
62
|
2. programmatic `registerTelegramOutboundHandler("voice", ...)` handlers
|
|
63
|
-
3. registered voice synthesis providers from `@llblab/pi-telegram/
|
|
63
|
+
3. registered voice synthesis providers from `@llblab/pi-telegram/voice`
|
|
64
64
|
|
|
65
65
|
This makes provider extensions a zero-config convenience without overriding explicit operator-owned `telegram.json` handlers. If several synthesis providers are registered, they are tried in registration order; the first provider that returns a valid `.ogg`/`.opus` artifact handles the reply. Returning `undefined` passes to the next provider, while thrown errors or invalid files are recorded before the next fallback is tried.
|
|
66
66
|
|
|
67
67
|
## Voice Synthesis Provider API
|
|
68
68
|
|
|
69
|
-
Voice replies can be delivered by synthesis providers registered through `@llblab/pi-telegram/
|
|
69
|
+
Voice replies can be delivered by synthesis providers registered through `@llblab/pi-telegram/voice`:
|
|
70
70
|
|
|
71
71
|
```ts
|
|
72
|
-
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/
|
|
73
|
-
|
|
74
|
-
const dispose = registerTelegramVoiceSynthesisProvider(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
72
|
+
import { registerTelegramVoiceSynthesisProvider } from "@llblab/pi-telegram/voice";
|
|
73
|
+
|
|
74
|
+
const dispose = registerTelegramVoiceSynthesisProvider(
|
|
75
|
+
async (text, options) => {
|
|
76
|
+
const audioPath = await synthesizeToOggOpus(text, options);
|
|
77
|
+
return { audioPath, transcriptText: text };
|
|
78
|
+
},
|
|
79
|
+
{ id: "my-extension/tts" },
|
|
80
|
+
);
|
|
78
81
|
```
|
|
79
82
|
|
|
80
|
-
Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints.
|
|
83
|
+
Synthesis providers receive the extracted `telegram_voice` text plus optional `lang`/`rate` hints. Stable registrations pass a durable `id`; omitted ids remain a compatibility path for older providers. Providers own translation, TTS, speech rewriting, transcript choice, and OGG/Opus conversion. The bridge validates that the returned file ends in `.ogg` or `.opus`, sends it through Telegram `sendVoice`, and falls back to planned text if delivery fails before any visible text was delivered. Providers run after configured and programmatic voice handlers in the priority chain above.
|
|
81
84
|
|
|
82
85
|
## Voice Markup
|
|
83
86
|
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
# Public API
|
|
2
|
+
|
|
3
|
+
`pi-telegram` is both a π extension and a small Telegram platform for companion extensions. This document defines the stable public surface. Everything outside this document is implementation detail unless another focused doc explicitly marks it stable.
|
|
4
|
+
|
|
5
|
+
## Stability Levels
|
|
6
|
+
|
|
7
|
+
- **Stable:** documented here and covered by compatibility expectations.
|
|
8
|
+
- **Advanced stable:** public for extension authors, but lower-level; prefer higher-level APIs when possible.
|
|
9
|
+
- **Compatibility:** older import/config paths that remain supported but should not be used for new code.
|
|
10
|
+
- **Internal:** exported from source for tests or domain reuse, but not a compatibility promise.
|
|
11
|
+
|
|
12
|
+
## Package Entrypoints
|
|
13
|
+
|
|
14
|
+
Preferred public imports:
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import telegram from "@llblab/pi-telegram";
|
|
18
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
19
|
+
import { registerTelegramUpdateHandler } from "@llblab/pi-telegram/updates";
|
|
20
|
+
import { registerTelegramInboundHandler } from "@llblab/pi-telegram/inbound";
|
|
21
|
+
import { registerTelegramOutboundHandler } from "@llblab/pi-telegram/outbound";
|
|
22
|
+
import {
|
|
23
|
+
registerTelegramVoiceSynthesisProvider,
|
|
24
|
+
registerTelegramVoiceTranscriptionProvider,
|
|
25
|
+
} from "@llblab/pi-telegram/voice";
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`0.12.0` intentionally removes the published `@llblab/pi-telegram/lib/*.ts` compatibility wildcard. Integrations should use the public API domain subpaths above. Package exports point at `/api/*.ts` membranes that re-export only stable companion-extension symbols; implementation modules under `lib/` remain package-private.
|
|
29
|
+
|
|
30
|
+
## User-Facing API
|
|
31
|
+
|
|
32
|
+
### π commands
|
|
33
|
+
|
|
34
|
+
Stable commands inside π:
|
|
35
|
+
|
|
36
|
+
- `/telegram-setup` — configure/update the bot token.
|
|
37
|
+
- `/telegram-connect` — start polling in the current session and acquire ownership.
|
|
38
|
+
- `/telegram-disconnect` — stop polling and release ownership.
|
|
39
|
+
- `/telegram-status` — show connection, polling, execution, queue, and recent event diagnostics.
|
|
40
|
+
|
|
41
|
+
### Telegram commands
|
|
42
|
+
|
|
43
|
+
Stable commands inside the paired Telegram DM:
|
|
44
|
+
|
|
45
|
+
- `/start` — pair when needed and open the main application menu.
|
|
46
|
+
- `/compact` — open confirmation and compact when idle.
|
|
47
|
+
- `/next` — dispatch the next queued turn, aborting active work first when needed.
|
|
48
|
+
- `/continue` — enqueue a priority `continue` prompt.
|
|
49
|
+
- `/abort` — abort active Telegram-owned work and keep the queue.
|
|
50
|
+
- `/stop` — abort active Telegram-owned work and clear waiting Telegram queue items.
|
|
51
|
+
|
|
52
|
+
Hidden compatibility shortcuts may open sections directly: `/help`, `/status`, `/model`, `/thinking`, `/queue`, and `/settings`.
|
|
53
|
+
|
|
54
|
+
### Tools and assistant-authored actions
|
|
55
|
+
|
|
56
|
+
- `telegram_attach(paths)` is the stable artifact delivery tool for generated files.
|
|
57
|
+
- `telegram_voice` hidden comments request Telegram-native voice delivery.
|
|
58
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts.
|
|
59
|
+
|
|
60
|
+
See [Outbound Handlers](./outbound.md) for exact markup forms.
|
|
61
|
+
|
|
62
|
+
## Configuration API
|
|
63
|
+
|
|
64
|
+
Configuration lives in `~/.pi/agent/telegram.json` unless `PI_CODING_AGENT_DIR` changes the agent root.
|
|
65
|
+
|
|
66
|
+
Stable config keys:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
interface TelegramConfig {
|
|
70
|
+
botToken?: string;
|
|
71
|
+
botUsername?: string; // runtime-managed
|
|
72
|
+
botId?: number; // runtime-managed
|
|
73
|
+
allowedUserId?: number;
|
|
74
|
+
lastUpdateId?: number; // runtime-managed
|
|
75
|
+
proactivePush?: boolean;
|
|
76
|
+
inboundHandlers?: TelegramInboundHandlerConfig[];
|
|
77
|
+
attachmentHandlers?: TelegramInboundHandlerConfig[]; // compatibility alias
|
|
78
|
+
outboundHandlers?: TelegramOutboundHandlerConfig[];
|
|
79
|
+
voice?: {
|
|
80
|
+
replyMode?: "manual" | "mirror" | "always";
|
|
81
|
+
sendTranscript?: boolean;
|
|
82
|
+
};
|
|
83
|
+
time?: {
|
|
84
|
+
injectionMode?: "hidden" | "always" | "interval";
|
|
85
|
+
interval?: number;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Hidden/default semantics are represented by absence:
|
|
91
|
+
|
|
92
|
+
- Voice Reply `hidden`: no `voice.replyMode` key is persisted.
|
|
93
|
+
- Time Injection `hidden`: no `time.injectionMode` key is persisted; if `time` becomes empty, the whole `time` object may be omitted.
|
|
94
|
+
|
|
95
|
+
Environment variables are stable only where documented in the README: bot-token bootstrap, proxy behavior, agent root, and inbound/outbound file size limits.
|
|
96
|
+
|
|
97
|
+
## Programmatic API Matrix
|
|
98
|
+
|
|
99
|
+
High-level stable APIs:
|
|
100
|
+
|
|
101
|
+
- `registerTelegramSection()`
|
|
102
|
+
- Identity: required `id`.
|
|
103
|
+
- Purpose: managed menu/settings UI surfaces.
|
|
104
|
+
- `registerTelegramVoiceTranscriptionProvider()`
|
|
105
|
+
- Identity: required stable `id` for new code.
|
|
106
|
+
- Purpose: STT fallback for voice/audio input.
|
|
107
|
+
- `registerTelegramVoiceSynthesisProvider()`
|
|
108
|
+
- Identity: required stable `id` for new code.
|
|
109
|
+
- Purpose: TTS fallback for Telegram voice replies.
|
|
110
|
+
|
|
111
|
+
Low-level stable buses:
|
|
112
|
+
|
|
113
|
+
- `registerTelegramUpdateHandler()`
|
|
114
|
+
- Identity: no id.
|
|
115
|
+
- Purpose: observe or consume raw Telegram updates before default routing.
|
|
116
|
+
- `registerTelegramInboundHandler()`
|
|
117
|
+
- Identity: no id.
|
|
118
|
+
- Purpose: generic Telegram-to-π transforms.
|
|
119
|
+
- `registerTelegramOutboundHandler()`
|
|
120
|
+
- Identity: no id.
|
|
121
|
+
- Purpose: generic final-reply transforms or voice command fallbacks.
|
|
122
|
+
|
|
123
|
+
Advanced stable diagnostics:
|
|
124
|
+
|
|
125
|
+
- `recordTelegramRuntimeEvent()`
|
|
126
|
+
- Identity: caller supplies category.
|
|
127
|
+
- Purpose: surface companion diagnostics in `/telegram-status`.
|
|
128
|
+
|
|
129
|
+
All registration APIs return a disposer. Companion extensions should call disposers on shutdown and re-register on session start when they recreate runtime state. Low-level bus APIs intentionally avoid ids and run in registration order. High-level provider/UI APIs require stable identity in their public contract so diagnostics, replacement, and cleanup are understandable. Generated voice-provider ids remain a temporary compatibility path where documented.
|
|
130
|
+
|
|
131
|
+
## Sections
|
|
132
|
+
|
|
133
|
+
Import from `@llblab/pi-telegram/sections`.
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
const unregister = registerTelegramSection({
|
|
137
|
+
id: "@scope/my-extension",
|
|
138
|
+
label: "🧩 My extension",
|
|
139
|
+
order: 10,
|
|
140
|
+
render: async (ctx) => ({
|
|
141
|
+
text: "<b>My extension</b>",
|
|
142
|
+
parseMode: "html",
|
|
143
|
+
replyMarkup: {
|
|
144
|
+
inline_keyboard: [
|
|
145
|
+
[{ text: "▶️ Run", callback_data: ctx.callbackData("run") }],
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
}),
|
|
149
|
+
handleCallback: async (ctx) => {
|
|
150
|
+
if (ctx.action !== "run") return "pass";
|
|
151
|
+
await ctx.enqueuePrompt("Run my extension workflow.");
|
|
152
|
+
await ctx.answerCallback("Queued");
|
|
153
|
+
return "handled";
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Contract:
|
|
159
|
+
|
|
160
|
+
- `id` is unique per active registry. Duplicate ids are rejected.
|
|
161
|
+
- `ctx.callbackData(action, payload?)` builds compact `section:` callbacks and validates Telegram's 64-byte limit.
|
|
162
|
+
- `ctx.edit()` and `ctx.open()` auto-prepend the correct Back/Main-menu row.
|
|
163
|
+
- Section errors are isolated and surfaced as callback popups/diagnostics.
|
|
164
|
+
|
|
165
|
+
Full behavior: [Extension Sections](./sections.md).
|
|
166
|
+
|
|
167
|
+
## Updates
|
|
168
|
+
|
|
169
|
+
Import from `@llblab/pi-telegram/updates`.
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const off = registerTelegramUpdateHandler(async (update) => {
|
|
173
|
+
const data = (update as { callback_query?: { data?: string } }).callback_query
|
|
174
|
+
?.data;
|
|
175
|
+
if (!data?.startsWith("myext:")) return "pass";
|
|
176
|
+
await handleMyCallback(data);
|
|
177
|
+
return "consume";
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Use this as a low-level escape hatch. Prefer sections for menu-integrated UI.
|
|
182
|
+
|
|
183
|
+
Full behavior: [Updates](./updates.md).
|
|
184
|
+
|
|
185
|
+
## Inbound
|
|
186
|
+
|
|
187
|
+
Import from `@llblab/pi-telegram/inbound`.
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
const off = registerTelegramInboundHandler("document", async ({ file }) => {
|
|
191
|
+
if (!file?.mimeType?.includes("pdf")) return undefined;
|
|
192
|
+
return await extractPdfText(file.path);
|
|
193
|
+
});
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
Priority order:
|
|
197
|
+
|
|
198
|
+
1. configured `inboundHandlers`
|
|
199
|
+
2. compatibility `attachmentHandlers`
|
|
200
|
+
3. programmatic inbound handlers
|
|
201
|
+
4. voice transcription providers
|
|
202
|
+
5. built-in text-file fallback
|
|
203
|
+
|
|
204
|
+
Full behavior: [Inbound Handlers](./inbound.md).
|
|
205
|
+
|
|
206
|
+
## Outbound
|
|
207
|
+
|
|
208
|
+
Import from `@llblab/pi-telegram/outbound`.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const off = registerTelegramOutboundHandler("text", async (text) => {
|
|
212
|
+
return await rewriteFinalText(text);
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Programmatic outbound handlers are fallbacks/transformers behind operator-owned `telegram.json` configuration. Voice delivery priority is configured voice handlers, then programmatic `voice` handlers, then synthesis providers.
|
|
217
|
+
|
|
218
|
+
Full behavior: [Outbound Handlers](./outbound.md).
|
|
219
|
+
|
|
220
|
+
## Voice Providers
|
|
221
|
+
|
|
222
|
+
Import from `@llblab/pi-telegram/voice`.
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
const offStt = registerTelegramVoiceTranscriptionProvider(
|
|
226
|
+
async (file) => {
|
|
227
|
+
if (file.kind !== "voice" && file.kind !== "audio") return undefined;
|
|
228
|
+
return { text: await transcribe(file.path) };
|
|
229
|
+
},
|
|
230
|
+
{ id: "@scope/my-extension/stt" },
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const offTts = registerTelegramVoiceSynthesisProvider(
|
|
234
|
+
async (text, options) => {
|
|
235
|
+
const audioPath = await synthesizeOggOpus(text, options);
|
|
236
|
+
return { audioPath, transcriptText: text };
|
|
237
|
+
},
|
|
238
|
+
{ id: "@scope/my-extension/tts" },
|
|
239
|
+
);
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Stable voice-provider registrations pass a durable `id`. Omitting `id` is a compatibility path for older providers and receives a generated session-local id. Providers return `undefined` to pass. TTS providers must return `.ogg` or `.opus` files for native Telegram voice notes.
|
|
243
|
+
|
|
244
|
+
Full behavior: [Voice Integration](./voice.md).
|
|
245
|
+
|
|
246
|
+
## Callback Namespaces
|
|
247
|
+
|
|
248
|
+
Owned prefixes are reserved by `pi-telegram`: `compact:`, `tgbtn:`, `menu:`, `model:`, `thinking:`, `status:`, `queue:`, `settings:`, and `section:`.
|
|
249
|
+
|
|
250
|
+
Companion extensions should use their own short prefix for raw callbacks or use `ctx.callbackData()` inside sections. Unknown unowned callbacks may be forwarded to π as `[callback] <data>` after built-in handlers decline them.
|
|
251
|
+
|
|
252
|
+
Full behavior: [Callback Namespaces](./callback-namespaces.md).
|
|
253
|
+
|
|
254
|
+
## Internal Surface
|
|
255
|
+
|
|
256
|
+
The following are not stable public contracts unless explicitly documented elsewhere:
|
|
257
|
+
|
|
258
|
+
- queue/runtime/lifecycle stores and planners
|
|
259
|
+
- menu implementation helpers
|
|
260
|
+
- polling/lock internals
|
|
261
|
+
- Telegram API transport helpers
|
|
262
|
+
- rendering internals
|
|
263
|
+
- command implementation helpers
|
|
264
|
+
- test support functions
|
|
265
|
+
|
|
266
|
+
They are intentionally not exposed through a `./lib/*.ts` export wildcard in `0.12.0`.
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
Telegram Extension Sections let ordinary pi extensions add structured UI surfaces to the `pi-telegram` inline application menu. The platform mirrors π's own extensibility model: small, composable extensions that plug into a shared shell without owning transport, polling, authorization, or menu lifecycle.
|
|
12
12
|
|
|
13
|
-
`pi-telegram` stays the single bot operator. Extensions register typed sections; the bridge handles rendering, callback routing, token mapping, navigation hierarchy, and diagnostics. No second
|
|
13
|
+
`pi-telegram` stays the single bot operator. Extensions register typed sections; the bridge handles rendering, callback routing, token mapping, navigation hierarchy, and diagnostics. No second polling loop, no new loader — just one `registerTelegramSection()` call.
|
|
14
14
|
|
|
15
15
|
## 2. Contract Layers
|
|
16
16
|
|
|
@@ -39,7 +39,7 @@ The `id` is the owner identity. No separate `owner` field. Used for registry own
|
|
|
39
39
|
## 4. Registration Shape
|
|
40
40
|
|
|
41
41
|
```ts
|
|
42
|
-
import { registerTelegramSection } from "@llblab/pi-telegram/
|
|
42
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/sections";
|
|
43
43
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
44
44
|
|
|
45
45
|
export default function (pi: ExtensionAPI) {
|
|
@@ -127,12 +127,12 @@ unregister(); // removes from main menu, settings, and callback routing
|
|
|
127
127
|
|
|
128
128
|
Two paths, same registry:
|
|
129
129
|
|
|
130
|
-
**Typed import (preferred):** Extension imports `registerTelegramSection` from `@llblab/pi-telegram/
|
|
130
|
+
**Typed import (preferred):** Extension imports `registerTelegramSection` from `@llblab/pi-telegram/sections`. The function reads from a `globalThis` registry set by `pi-telegram` at startup. In `0.12.0`, package-private `@llblab/pi-telegram/lib/*.ts` deep imports are no longer exported.
|
|
131
131
|
|
|
132
|
-
**Relative import (local):** When the extension cannot resolve `@llblab/pi-telegram` as an npm package, use a relative path:
|
|
132
|
+
**Relative import (local):** When the extension cannot resolve `@llblab/pi-telegram` as an npm package, use the public API membrane via a relative path:
|
|
133
133
|
|
|
134
134
|
```ts
|
|
135
|
-
import { registerTelegramSection } from "../pi-telegram/
|
|
135
|
+
import { registerTelegramSection } from "../pi-telegram/api/sections.ts";
|
|
136
136
|
```
|
|
137
137
|
|
|
138
138
|
**GlobalThis bridge (zero-coupling):** `pi-telegram` exposes `__piTelegramSectionRegistry__` on `globalThis`. The typed import is a thin wrapper. Extensions never touch the raw registry.
|
|
@@ -187,18 +187,19 @@ section:<token>:<action>:<payload>
|
|
|
187
187
|
|
|
188
188
|
Example: `section:0:counter:5`
|
|
189
189
|
|
|
190
|
-
The token is an implementation detail. Section authors **never** write `section:` strings manually. Use `ctx.callbackData(action, payload?)` which fills in the correct token.
|
|
190
|
+
The token is an implementation detail. Section authors **never** write `section:` strings manually. Use `ctx.callbackData(action, payload?)` which fills in the correct token and rejects callback data above Telegram's 64-byte limit.
|
|
191
191
|
|
|
192
192
|
### Routing order
|
|
193
193
|
|
|
194
|
-
1. Telegram update arrives through the single `pi-telegram`
|
|
195
|
-
2.
|
|
194
|
+
1. Telegram update arrives through the single `pi-telegram` polling loop
|
|
195
|
+
2. Update handlers observe/consume (raw update interception)
|
|
196
196
|
3. Button action store (`tgbtn:*`)
|
|
197
|
-
4.
|
|
198
|
-
5.
|
|
199
|
-
6.
|
|
200
|
-
7.
|
|
201
|
-
8.
|
|
197
|
+
4. Compact confirmation callbacks (`compact:*`)
|
|
198
|
+
5. Queue menu callbacks (`queue:*`)
|
|
199
|
+
6. Settings menu callbacks (`settings:*`)
|
|
200
|
+
7. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
|
|
201
|
+
8. Section callbacks (`section:*`) — dispatched before step 7's full handler
|
|
202
|
+
9. Unknown callbacks fall back to `[callback]` prompt text
|
|
202
203
|
|
|
203
204
|
### Handler return values
|
|
204
205
|
|
|
@@ -221,7 +222,7 @@ If a section is unregistered or a token is unknown, the callback is answered wit
|
|
|
221
222
|
|
|
222
223
|
> "This section is no longer available."
|
|
223
224
|
|
|
224
|
-
Section errors are caught and surfaced as popup text. No unhandled exceptions leak to
|
|
225
|
+
Section errors are caught and surfaced as popup text. No unhandled exceptions leak to polling.
|
|
225
226
|
|
|
226
227
|
## 8. Navigation Hierarchy
|
|
227
228
|
|
|
@@ -298,7 +299,7 @@ Context ports are intentionally narrow. Sections **cannot**:
|
|
|
298
299
|
|
|
299
300
|
- Read/write filesystem
|
|
300
301
|
- Access raw process or bot clients
|
|
301
|
-
- Start a second
|
|
302
|
+
- Start a second polling loop
|
|
302
303
|
- Mutate session state
|
|
303
304
|
- Send arbitrary Telegram API calls
|
|
304
305
|
|
|
@@ -320,12 +321,15 @@ handleCallback: async (ctx) => {
|
|
|
320
321
|
text: `<b>Delete ${ctx.payload}?</b>\n\nThis cannot be undone.`,
|
|
321
322
|
parseMode: "html",
|
|
322
323
|
replyMarkup: {
|
|
323
|
-
inline_keyboard: [
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
324
|
+
inline_keyboard: [
|
|
325
|
+
[
|
|
326
|
+
{
|
|
327
|
+
text: "✅ Yes, delete",
|
|
328
|
+
callback_data: ctx.callbackData("confirm-delete", ctx.payload),
|
|
329
|
+
},
|
|
330
|
+
{ text: "❌ Cancel", callback_data: ctx.callbackData("cancel") },
|
|
331
|
+
],
|
|
332
|
+
],
|
|
329
333
|
},
|
|
330
334
|
});
|
|
331
335
|
return "handled";
|
|
@@ -339,7 +343,7 @@ handleCallback: async (ctx) => {
|
|
|
339
343
|
await ctx.deleteMessage();
|
|
340
344
|
return "handled";
|
|
341
345
|
}
|
|
342
|
-
}
|
|
346
|
+
};
|
|
343
347
|
```
|
|
344
348
|
|
|
345
349
|
`ctx.deleteMessage()` removes the dialog from chat after the user makes a choice. Callbacks from chat buttons route through the same `handleCallback` — the same `ctx.callbackData()` works regardless of where the button lives. The extension owns its callback namespace; the bridge owns transport.
|
|
@@ -356,7 +360,7 @@ section:0:settings:open → open settings root
|
|
|
356
360
|
section:0:<action>:<payload> → forwarded to handleCallback
|
|
357
361
|
```
|
|
358
362
|
|
|
359
|
-
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
363
|
+
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `compact:`, `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
360
364
|
|
|
361
365
|
### Inline keyboard layout
|
|
362
366
|
|
|
@@ -378,7 +382,7 @@ The platform inherits from π's own extension model:
|
|
|
378
382
|
|
|
379
383
|
- `export default function(pi)` → `registerTelegramSection(section)`
|
|
380
384
|
- `pi.on("shutdown", ...)` → disposer from `registerTelegramSection`
|
|
381
|
-
- Typed imports → typed import from `@llblab/pi-telegram/
|
|
385
|
+
- Typed imports → typed import from `@llblab/pi-telegram/sections`
|
|
382
386
|
- `globalThis` registry → `__piTelegramSectionRegistry__` on `globalThis`
|
|
383
387
|
- Identity from `package.json/name` → same identity rules as Locks Standard
|
|
384
388
|
- Narrow typed context ports → `TelegramSectionContext` / `TelegramSectionCallbackContext`
|
|
@@ -419,7 +423,7 @@ Available programmatically via `getTelegramSectionDiagnostics()`. Section runtim
|
|
|
419
423
|
|
|
420
424
|
### Non-goals:
|
|
421
425
|
|
|
422
|
-
- No second Telegram
|
|
426
|
+
- No second Telegram polling loop
|
|
423
427
|
- No new pi extension loader
|
|
424
428
|
- No generic webview system
|
|
425
429
|
- No default filesystem mutation API
|
|
@@ -429,8 +433,8 @@ Available programmatically via `getTelegramSectionDiagnostics()`. Section runtim
|
|
|
429
433
|
## 14. Relationship to Other Standards
|
|
430
434
|
|
|
431
435
|
- [Callback Namespaces](./callback-namespaces.md): defines `section:` as pi-telegram-owned prefix. Sections use namespaced callbacks but authors never hand-roll them
|
|
432
|
-
- [
|
|
433
|
-
- [Extension Locks](../docs/locks.md) (
|
|
436
|
+
- [Updates](./updates.md): raw update interception for direct Telegram update access. Sections are the structured UI layer above
|
|
437
|
+
- [Extension Locks](../docs/locks.md) (polling): same identity key rules (`package.json/name` → canonical id)
|
|
434
438
|
- [Command Templates](./command-templates.md): sections do not execute command templates by default. UI registration + callback routing, not shell execution
|
|
435
439
|
|
|
436
440
|
## 15. Demo Extension
|
package/docs/ui-style.md
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# UI Style Guide
|
|
2
|
+
|
|
3
|
+
Small standard for inline buttons, menu rows, state controls, cards, and confirmation dialogs.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
- Keep UI compact and phone-readable.
|
|
8
|
+
- Put emoji where they help scanning, not everywhere.
|
|
9
|
+
- Use one strong indicator for current selection; avoid emoji noise on every option.
|
|
10
|
+
- Match label casing to control role.
|
|
11
|
+
- Prefer minimal, clear configuration UI over exhaustive explanation.
|
|
12
|
+
- Preserve domain-owned callback prefixes and behavior in the owning module.
|
|
13
|
+
|
|
14
|
+
## Action Buttons
|
|
15
|
+
|
|
16
|
+
Action buttons perform an operation.
|
|
17
|
+
|
|
18
|
+
Rules:
|
|
19
|
+
|
|
20
|
+
- Use an emoji plus capitalized action text.
|
|
21
|
+
- Prefer direct verb or action noun.
|
|
22
|
+
- Keep labels short.
|
|
23
|
+
|
|
24
|
+
Examples:
|
|
25
|
+
|
|
26
|
+
- `🗜 Yes, compact`
|
|
27
|
+
- `❌ No`
|
|
28
|
+
- `🗑 Yes, delete`
|
|
29
|
+
- `☑️ Activate`
|
|
30
|
+
|
|
31
|
+
## State & Navigation Buttons
|
|
32
|
+
|
|
33
|
+
State buttons show the current state and navigate to a submenu or detail rather than performing an operation directly.
|
|
34
|
+
|
|
35
|
+
Rules:
|
|
36
|
+
|
|
37
|
+
- Use an emoji that reflects the current state.
|
|
38
|
+
- Use Capitalized, descriptive state text.
|
|
39
|
+
- Tapping opens a submenu or returns to the parent list.
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
|
|
43
|
+
- `🟢 Active` — model detail, navigates back to model list
|
|
44
|
+
- `📌 Proactive push: On` — settings row, opens the toggle submenu
|
|
45
|
+
- `👄 Voice reply: Mirror` — settings row, opens the option list
|
|
46
|
+
|
|
47
|
+
## Boolean Toggles
|
|
48
|
+
|
|
49
|
+
Boolean settings use a horizontal `On` / `Off` pair, like a checkbox stretched across two buttons.
|
|
50
|
+
|
|
51
|
+
Rules:
|
|
52
|
+
|
|
53
|
+
- Keep the pair in one row: `On` left, `Off` right.
|
|
54
|
+
- Use Capitalized labels.
|
|
55
|
+
- Always show an indicator on both buttons to avoid horizontal label shift.
|
|
56
|
+
- Mark active `On` with `🟢`.
|
|
57
|
+
- Mark active `Off` with `🟡`.
|
|
58
|
+
- Mark the inactive value with `⚫️`.
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
|
|
62
|
+
- `🟢 On` / `⚫️ Off`
|
|
63
|
+
- `⚫️ On` / `🟡 Off`
|
|
64
|
+
|
|
65
|
+
## Horizontal Tabs
|
|
66
|
+
|
|
67
|
+
Tabs or small mutually-exclusive scopes use a horizontal row.
|
|
68
|
+
|
|
69
|
+
Rules:
|
|
70
|
+
|
|
71
|
+
- Use Capitalized labels.
|
|
72
|
+
- Always show an indicator on every tab to avoid horizontal label shift.
|
|
73
|
+
- Use active tab color to convey semantics:
|
|
74
|
+
- `🟣` for the default / normal state (All models, Normal priority).
|
|
75
|
+
- `🟡` for an elevated or filtered state (Scoped models, Priority).
|
|
76
|
+
- `🟣` for neutral navigation controls (page picker).
|
|
77
|
+
- Mark inactive tabs with `⚫️`.
|
|
78
|
+
|
|
79
|
+
Examples:
|
|
80
|
+
|
|
81
|
+
- `🟡 Scoped` / `⚫️ All`
|
|
82
|
+
- `⚫️ Priority` / `🟣 Normal`
|
|
83
|
+
- `1` / `🟣 2` / `3`
|
|
84
|
+
|
|
85
|
+
## Vertical Option Lists
|
|
86
|
+
|
|
87
|
+
Vertical option lists choose one value from a potentially longer list, for example model selection, thinking level, voice reply mode, or time injection mode.
|
|
88
|
+
|
|
89
|
+
Rules:
|
|
90
|
+
|
|
91
|
+
- Put each option on its own row.
|
|
92
|
+
- Mark only the current value with `🟢`.
|
|
93
|
+
- Leave non-current values without emoji.
|
|
94
|
+
- Use lowercase labels when the option is a value.
|
|
95
|
+
|
|
96
|
+
Examples:
|
|
97
|
+
|
|
98
|
+
- `🟢 mirror`
|
|
99
|
+
- `manual`
|
|
100
|
+
- `always`
|
|
101
|
+
|
|
102
|
+
## Navigation
|
|
103
|
+
|
|
104
|
+
Inline submenu navigation is hierarchical.
|
|
105
|
+
|
|
106
|
+
Rules:
|
|
107
|
+
|
|
108
|
+
- Put the navigation row first.
|
|
109
|
+
- First-level submenus opened from the main inline menu start with `⬆️ Main menu`.
|
|
110
|
+
- Deeper submenus start with `⬆️ Back`.
|
|
111
|
+
- `Main menu` returns to the root inline menu.
|
|
112
|
+
- `Back` returns one level up, never directly to the root unless the parent is the root.
|
|
113
|
+
|
|
114
|
+
Examples:
|
|
115
|
+
|
|
116
|
+
- Main menu → Settings: first row is `⬆️ Main menu`.
|
|
117
|
+
- Settings → Voice reply mode: first row is `⬆️ Back`.
|
|
118
|
+
|
|
119
|
+
## Message Cards
|
|
120
|
+
|
|
121
|
+
Message cards sent by the bot should start with a strong heading.
|
|
122
|
+
|
|
123
|
+
Rules:
|
|
124
|
+
|
|
125
|
+
- Start with a bold heading or, for dialogs, a bold question.
|
|
126
|
+
- Setting detail cards may include an emoji in the heading, then a colon and the current value in `<code>`.
|
|
127
|
+
- Explain what the setting does and what the options mean only as much as needed.
|
|
128
|
+
- Keep descriptions short and clear.
|
|
129
|
+
|
|
130
|
+
Examples:
|
|
131
|
+
|
|
132
|
+
```html
|
|
133
|
+
<b>👄 Voice reply mode:</b> <code>mirror</code>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
```html
|
|
137
|
+
<b>Queue</b>
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Confirmation Dialogs
|
|
141
|
+
|
|
142
|
+
Confirmation dialogs protect risky or disruptive actions.
|
|
143
|
+
|
|
144
|
+
Rules:
|
|
145
|
+
|
|
146
|
+
- Body text is one bold text-only question.
|
|
147
|
+
- Do not put emoji in the dialog question.
|
|
148
|
+
- Do not add explanatory body copy unless the risk cannot be understood from the question and action labels.
|
|
149
|
+
- Put emoji on the buttons, not in the question.
|
|
150
|
+
- Preserve dialog-specific button order by intent.
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
|
|
154
|
+
```html
|
|
155
|
+
<b>Compact session?</b>
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Buttons:
|
|
159
|
+
|
|
160
|
+
- `🗜 Yes, compact`
|
|
161
|
+
- `❌ No`
|
|
162
|
+
|
|
163
|
+
## Callback Ownership
|
|
164
|
+
|
|
165
|
+
UI style does not change callback ownership. Callback prefixes remain owned by their feature domain and must be listed in callback namespace documentation when they become public collision risks.
|