@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.
@@ -1,83 +1,84 @@
1
- # Telegram Extension Sections Standard Draft
1
+ # Telegram Extension Sections Standard
2
2
 
3
- **Status:** Draft. This document is a design note for the upcoming Extension Sections platform and is not an implemented or stable public API yet. Treat all names, shapes, and examples as provisional until the implementation lands.
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
- Telegram Extension Sections are a proposed registration contract that lets ordinary pi extensions add structured UI sections to the `pi-telegram` inline application menu.
9
+ ## 1. Philosophy
10
10
 
11
- The guiding philosophy is pi-native extensibility: `pi-telegram` should inherit π's own model of small composable extensions. The bridge should act as a shared Telegram shell for loaded π extensions, not as a closed one-off bot or a place where every feature must fork Telegram polling and transport.
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
- They are not a new extension loader. pi still loads extensions through its normal TypeScript/package system. A loaded extension registers a Telegram section with `pi-telegram`; `pi-telegram` owns bot polling, menu rendering, callback routing, Telegram authorization, and message lifecycle.
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
- ## Purpose
15
+ ## 2. Contract Layers
16
16
 
17
- Use sections when an extension needs a Telegram-native UI surface inside the existing bot shell:
17
+ The standard operates across three integration surfaces:
18
18
 
19
- - File or project explorers
20
- - Prompt/session history viewers
21
- - Tool approval dashboards
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 key
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 extension entrypoint is `index.ts` but there is no package name
36
- 3. File basename when the extension is a single file
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
- ```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 section `id` is also the owner identity. Do not add a separate `owner` field unless a later concrete need appears.
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
- Minimum shape:
39
+ ## 4. Registration Shape
62
40
 
63
41
  ```ts
