@llblab/pi-telegram 0.9.9 → 0.10.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 +19 -2
- package/BACKLOG.md +0 -2
- package/CHANGELOG.md +19 -1
- package/README.md +8 -3
- package/docs/README.md +1 -1
- package/docs/callback-namespaces.md +1 -1
- package/docs/extension-sections.md +269 -164
- 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.
|
|
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
|
|
27
22
|
|
|
28
|
-
## Identity
|
|
23
|
+
## 3. Identity Key
|
|
29
24
|
|
|
30
|
-
Each section has one stable identity key.
|
|
31
|
-
|
|
32
|
-
Use the same identity rules as the Extension Locks Standard:
|
|
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
|
|
37
|
-
|
|
38
|
-
For npm-style package extensions, the canonical value is the `package.json` `name`.
|
|
39
|
-
|
|
40
|
-
Examples:
|
|
28
|
+
2. Directory name when the entrypoint is `index.ts` without `package.json`
|
|
29
|
+
3. File basename for single-file extensions
|
|
41
30
|
|
|
42
|
-
```
|
|
43
|
-
extensions/pi-telegram-
|
|
44
|
-
extensions/pi-telegram-
|
|
45
|
-
extensions/pi-telegram-
|
|
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,285 @@ 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.
|
|
131
139
|
|
|
132
|
-
|
|
140
|
+
**Shutdown:** Call `pi.on("shutdown", () => unregister())` to clean up. The registry is cleared on `pi-telegram` unload.
|
|
133
141
|
|
|
134
|
-
|
|
142
|
+
## 6. Menu Integration
|
|
135
143
|
|
|
136
|
-
|
|
144
|
+
Sections appear in two locations:
|
|
137
145
|
|
|
138
|
-
|
|
146
|
+
### Main menu
|
|
139
147
|
|
|
140
|
-
|
|
141
|
-
- Built-in sections keep priority over external sections
|
|
142
|
-
- Duplicate `id` registration is rejected or replaces only the same live owner through an explicit disposer path
|
|
143
|
-
- Section errors must not break the whole main menu
|
|
144
|
-
- If a section fails to render, `pi-telegram` should show a compact error row or omit the section and record diagnostics
|
|
148
|
+
Section rows are injected **before the ⚙️ Settings row**. Ordered by `order` (lower first), then `id` alphabetically.
|
|
145
149
|
|
|
146
|
-
|
|
150
|
+
```
|
|
151
|
+
🤖 Model: anthropic/claude-sonnet-4-5
|
|
152
|
+
🧠 Thinking: off
|
|
153
|
+
⌛ Queue: 0
|
|
154
|
+
🧪 Demo submenu ← extension section
|
|
155
|
+
⚙️ Settings
|
|
156
|
+
```
|
|
147
157
|
|
|
148
|
-
|
|
158
|
+
Built-in core rows keep priority. Section errors do not break menu rendering — a failed section is omitted with a diagnostic entry.
|
|
149
159
|
|
|
150
|
-
|
|
160
|
+
### Settings submenu
|
|
151
161
|
|
|
152
|
-
|
|
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:
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
getLabel: () => `${flag ? "🟢" : "⚫️"} Demo settings`;
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
⬆️ Main menu
|
|
170
|
+
🟢 Demo settings ← extension settings (dynamic label)
|
|
171
|
+
🟢 Proactive push
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Ordered by `settings.order` (lower first), then `id` alphabetically.
|
|
175
|
+
|
|
176
|
+
## 7. Callback Routing
|
|
177
|
+
|
|
178
|
+
### Token mapping
|
|
179
|
+
|
|
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
|
|
167
202
|
|
|
168
|
-
|
|
203
|
+
- `"handled"`: callback consumed, stop routing, `answerCallbackQuery` already called
|
|
204
|
+
- `"pass"`: section declines; fallback to settings handler (if exists), then to caller
|
|
169
205
|
|
|
170
|
-
|
|
171
|
-
- `"pass"` — section declines this callback, allow fallback routing
|
|
206
|
+
### Fallback chain in `handleCallback`
|
|
172
207
|
|
|
173
|
-
|
|
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:
|
|
219
|
+
|
|
220
|
+
> "This section is no longer available."
|
|
174
221
|
|
|
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
|
|
222
|
+
Section errors are caught and surfaced as popup text. No unhandled exceptions leak to the poller.
|
|
178
223
|
|
|
179
|
-
##
|
|
224
|
+
## 8. Navigation Hierarchy
|
|
225
|
+
|
|
226
|
+
`ctx.edit()` and `ctx.open()` automatically prepend a Back row. The Back target depends on the navigation level:
|
|
227
|
+
|
|
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`
|
|
232
|
+
|
|
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
|
+
```
|
|
180
243
|
|
|
181
|
-
|
|
244
|
+
## 9. Context Ports
|
|
182
245
|
|
|
183
|
-
|
|
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
|
+
## 10. Telegram Bot API Integration
|
|
216
302
|
|
|
217
|
-
`
|
|
303
|
+
### `callback_data` contract
|
|
218
304
|
|
|
219
|
-
|
|
305
|
+
Section callbacks use the `section:` prefix owned by `pi-telegram`:
|
|
220
306
|
|
|
221
307
|
```text
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
lastError
|
|
308
|
+
section:0:open → open section root
|
|
309
|
+
section:0:settings:open → open settings root
|
|
310
|
+
section:0:<action>:<payload> → forwarded to handleCallback
|
|
226
311
|
```
|
|
227
312
|
|
|
228
|
-
|
|
313
|
+
`section:` is listed in `TELEGRAM_OWNED_CALLBACK_PREFIXES` alongside `menu:`, `model:`, `settings:`, `status:`, `tgbtn:`, `thinking:`, `queue:`. Layered extensions must not use this prefix.
|
|
229
314
|
|
|
230
|
-
|
|
315
|
+
### Inline keyboard layout
|
|
231
316
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
317
|
+
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"`.
|
|
318
|
+
|
|
319
|
+
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.
|
|
320
|
+
|
|
321
|
+
### Stale message handling
|
|
322
|
+
|
|
323
|
+
If the interactive message has expired (no stored model menu state), the callback receives:
|
|
324
|
+
|
|
325
|
+
> "Interactive message expired."
|
|
240
326
|
|
|
241
|
-
|
|
327
|
+
This applies to section callbacks as well — the state check runs before dispatch.
|
|
242
328
|
|
|
243
|
-
##
|
|
329
|
+
## 11. Pi Extension API Inspiration
|
|
244
330
|
|
|
245
|
-
|
|
331
|
+
The platform inherits from π's own extension model:
|
|
246
332
|
|
|
247
|
-
|
|
333
|
+
- `export default function(pi)` → `registerTelegramSection(section)`
|
|
334
|
+
- `pi.on("shutdown", ...)` → disposer from `registerTelegramSection`
|
|
335
|
+
- Typed imports → typed import from `@llblab/pi-telegram/lib/extension-sections.ts`
|
|
336
|
+
- `globalThis` registry → `__piTelegramSectionRegistry__` on `globalThis`
|
|
337
|
+
- Identity from `package.json/name` → same identity rules as Locks Standard
|
|
338
|
+
- Narrow typed context ports → `TelegramSectionContext` / `TelegramSectionCallbackContext`
|
|
339
|
+
- Extension does not own transport → `pi-telegram` owns polling, message lifecycle
|
|
248
340
|
|
|
249
|
-
|
|
341
|
+
## 12. Diagnostics
|
|
250
342
|
|
|
251
|
-
|
|
343
|
+
`getTelegramSectionDiagnostics()` returns:
|
|
252
344
|
|
|
253
|
-
|
|
345
|
+
```ts
|
|
346
|
+
interface TelegramSectionDiagnostic {
|
|
347
|
+
id: string;
|
|
348
|
+
token: string;
|
|
349
|
+
label: string;
|
|
350
|
+
status: "active" | "stale" | "error";
|
|
351
|
+
lastError?: string;
|
|
352
|
+
}
|
|
353
|
+
```
|
|
254
354
|
|
|
255
|
-
|
|
355
|
+
Available through `pi-telegram`'s `/telegram-status` or programmatically via `getTelegramSectionDiagnostics()`.
|
|
256
356
|
|
|
257
|
-
|
|
357
|
+
## 13. Purpose and Non-Goals
|
|
258
358
|
|
|
259
|
-
|
|
359
|
+
### Use sections for:
|
|
360
|
+
|
|
361
|
+
- File/project explorers
|
|
362
|
+
- Prompt/session history viewers
|
|
363
|
+
- Tool approval dashboards
|
|
364
|
+
- Runtime status panels
|
|
365
|
+
- Extension settings or diagnostics
|
|
366
|
+
- Human-in-the-loop forms that should not become agent turns
|
|
367
|
+
|
|
368
|
+
### Do not use sections for:
|
|
369
|
+
|
|
370
|
+
- Plain agent prompts (use normal queue)
|
|
371
|
+
- One-shot assistant-authored buttons (use `telegram_button` outbound comments)
|
|
372
|
+
- Command-template pipelines (use inbound/outbound handlers)
|
|
373
|
+
|
|
374
|
+
### Non-goals:
|
|
260
375
|
|
|
261
376
|
- No second Telegram poller
|
|
262
377
|
- No new pi extension loader
|
|
263
378
|
- No generic webview system
|
|
264
379
|
- No default filesystem mutation API
|
|
265
|
-
- No prompt rollback semantics
|
|
266
|
-
- No separate owner field while identity key is sufficient
|
|
267
|
-
|
|
268
|
-
## Evolution path
|
|
269
|
-
|
|
270
|
-
0.10.0 minimum:
|
|
380
|
+
- No prompt rollback semantics
|
|
381
|
+
- No separate `owner` field while identity key is sufficient
|
|
271
382
|
|
|
272
|
-
|
|
273
|
-
- Main menu integration
|
|
274
|
-
- Section callback routing
|
|
275
|
-
- Narrow runtime ports
|
|
276
|
-
- Diagnostics for registered sections
|
|
277
|
-
- Documentation for extension authors
|
|
383
|
+
## 14. Relationship to Other Standards
|
|
278
384
|
|
|
279
|
-
|
|
385
|
+
- [Callback Namespaces](./callback-namespaces.md): defines `section:` as pi-telegram-owned prefix. Sections use namespaced callbacks but authors never hand-roll them
|
|
386
|
+
- [External Handlers](./external-handlers.md): raw update interception for direct Telegram update access. Sections are the structured UI layer above
|
|
387
|
+
- [Extension Locks](../docs/locks.md) (external): same identity key rules (`package.json/name` → canonical id)
|
|
388
|
+
- [Command Templates](./command-templates.md): sections do not execute command templates by default. UI registration + callback routing, not shell execution
|
|
280
389
|
|
|
281
|
-
|
|
282
|
-
@llblab/pi-telegram-explorer
|
|
283
|
-
```
|
|
390
|
+
## 15. Demo Extension
|
|
284
391
|
|
|
285
|
-
|
|
392
|
+
`@llblab/pi-telegram-extension-demo` (`extensions/pi-telegram-extension-demo/`) is the reference implementation:
|
|
286
393
|
|
|
287
|
-
-
|
|
288
|
-
-
|
|
289
|
-
-
|
|
290
|
-
- Browse recent prompt/session snapshots read-only
|
|
291
|
-
- Enqueue a prompt derived from a selected item
|
|
394
|
+
- Main menu: `🧪 Demo submenu` — enqueue prompt, answer callback, show info, interactive counter
|
|
395
|
+
- Settings: `🧪 Demo settings` — ON/OFF toggle with dynamic `getLabel()` status indicator, enqueue from settings
|
|
396
|
+
- Navigation: full Back/Main menu hierarchy across all three levels
|
|
292
397
|
|
|
293
|
-
|
|
398
|
+
Use it as a template for new section-based extensions.
|
package/index.ts
CHANGED
|
@@ -8,6 +8,11 @@ import * as Api from "./lib/api.ts";
|
|
|
8
8
|
import * as CommandTemplates from "./lib/command-templates.ts";
|
|
9
9
|
import * as Commands from "./lib/commands.ts";
|
|
10
10
|
import * as Config from "./lib/config.ts";
|
|
11
|
+
import {
|
|
12
|
+
createTelegramExtensionSectionRegistry,
|
|
13
|
+
setGlobalTelegramSectionRegistry,
|
|
14
|
+
type TelegramSectionRegistry,
|
|
15
|
+
} from "./lib/extension-sections.ts";
|
|
11
16
|
import { createTelegramExternalHandleUpdate } from "./lib/external-handlers.ts";
|
|
12
17
|
import * as InboundHandlers from "./lib/inbound-handlers.ts";
|
|
13
18
|
import * as Keyboard from "./lib/keyboard.ts";
|
|
@@ -69,6 +74,9 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
69
74
|
Model.ScopedTelegramModel<ActivePiModel>
|
|
70
75
|
>();
|
|
71
76
|
const modelMenuRuntime = Menu.createTelegramModelMenuRuntime<ActivePiModel>();
|
|
77
|
+
const sectionRegistry: TelegramSectionRegistry =
|
|
78
|
+
createTelegramExtensionSectionRegistry();
|
|
79
|
+
setGlobalTelegramSectionRegistry(sectionRegistry);
|
|
72
80
|
const runtimeEvents = Status.createTelegramRuntimeEventRecorder({
|
|
73
81
|
getBotToken: configStore.getBotToken,
|
|
74
82
|
});
|
|
@@ -277,6 +285,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
277
285
|
sendTextReply,
|
|
278
286
|
editInteractiveMessage,
|
|
279
287
|
sendInteractiveMessage,
|
|
288
|
+
sectionRegistry,
|
|
280
289
|
});
|
|
281
290
|
|
|
282
291
|
// --- Queue Menu ---
|
|
@@ -298,16 +307,19 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
298
307
|
updateStatusMessage: menuActions.updateStatusMessage,
|
|
299
308
|
updateStatus,
|
|
300
309
|
});
|
|
301
|
-
const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
310
|
+
const settingsMenuRuntime = MenuSettings.createTelegramSettingsMenuRuntime(
|
|
311
|
+
{
|
|
312
|
+
getModelMenuState: getQueueMenuState,
|
|
313
|
+
getStoredModelMenuState: modelMenuRuntime.getState,
|
|
314
|
+
storeModelMenuState: modelMenuRuntime.storeState,
|
|
315
|
+
editInteractiveMessage,
|
|
316
|
+
sendInteractiveMessage,
|
|
317
|
+
answerCallbackQuery,
|
|
318
|
+
isProactivePushEnabled,
|
|
319
|
+
setProactivePushEnabled,
|
|
320
|
+
},
|
|
321
|
+
sectionRegistry,
|
|
322
|
+
);
|
|
311
323
|
|
|
312
324
|
// --- Polling ---
|
|
313
325
|
|
|
@@ -334,11 +346,14 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
334
346
|
queueMenuCallbackHandler: queueMenuRuntime.handleCallbackQuery,
|
|
335
347
|
openSettingsMenu: settingsMenuRuntime.openSettingsMenu,
|
|
336
348
|
settingsMenuCallbackHandler: settingsMenuRuntime.handleCallbackQuery,
|
|
349
|
+
sectionRegistry,
|
|
337
350
|
buttonActionStore,
|
|
338
351
|
inboundHandlerRuntime,
|
|
339
352
|
updateStatus,
|
|
340
353
|
dispatchNextQueuedTelegramTurn,
|
|
341
354
|
answerCallbackQuery,
|
|
355
|
+
editInteractiveMessage,
|
|
356
|
+
sendInteractiveMessage,
|
|
342
357
|
answerGuestQuery,
|
|
343
358
|
sendTextReply,
|
|
344
359
|
setMyCommands,
|
|
@@ -439,8 +454,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
439
454
|
startPolling: lockedPollingRuntime.start,
|
|
440
455
|
stopPolling: lockedPollingRuntime.stop,
|
|
441
456
|
updateStatus,
|
|
442
|
-
isProactivePushEnabled,
|
|
443
|
-
setProactivePushEnabled,
|
|
444
457
|
});
|
|
445
458
|
|
|
446
459
|
// --- Lifecycle Hooks ---
|