@llblab/pi-telegram 0.9.9 → 0.10.1
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 +19 -2
- package/BACKLOG.md +0 -2
- package/CHANGELOG.md +24 -1
- package/README.md +8 -3
- package/docs/README.md +1 -1
- package/docs/callback-namespaces.md +1 -1
- package/docs/extension-sections.md +304 -163
- package/index.ts +25 -12
- package/lib/commands.ts +0 -36
- package/lib/extension-sections.ts +627 -0
- package/lib/menu-model.ts +1 -1
- package/lib/menu-settings.ts +68 -33
- package/lib/menu-status.ts +18 -0
- package/lib/menu.ts +130 -1
- package/lib/replies.ts +1 -2
- package/lib/routing.ts +63 -16
- package/package.json +2 -2
|
@@ -1,83 +1,84 @@
|
|
|
1
|
-
# Telegram Extension Sections Standard
|
|
1
|
+
# Telegram Extension Sections Standard
|
|
2
2
|
|
|
3
|
-
**Status:**
|
|
3
|
+
**Status:** Implemented in 0.10.0. Stable public API.
|
|
4
4
|
|
|
5
5
|
**Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
## 1. Philosophy
|
|
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
|
-
|
|
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 poller, no new loader — just one `registerTelegramSection()` call.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## 2. Contract Layers
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
The standard operates across three integration surfaces:
|
|
18
18
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
- Runtime status panels
|
|
23
|
-
- Extension settings or diagnostics
|
|
24
|
-
- Human-in-the-loop forms that should not become agent turns
|
|
25
|
-
|
|
26
|
-
Do not use sections for plain agent prompts, one-shot buttons authored by the assistant, or command-template pipelines. Those stay in the normal queue, outbound action comments, inbound/outbound handlers, or command-template domains.
|
|
27
|
-
|
|
28
|
-
## Identity key
|
|
19
|
+
- **Extension API**: registration shape, context ports, `callbackData()`, `getLabel()`, navigation, disposer
|
|
20
|
+
- **Telegram Bot API**: 64-byte limit → token mapping, inline keyboard, `menu:back`/`settings:list` routing, stale-token answers
|
|
21
|
+
- **Pi Extension API**: typed import + `globalThis`, `pi.on("shutdown")` cleanup, load-order, identity
|
|
29
22
|
|
|
30
|
-
|
|
23
|
+
## 3. Identity Key
|
|
31
24
|
|
|
32
|
-
Use the same
|
|
25
|
+
Each section has one stable identity key. Use the same rules as the Extension Locks Standard:
|
|
33
26
|
|
|
34
27
|
1. `package.json/name` for npm-style pi packages
|
|
35
|
-
2. Directory name when the
|
|
36
|
-
3. File basename
|
|
28
|
+
2. Directory name when the entrypoint is `index.ts` without `package.json`
|
|
29
|
+
3. File basename for single-file extensions
|
|
37
30
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
```text
|
|
43
|
-
extensions/pi-telegram-explorer/package.json name=@llblab/pi-telegram-explorer -> @llblab/pi-telegram-explorer
|
|
44
|
-
extensions/pi-telegram-explorer/index.ts without package.json -> pi-telegram-explorer
|
|
45
|
-
extensions/pi-telegram-explorer.ts -> pi-telegram-explorer
|
|
31
|
+
```
|
|
32
|
+
extensions/pi-telegram-extension-demo/package.json name=@llblab/pi-telegram-extension-demo → @llblab/pi-telegram-extension-demo
|
|
33
|
+
extensions/pi-telegram-extension-demo/index.ts without package.json → pi-telegram-extension-demo
|
|
34
|
+
extensions/pi-telegram-extension-demo.ts → pi-telegram-extension-demo
|
|
46
35
|
```
|
|
47
36
|
|
|
48
|
-
The
|
|
49
|
-
|
|
50
|
-
The identity key is used for:
|
|
51
|
-
|
|
52
|
-
- Registry ownership
|
|
53
|
-
- Conflict detection
|
|
54
|
-
- Diagnostics
|
|
55
|
-
- Cleanup
|
|
56
|
-
- Callback routing lookup
|
|
57
|
-
- Future capability policy
|
|
58
|
-
|
|
59
|
-
## Registration shape
|
|
37
|
+
The `id` is the owner identity. No separate `owner` field. Used for registry ownership, conflict detection, diagnostics, cleanup, and callback routing lookup.
|
|
60
38
|
|
|
61
|
-
|
|
39
|
+
## 4. Registration Shape
|
|
62
40
|
|
|
63
41
|
```ts
|
|
64
|
-
registerTelegramSection
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
42
|
+
import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
|
|
43
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
44
|
+
|
|
45
|
+
export default function (pi: ExtensionAPI) {
|
|
46
|
+
const unregister = registerTelegramSection({
|
|
47
|
+
id: "@llblab/pi-telegram-extension-demo",
|
|
48
|
+
label: "🧪 Demo submenu",
|
|
49
|
+
order: 10,
|
|
50
|
+
render: async (ctx) => ({
|
|
51
|
+
text: "<b>Demo</b>",
|
|
70
52
|
parseMode: "html",
|
|
71
|
-
replyMarkup
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
53
|
+
replyMarkup: {
|
|
54
|
+
inline_keyboard: [
|
|
55
|
+
[{ text: "Click me", callback_data: ctx.callbackData("act", "x") }],
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
handleCallback: async (ctx) => {
|
|
60
|
+
if (ctx.action === "act") {
|
|
61
|
+
await ctx.answerCallback(`payload: ${ctx.payload}`);
|
|
62
|
+
return "handled";
|
|
63
|
+
}
|
|
64
|
+
return "pass";
|
|
65
|
+
},
|
|
66
|
+
settings: {
|
|
67
|
+
label: "🧪 Demo settings",
|
|
68
|
+
order: 0,
|
|
69
|
+
getLabel: () => `${flag ? "🟢" : "⚫️"} Demo settings`,
|
|
70
|
+
open: async (ctx) => ({ text: "<b>Settings</b>", parseMode: "html" }),
|
|
71
|
+
handleCallback: async (ctx) => {
|
|
72
|
+
flag = ctx.payload === "on";
|
|
73
|
+
return "handled";
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
pi.on("shutdown", () => unregister());
|
|
78
|
+
}
|
|
78
79
|
```
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
### Full TypeScript shape
|
|
81
82
|
|
|
82
83
|
```ts
|
|
83
84
|
type TelegramSectionId = string;
|
|
@@ -88,11 +89,22 @@ interface TelegramSectionRegistration {
|
|
|
88
89
|
label: string;
|
|
89
90
|
order?: number;
|
|
90
91
|
render: (
|
|
91
|
-
ctx:
|
|
92
|
+
ctx: TelegramSectionContext,
|
|
92
93
|
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
93
94
|
handleCallback?: (
|
|
94
95
|
ctx: TelegramSectionCallbackContext,
|
|
95
96
|
) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
|
|
97
|
+
settings?: {
|
|
98
|
+
label: string;
|
|
99
|
+
order?: number;
|
|
100
|
+
getLabel?: () => string;
|
|
101
|
+
open: (
|
|
102
|
+
ctx: TelegramSectionContext,
|
|
103
|
+
) => TelegramSectionView | Promise<TelegramSectionView>;
|
|
104
|
+
handleCallback?: (
|
|
105
|
+
ctx: TelegramSectionCallbackContext,
|
|
106
|
+
) => TelegramSectionCallbackResult | Promise<TelegramSectionCallbackResult>;
|
|
107
|
+
};
|
|
96
108
|
}
|
|
97
109
|
|
|
98
110
|
interface TelegramSectionView {
|
|
@@ -102,192 +114,321 @@ interface TelegramSectionView {
|
|
|
102
114
|
}
|
|
103
115
|
```
|
|
104
116
|
|
|
105
|
-
Registration returns a disposer
|
|
117
|
+
### Registration returns a disposer
|
|
106
118
|
|
|
107
119
|
```ts
|
|
108
120
|
const unregister = registerTelegramSection(section);
|
|
109
|
-
unregister();
|
|
121
|
+
unregister(); // removes from main menu, settings, and callback routing
|
|
110
122
|
```
|
|
111
123
|
|
|
112
|
-
## Loading
|
|
124
|
+
## 5. Loading Model
|
|
113
125
|
|
|
114
|
-
|
|
126
|
+
Two paths, same registry:
|
|
115
127
|
|
|
116
|
-
|
|
117
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
118
|
-
import { registerTelegramSection } from "@llblab/pi-telegram/lib/extension-sections.ts";
|
|
128
|
+
**Typed import (preferred):** Extension imports `registerTelegramSection` from `@llblab/pi-telegram/lib/extension-sections.ts`. The function reads from a `globalThis` registry set by `pi-telegram` at startup.
|
|
119
129
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
render: async (ctx) => ctx.html("<b>Explorer</b>"),
|
|
125
|
-
});
|
|
126
|
-
pi.on("shutdown", () => unregister());
|
|
127
|
-
}
|
|
130
|
+
**Relative import (local):** When the extension cannot resolve `@llblab/pi-telegram` as an npm package, use a relative path:
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
import { registerTelegramSection } from "../pi-telegram/lib/extension-sections.ts";
|
|
128
134
|
```
|
|
129
135
|
|
|
130
|
-
`pi-telegram`
|
|
136
|
+
**GlobalThis bridge (zero-coupling):** `pi-telegram` exposes `__piTelegramSectionRegistry__` on `globalThis`. The typed import is a thin wrapper. Extensions never touch the raw registry.
|
|
137
|
+
|
|
138
|
+
**Load order:** `pi-telegram` must load first (sets the global registry). Demo/consumer extensions load second (call `registerTelegramSection`). Pi's normal extension loader guarantees this when `pi-telegram` is listed first.
|
|
139
|
+
|
|
140
|
+
**Shutdown:** Call `pi.on("shutdown", () => unregister())` to clean up. The registry is cleared on `pi-telegram` unload.
|
|
141
|
+
|
|
142
|
+
## 6. Menu Integration
|
|
143
|
+
|
|
144
|
+
Sections appear in two locations:
|
|
145
|
+
|
|
146
|
+
### Main menu
|
|
147
|
+
|
|
148
|
+
Section rows are injected **before the ⚙️ Settings row**. Ordered by `order` (lower first), then `id` alphabetically.
|
|
131
149
|
|
|
132
|
-
|
|
150
|
+
```
|
|
151
|
+
🤖 Model: anthropic/claude-sonnet-4-5
|
|
152
|
+
🧠 Thinking: off
|
|
153
|
+
⌛ Queue: 0
|
|
154
|
+
🧪 Demo submenu ← extension section
|
|
155
|
+
⚙️ Settings
|
|
156
|
+
```
|
|
133
157
|
|
|
134
|
-
|
|
158
|
+
Built-in core rows keep priority. Section errors do not break menu rendering — a failed section is omitted with a diagnostic entry.
|
|
135
159
|
|
|
136
|
-
|
|
160
|
+
### Settings submenu
|
|
137
161
|
|
|
138
|
-
|
|
162
|
+
Extensions with a `settings` block inject rows **before built-in Proactive push**. The `getLabel()` function (if present) is called on every render to produce a dynamic label — use it for status indicators:
|
|
139
163
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
164
|
+
```ts
|
|
165
|
+
getLabel: () => `${flag ? "🟢" : "⚫️"} Demo settings`;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
⬆️ Main menu
|
|
170
|
+
🟢 Demo settings ← extension settings (dynamic label)
|
|
171
|
+
🟢 Proactive push
|
|
172
|
+
```
|
|
145
173
|
|
|
146
|
-
|
|
174
|
+
Ordered by `settings.order` (lower first), then `id` alphabetically.
|
|
147
175
|
|
|
148
|
-
|
|
176
|
+
## 7. Callback Routing
|
|
149
177
|
|
|
150
|
-
|
|
178
|
+
### Token mapping
|
|
151
179
|
|
|
152
|
-
|
|
180
|
+
Telegram limits `callback_data` to 64 bytes. Full npm names like `@llblab/pi-telegram-explorer` often exceed this. `pi-telegram` maps each registered section to a compact numeric token:
|
|
153
181
|
|
|
154
182
|
```text
|
|
155
183
|
section:<token>:<action>:<payload>
|
|
156
184
|
```
|
|
157
185
|
|
|
158
|
-
|
|
186
|
+
Example: `section:0:counter:5`
|
|
159
187
|
|
|
160
|
-
|
|
188
|
+
The token is an implementation detail. Section authors **never** write `section:` strings manually. Use `ctx.callbackData(action, payload?)` which fills in the correct token.
|
|
189
|
+
|
|
190
|
+
### Routing order
|
|
161
191
|
|
|
162
192
|
1. Telegram update arrives through the single `pi-telegram` poller
|
|
163
|
-
2.
|
|
164
|
-
3.
|
|
165
|
-
4.
|
|
166
|
-
5.
|
|
193
|
+
2. External handlers observe/consume (raw update interception)
|
|
194
|
+
3. Button action store (`tgbtn:*`)
|
|
195
|
+
4. Queue menu callbacks (`queue:*`)
|
|
196
|
+
5. Settings menu callbacks (`settings:*`)
|
|
197
|
+
6. Built-in menu callbacks (`menu:*`, `model:*`, `thinking:*`, `status:*`)
|
|
198
|
+
7. Section callbacks (`section:*`) — dispatched before step 6's full handler
|
|
199
|
+
8. Unknown callbacks fall back to `[callback]` prompt text
|
|
200
|
+
|
|
201
|
+
### Handler return values
|
|
202
|
+
|
|
203
|
+
- `"handled"`: callback consumed, stop routing, `answerCallbackQuery` already called
|
|
204
|
+
- `"pass"`: section declines; fallback to settings handler (if exists), then to caller
|
|
205
|
+
|
|
206
|
+
### Fallback chain in `handleCallback`
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
section.handleCallback(ctx) → "handled" | "pass"
|
|
210
|
+
└─ if "pass" and settings.handleCallback exists →
|
|
211
|
+
settings.handleCallback(newCtx with backCallback="settings:list")
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The fallback creates a **new context** with the correct `backCallback` for the navigation level.
|
|
215
|
+
|
|
216
|
+
### Stale tokens
|
|
217
|
+
|
|
218
|
+
If a section is unregistered or a token is unknown, the callback is answered with a short Telegram native popup:
|
|
167
219
|
|
|
168
|
-
|
|
220
|
+
> "This section is no longer available."
|
|
169
221
|
|
|
170
|
-
|
|
171
|
-
- `"pass"` — section declines this callback, allow fallback routing
|
|
222
|
+
Section errors are caught and surfaced as popup text. No unhandled exceptions leak to the poller.
|
|
172
223
|
|
|
173
|
-
|
|
224
|
+
## 8. Navigation Hierarchy
|
|
174
225
|
|
|
175
|
-
|
|
176
|
-
- Missing target state should re-render the section root when possible
|
|
177
|
-
- Section errors should be caught, surfaced as a short callback answer, and recorded in diagnostics
|
|
226
|
+
`ctx.edit()` and `ctx.open()` automatically prepend a Back row. The Back target depends on the navigation level:
|
|
178
227
|
|
|
179
|
-
|
|
228
|
+
- Section root (from main menu): `⬆️ Main menu` → `menu:back`
|
|
229
|
+
- Section sub-view (`ctx.edit()` in handler): `⬆️ Back` → `section:<token>:open`
|
|
230
|
+
- Settings root (from Settings list): `⬆️ Back` → `settings:list`
|
|
231
|
+
- Settings sub-view (`ctx.edit()` in settings handler): `⬆️ Back` → `settings:list`
|
|
180
232
|
|
|
181
|
-
|
|
233
|
+
Section authors do not need to manage the Back button — it is added automatically and deduplicated when already present.
|
|
234
|
+
|
|
235
|
+
```
|
|
236
|
+
Main menu
|
|
237
|
+
├─ 🧪 Demo submenu ──── [⬆️ Main menu]
|
|
238
|
+
│ └─ Counter ─────── [⬆️ Back → Demo submenu]
|
|
239
|
+
└─ ⚙️ Settings ──────── [⬆️ Main menu]
|
|
240
|
+
└─ Demo settings ─ [⬆️ Back → Settings list]
|
|
241
|
+
└─ toggle ─── [⬆️ Back → Settings list] (via ctx.edit)
|
|
242
|
+
```
|
|
182
243
|
|
|
183
|
-
|
|
244
|
+
## 9. Context Ports
|
|
245
|
+
|
|
246
|
+
### `TelegramSectionContext` — for `render()` and `settings.open()`
|
|
184
247
|
|
|
185
248
|
```ts
|
|
186
249
|
interface TelegramSectionContext {
|
|
187
250
|
sectionId: string;
|
|
188
251
|
chatId: number;
|
|
189
252
|
messageId?: number;
|
|
253
|
+
/** Answer the callback query with an optional popup text */
|
|
254
|
+
answerCallback(text?: string): Promise<void>;
|
|
255
|
+
/** Edit the current message (auto-prepends Back row) */
|
|
256
|
+
edit(view: TelegramSectionView): Promise<void>;
|
|
257
|
+
/** Send a new message (auto-prepends Back row) */
|
|
258
|
+
open(view: TelegramSectionView): Promise<void>;
|
|
259
|
+
/** Enqueue a plain-text prompt turn */
|
|
260
|
+
enqueuePrompt(prompt: string): Promise<void>;
|
|
261
|
+
/** Build a section-namespaced callback_data string */
|
|
262
|
+
callbackData(action: string, payload?: string): string;
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### `TelegramSectionCallbackContext` — for `handleCallback()`
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
interface TelegramSectionCallbackContext {
|
|
270
|
+
sectionId: string;
|
|
271
|
+
chatId: number;
|
|
272
|
+
messageId?: number;
|
|
273
|
+
/** The action segment from callback_data */
|
|
274
|
+
action: string;
|
|
275
|
+
/** The payload segment from callback_data */
|
|
276
|
+
payload: string;
|
|
190
277
|
answerCallback(text?: string): Promise<void>;
|
|
191
278
|
edit(view: TelegramSectionView): Promise<void>;
|
|
192
279
|
open(view: TelegramSectionView): Promise<void>;
|
|
193
280
|
enqueuePrompt(prompt: string): Promise<void>;
|
|
194
|
-
|
|
195
|
-
getSessionSnapshot?(): TelegramSessionSnapshot;
|
|
281
|
+
callbackData(action: string, payload?: string): string;
|
|
196
282
|
}
|
|
197
283
|
```
|
|
198
284
|
|
|
199
|
-
|
|
285
|
+
### `enqueuePrompt` semantics
|
|
200
286
|
|
|
201
|
-
|
|
287
|
+
Queues a `[telegram] <prompt>` turn in the default lane with the paired user's `chatId`. Uses `queueMutationRuntime.append()` and triggers `dispatchNextQueuedTelegramTurn()`. The prompt arrives as a normal Telegram-owned turn — the agent sees it as if the user typed it.
|
|
202
288
|
|
|
203
|
-
|
|
289
|
+
### Capability scope
|
|
204
290
|
|
|
205
|
-
|
|
291
|
+
Context ports are intentionally narrow. Sections **cannot**:
|
|
206
292
|
|
|
207
|
-
-
|
|
208
|
-
-
|
|
209
|
-
-
|
|
210
|
-
-
|
|
211
|
-
-
|
|
293
|
+
- Read/write filesystem
|
|
294
|
+
- Access raw process or bot clients
|
|
295
|
+
- Start a second poller
|
|
296
|
+
- Mutate session state
|
|
297
|
+
- Send arbitrary Telegram API calls
|
|
212
298
|
|
|
213
|
-
|
|
299
|
+
Add capability-specific ports only when the first real extension proves the need.
|
|
214
300
|
|
|
215
|
-
|
|
301
|
+
### Interactive messages in chat (`ctx.open`)
|
|
216
302
|
|
|
217
|
-
`
|
|
303
|
+
`ctx.open()` sends a new message directly into the Telegram chat — outside the menu hierarchy. No Back row is prepended. Use it for extension-driven interactions that live in the conversation:
|
|
218
304
|
|
|
219
|
-
|
|
305
|
+
- Confirmation dialogs ("Delete file.txt?")
|
|
306
|
+
- Approve/deny gates ("Allow tool execution?")
|
|
307
|
+
- Multi-step forms that should not be menu-bound
|
|
308
|
+
- Status reports with action buttons
|
|
220
309
|
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
310
|
+
```ts
|
|
311
|
+
handleCallback: async (ctx) => {
|
|
312
|
+
if (ctx.action === "delete-file") {
|
|
313
|
+
await ctx.open({
|
|
314
|
+
text: `<b>Delete ${ctx.payload}?</b>\n\nThis cannot be undone.`,
|
|
315
|
+
parseMode: "html",
|
|
316
|
+
replyMarkup: {
|
|
317
|
+
inline_keyboard: [[
|
|
318
|
+
{ text: "✅ Yes, delete",
|
|
319
|
+
callback_data: ctx.callbackData("confirm-delete", ctx.payload) },
|
|
320
|
+
{ text: "❌ Cancel",
|
|
321
|
+
callback_data: ctx.callbackData("cancel") },
|
|
322
|
+
]],
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
return "handled";
|
|
326
|
+
}
|
|
327
|
+
if (ctx.action === "confirm-delete") {
|
|
328
|
+
// actual deletion logic
|
|
329
|
+
await ctx.answerCallback(`Deleted: ${ctx.payload}`);
|
|
330
|
+
return "handled";
|
|
331
|
+
}
|
|
332
|
+
}
|
|
226
333
|
```
|
|
227
334
|
|
|
228
|
-
|
|
335
|
+
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.
|
|
229
336
|
|
|
230
|
-
|
|
337
|
+
## 10. Telegram Bot API Integration
|
|
338
|
+
|
|
339
|
+
### `callback_data` contract
|
|
340
|
+
|
|
341
|
+
Section callbacks use the `section:` prefix owned by `pi-telegram`:
|
|
231
342
|
|
|
232
343
|
```text
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
callbackCount
|
|
237
|
-
errorCount
|
|
238
|
-
capabilities
|
|
344
|
+
section:0:open → open section root
|
|
345
|
+
section:0:settings:open → open settings root
|
|
346
|
+
section:0:<action>:<payload> → forwarded to handleCallback
|
|
239
347
|
```
|
|
240
348
|
|
|
241
|
-
|
|
349
|
+
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
350
|
+
|
|
351
|
+
### Inline keyboard layout
|
|
352
|
+
|
|
353
|
+
Section views return `TelegramSectionView.replyMarkup` — a standard `TelegramInlineKeyboardMarkup`. The bridge prepends the Back row and sends the result through `editMessageText` / `sendMessage` with `parse_mode: "HTML"`.
|
|
354
|
+
|
|
355
|
+
Button labels are not truncated by the bridge. Section authors should keep labels compact for mobile Telegram (under ~30 display-width cells). Use `truncateTelegramButtonLabel` for long dynamic text.
|
|
356
|
+
|
|
357
|
+
### Stale message handling
|
|
358
|
+
|
|
359
|
+
If the interactive message has expired (no stored model menu state), the callback receives:
|
|
360
|
+
|
|
361
|
+
> "Interactive message expired."
|
|
362
|
+
|
|
363
|
+
This applies to section callbacks as well — the state check runs before dispatch.
|
|
364
|
+
|
|
365
|
+
## 11. Pi Extension API Inspiration
|
|
366
|
+
|
|
367
|
+
The platform inherits from π's own extension model:
|
|
368
|
+
|
|
369
|
+
- `export default function(pi)` → `registerTelegramSection(section)`
|
|
370
|
+
- `pi.on("shutdown", ...)` → disposer from `registerTelegramSection`
|
|
371
|
+
- Typed imports → typed import from `@llblab/pi-telegram/lib/extension-sections.ts`
|
|
372
|
+
- `globalThis` registry → `__piTelegramSectionRegistry__` on `globalThis`
|
|
373
|
+
- Identity from `package.json/name` → same identity rules as Locks Standard
|
|
374
|
+
- Narrow typed context ports → `TelegramSectionContext` / `TelegramSectionCallbackContext`
|
|
375
|
+
- Extension does not own transport → `pi-telegram` owns polling, message lifecycle
|
|
376
|
+
|
|
377
|
+
## 12. Diagnostics
|
|
242
378
|
|
|
243
|
-
|
|
379
|
+
`getTelegramSectionDiagnostics()` returns:
|
|
244
380
|
|
|
245
|
-
|
|
381
|
+
```ts
|
|
382
|
+
interface TelegramSectionDiagnostic {
|
|
383
|
+
id: string;
|
|
384
|
+
token: string;
|
|
385
|
+
label: string;
|
|
386
|
+
status: "active" | "stale" | "error";
|
|
387
|
+
lastError?: string;
|
|
388
|
+
}
|
|
389
|
+
```
|
|
246
390
|
|
|
247
|
-
|
|
391
|
+
Available through `pi-telegram`'s `/telegram-status` or programmatically via `getTelegramSectionDiagnostics()`.
|
|
248
392
|
|
|
249
|
-
|
|
393
|
+
## 13. Purpose and Non-Goals
|
|
250
394
|
|
|
251
|
-
Use
|
|
395
|
+
### Use sections for:
|
|
252
396
|
|
|
253
|
-
|
|
397
|
+
- File/project explorers
|
|
398
|
+
- Prompt/session history viewers
|
|
399
|
+
- Tool approval dashboards
|
|
400
|
+
- Runtime status panels
|
|
401
|
+
- Extension settings or diagnostics
|
|
402
|
+
- Human-in-the-loop forms that should not become agent turns
|
|
254
403
|
|
|
255
|
-
|
|
404
|
+
### Do not use sections for:
|
|
256
405
|
|
|
257
|
-
|
|
406
|
+
- Plain agent prompts (use normal queue)
|
|
407
|
+
- One-shot assistant-authored buttons (use `telegram_button` outbound comments)
|
|
408
|
+
- Command-template pipelines (use inbound/outbound handlers)
|
|
258
409
|
|
|
259
|
-
|
|
410
|
+
### Non-goals:
|
|
260
411
|
|
|
261
412
|
- No second Telegram poller
|
|
262
413
|
- No new pi extension loader
|
|
263
414
|
- No generic webview system
|
|
264
415
|
- No default filesystem mutation API
|
|
265
|
-
- No prompt rollback semantics
|
|
266
|
-
- No separate owner field while identity key is sufficient
|
|
267
|
-
|
|
268
|
-
## Evolution path
|
|
416
|
+
- No prompt rollback semantics
|
|
417
|
+
- No separate `owner` field while identity key is sufficient
|
|
269
418
|
|
|
270
|
-
|
|
419
|
+
## 14. Relationship to Other Standards
|
|
271
420
|
|
|
272
|
-
-
|
|
273
|
-
-
|
|
274
|
-
-
|
|
275
|
-
-
|
|
276
|
-
- Diagnostics for registered sections
|
|
277
|
-
- Documentation for extension authors
|
|
421
|
+
- [Callback Namespaces](./callback-namespaces.md): defines `section:` as pi-telegram-owned prefix. Sections use namespaced callbacks but authors never hand-roll them
|
|
422
|
+
- [External Handlers](./external-handlers.md): raw update interception for direct Telegram update access. Sections are the structured UI layer above
|
|
423
|
+
- [Extension Locks](../docs/locks.md) (external): same identity key rules (`package.json/name` → canonical id)
|
|
424
|
+
- [Command Templates](./command-templates.md): sections do not execute command templates by default. UI registration + callback routing, not shell execution
|
|
278
425
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
```text
|
|
282
|
-
@llblab/pi-telegram-explorer
|
|
283
|
-
```
|
|
426
|
+
## 15. Demo Extension
|
|
284
427
|
|
|
285
|
-
|
|
428
|
+
`@llblab/pi-telegram-extension-demo` (`extensions/pi-telegram-extension-demo/`) is the reference implementation:
|
|
286
429
|
|
|
287
|
-
-
|
|
288
|
-
-
|
|
289
|
-
-
|
|
290
|
-
- Browse recent prompt/session snapshots read-only
|
|
291
|
-
- Enqueue a prompt derived from a selected item
|
|
430
|
+
- Main menu: `🧪 Demo submenu` — enqueue prompt, answer callback, show info, interactive counter
|
|
431
|
+
- Settings: `🧪 Demo settings` — ON/OFF toggle with dynamic `getLabel()` status indicator, enqueue from settings
|
|
432
|
+
- Navigation: full Back/Main menu hierarchy across all three levels
|
|
292
433
|
|
|
293
|
-
|
|
434
|
+
Use it as a template for new section-based extensions.
|