64
- registerTelegramSection({
65
- id: "@llblab/pi-telegram-explorer",
66
- label: "🗂 Explorer",
67
- render(ctx) {
68
- return {
69
- text: "<b>Explorer</b>",
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
- handleCallback(ctx) {
75
- return "handled";
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
- Recommended TypeScript shape:
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: TelegramSectionRenderContext,
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 model
124
+ ## 5. Loading Model
113
125
 
114
- Sections are registered by normal pi extensions:
126
+ Two paths, same registry:
115
127
 
116
- ```ts
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
- export default function (pi: ExtensionAPI) {
121
- const unregister = registerTelegramSection({
122
- id: "@llblab/pi-telegram-explorer",
123
- label: "🗂 Explorer",
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` may expose a typed import and a zero-coupling `globalThis` registry. The typed import is the preferred authoring path. The global registry exists only to tolerate load order and package coupling constraints.
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
- ## Menu integration
140
+ **Shutdown:** Call `pi.on("shutdown", () => unregister())` to clean up. The registry is cleared on `pi-telegram` unload.
133
141
 
134
- `pi-telegram` owns the main Telegram application menu.
142
+ ## 6. Menu Integration
135
143
 
136
- Registered sections appear as top-level menu rows after built-in core sections unless an `order` value says otherwise.
144
+ Sections appear in two locations:
137
145
 
138
- Rules:
146
+ ### Main menu
139
147
 
140
- - `label` must be compact enough for mobile Telegram
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
- ## Callback routing
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
- `pi-telegram` owns section callback transport.
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
- A section callback must include a `pi-telegram` owned prefix plus a compact section token that maps back to the full identity key.
160
+ ### Settings submenu
151
161
 
152
- Conceptual form:
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
- The token is an implementation detail. The registry maps it to the canonical section `id`. `section:` is reserved in the [Callback Namespace Standard](./callback-namespaces.md); section authors should build callbacks through section context helpers rather than hand-crafting `section:` payloads.
186
+ Example: `section:0:counter:5`
159
187
 
160
- Routing order:
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. Existing low-level external handlers may observe or consume the update first
164
- 3. Built-in menu callbacks are handled by built-in domains
165
- 4. Section callbacks are resolved by token and sent to the registered section
166
- 5. Unknown callbacks fall back to the existing callback namespace behavior when appropriate
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
- Section handlers return:
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
- - `"handled"` callback was handled, do not continue routing
171
- - `"pass"` — section declines this callback, allow fallback routing
206
+ ### Fallback chain in `handleCallback`
172
207
 
173
- Stale callbacks:
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
- - Missing section id or token should answer the callback with a short stale/expired notice
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
- ## Runtime ports
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
- A section receives a narrow context, not raw `pi-telegram` internals.
244
+ ## 9. Context Ports
182
245
 
183
- Initial safe ports:
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
- getQueueSnapshot(): TelegramQueueSnapshot;
195
- getSessionSnapshot?(): TelegramSessionSnapshot;
281
+ callbackData(action: string, payload?: string): string;
196
282
  }
197
283
  ```
198
284
 
199
- Filesystem or prompt-history mutation is not part of the baseline. Add capability-specific ports only when the first real extension needs them.
285
+ ### `enqueuePrompt` semantics
200
286
 
201
- ## Security and authorization
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
- `pi-telegram` keeps Telegram authorization ownership.
289
+ ### Capability scope
204
290
 
205
- Baseline rules:
291
+ Context ports are intentionally narrow. Sections **cannot**:
206
292
 
207
- - Section callbacks are accepted only from the paired/authorized Telegram user
208
- - Sections should not receive unauthorized updates
209
- - Sections must not start their own Telegram poller
210
- - Sections must not assume filesystem or session mutation rights
211
- - Sensitive capabilities should be exposed as explicit typed ports, not by passing raw process or bot clients
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
- For filesystem explorers, default to read-only browse and file-send behavior. Deleting, writing, shell execution, or rollback-like mutations require separate explicit capabilities and confirmation UI.
299
+ Add capability-specific ports only when the first real extension proves the need.
214
300
 
215
- ## Diagnostics
301
+ ## 10. Telegram Bot API Integration
216
302
 
217
- `pi-telegram` should be able to report registered sections.
303
+ ### `callback_data` contract
218
304
 
219
- Minimum diagnostic fields:
305
+ Section callbacks use the `section:` prefix owned by `pi-telegram`:
220
306
 
221
307
  ```text
222
- id
223
- label
224
- status: active | stale | error
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
- The identity key is sufficient as the owner label. Do not add a second owner field.
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
- Useful future fields:
315
+ ### Inline keyboard layout
231
316
 
232
- ```text
233
- registeredAt
234
- lastRenderAt
235
- lastCallbackAt
236
- callbackCount
237
- errorCount
238
- capabilities
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
- Diagnostics should be available through a status/debug surface without cluttering normal Telegram UI.
327
+ This applies to section callbacks as well the state check runs before dispatch.
242
328
 
243
- ## Relationship to callback namespaces and external handlers
329
+ ## 11. Pi Extension API Inspiration
244
330
 
245
- [Callback Namespaces](./callback-namespaces.md) define callback ownership names. [External Handlers](./external-handlers.md) define low-level raw update interception. Extension sections define the structured Telegram UI layer above both.
331
+ The platform inherits from π's own extension model:
246
332
 
247
- Sections still use namespaced callback data, but `pi-telegram` owns the `section:` prefix and maps compact tokens to canonical section identities. That keeps Telegram's 64-byte callback limit compatible with full npm package names such as `@llblab/pi-telegram-explorer`.
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
- Use external handlers when an extension needs direct raw Telegram update access, a custom callback namespace, or out-of-band Promise resolution.
341
+ ## 12. Diagnostics
250
342
 
251
- Use extension sections when an extension needs a durable menu surface, callback routing, and Telegram UI lifecycle managed by `pi-telegram`.
343
+ `getTelegramSectionDiagnostics()` returns:
252
344
 
253
- ## Relationship to command templates
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
- Command templates execute local commands and pipelines through stdin/stdout.
355
+ Available through `pi-telegram`'s `/telegram-status` or programmatically via `getTelegramSectionDiagnostics()`.
256
356
 
257
- Extension sections do not execute command templates by default. A section may call an extension-owned command or tool internally, but the section standard is a UI registration and callback-routing contract, not a shell execution contract.
357
+ ## 13. Purpose and Non-Goals
258
358
 
259
- ## Non-goals
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 in the base standard
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
- - Section registry
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
- First demo extension candidate:
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
- ```text
282
- @llblab/pi-telegram-explorer
283
- ```
390
+ ## 15. Demo Extension
284
391
 
285
- Initial demo scope:
392
+ `@llblab/pi-telegram-extension-demo` (`extensions/pi-telegram-extension-demo/`) is the reference implementation:
286
393
 
287
- - Browse current project tree read-only
288
- - View compact file previews
289
- - Send selected files as Telegram documents
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
- Defer rollback, filesystem writes, deletes, and broad mutation until the read-only and enqueue-only model is proven.
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
- getModelMenuState: getQueueMenuState,
303
- getStoredModelMenuState: modelMenuRuntime.getState,
304
- storeModelMenuState: modelMenuRuntime.storeState,
305
- editInteractiveMessage,
306
- sendInteractiveMessage,
307
- answerCallbackQuery,
308
- isProactivePushEnabled,
309
- setProactivePushEnabled,
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 ---