@llblab/pi-kit 0.4.0 → 0.5.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/CHANGELOG.md +4 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -3
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +39 -11
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -4
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/index.ts +4 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +12 -3
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +5 -3
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +72 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +81 -9
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +4 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +19 -8
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `@llblab/pi-kit` are documented here.
|
|
4
4
|
|
|
5
|
+
## 0.5.0 - 2026-09-05
|
|
6
|
+
|
|
7
|
+
- `Telegram Native Controls`: Advances the exact Telegram pin to `0.43.0`, bringing in-body button blocks, shared JSON/CML disabled controls, HTML footer fallback, and updated agent Skills while preserving package-owned implementations and the existing resource inventory.
|
|
8
|
+
|
|
5
9
|
## 0.4.0 - 2026-09-05
|
|
6
10
|
|
|
7
11
|
- `Expanded Composition`: Adds exact published pins for Clean Room and opt-in State Flow alongside the portable LLBLAB Skills collection, exposing each package's own declared resources without duplicating source or runtime ownership.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Package links lead to the owning repositories for usage, documentation, issues,
|
|
|
15
15
|
| [`@llblab/pi-codex-usage`](https://github.com/llblab/pi-codex-usage) | `0.9.4` | Compact Codex/Spark subscription-limit status |
|
|
16
16
|
| [`@llblab/pi-grow-loop`](https://github.com/llblab/pi-grow-loop) | `0.7.4` | Visible continuation scheduling and bounded worker Skills |
|
|
17
17
|
| [`@llblab/pi-state-flow`](https://github.com/llblab/pi-state-flow) | `0.3.0` | Opt-in validated state handoffs between agent runs |
|
|
18
|
-
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.
|
|
18
|
+
| [`@llblab/pi-telegram`](https://github.com/llblab/pi-telegram) | `0.43.0` | Telegram companion, queues, files, voice, controls, and Generative Apps guidance |
|
|
19
19
|
| [`@llblab/skills`](https://github.com/llblab/skills) | `1.14.0` | Portable workflows for engineering, review, design, context maintenance, and other focused tasks |
|
|
20
20
|
|
|
21
21
|
Versions are exact by design. An upstream release does not change an installed kit until this repository explicitly advances the dependency and publishes a new kit version. Runtime defects and package-specific feature requests belong in the linked repository; package selection and kit installation issues belong here.
|
|
@@ -4,6 +4,7 @@ _This file owns unresolved project work only. Completed behavior belongs in `CHA
|
|
|
4
4
|
|
|
5
5
|
- [ ] `Live-thread continuity and recovery`: Make loss and restoration of a live thread truthful and safe without discarding accepted work or deleting a currently owned target; the initial cause of the observed stale-thread API failures remains unproven.
|
|
6
6
|
- [ ] Complete the remaining operator-coordinated live smoke beyond the operator-confirmed successful leader Restore: verify follower Restore, inaccessible callbacks, already-absent chooser cleanup, direct stale-target diagnostics, and preservation of accepted local work/active-turn target. Use disposable test threads. The successful leader flow does not independently prove these failure paths or establish the initial stale-thread failure cause.
|
|
7
|
+
- [ ] `Native in-body controls live smoke`: Extend the operator-confirmed successful current-client smoke (singleton CML and mixed JSON/CML rows between paragraphs, ordinary prompt callbacks, labeled disabled control, separate footer callback, and HTML-mode footer fallback) to an independently identified second client, follower routing, blank disabled cells, and app-method dispatch/revision rejection. Do not infer coverage from the confirmed ordinary-button flow; in-body selection uses callback acknowledgement, not body recoloring.
|
|
7
8
|
- [ ] `Operator-confirmed pairing`: Replace first-contact automatic ownership with an explicit pending request that the trusted Pi interface can allow or reject before `allowedUserId` is persisted or any Telegram prompt executes; retain manual preconfiguration, bound pending-request lifetime and multiplicity, fail closed across session/transport replacement, and cover unauthorized-first-contact races.
|
|
8
9
|
- [ ] `Environment-backed bot tokens`: Let each profile store an exact Pi-native `$ENV_VAR` or `${ENV_VAR}` reference in `telegram.json` instead of copying the resolved secret; preserve the originating alias when `/telegram-setup` prefills from a supported environment variable, resolve only at validation/activation boundaries, fail closed with a redacted named-variable diagnostic when unresolved, retain literal-token compatibility, and cover named profiles, reload, persistence, status, and secret-redaction paths.
|
|
9
10
|
- [ ] [`Inference bypass Generative Apps`](./docs/generative-apps.md): Let the agent install and bind one managed JavaScript app owner for a generated-prompt prefix so deterministic controls can bypass model inference without creating another button grammar.
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
> Each release keeps at most 8 outcome records of at most 512 characters.
|
|
4
4
|
|
|
5
|
+
## 0.43.0: Native Button Blocks
|
|
6
|
+
|
|
7
|
+
- `In-Body Controls`: Renders standalone `telegram_button` fences as native button rows between paragraphs using the same singleton/mixed JSON/CML grammar and callback ownership as footer comments. Invalid/incomplete blocks register nothing, previews hide action payloads, literal examples remain inert, and HTML mode moves controls to the footer. In-body clicks acknowledge without recoloring; operator-confirmed current-client smoke passed; cross-client/follower checks remain pending.
|
|
8
|
+
- `Bot API Reference`: Corrects the local `sendRichMessageDraft` table against Bot API 10.3 with generation-stop parameters and draft replacement semantics, updates affected Skill indexes, and checks parity of stop-control documentation across both draft methods. Runtime generation controls are unchanged.
|
|
9
|
+
- `Disabled Controls`: Adds boolean JSON disabled state and the fourth CML button atom (`1`/`true` disables, `0`/`false` enables), including label-only `{Next|||1}` and blank `{|||1}` cells with no prompt or selected style. Disabled buttons stay visible without callback registration, prompt admission, or app invocation; shared keyboard types, Generative Apps, and agent Skills follow the same contract.
|
|
10
|
+
|
|
5
11
|
## 0.42.4: Thread Recovery Hotfix
|
|
6
12
|
|
|
7
13
|
- `Thread Restore`: Retains validated source and chooser identity when callback messages omit thread metadata, hides Restore in threadless/All choosers and explains how to supply a destination, rejects conflicting Restore instead of forwarding to the old target, and treats confirmed already-deleted chooser cleanup as complete without redispatch.
|
|
@@ -131,7 +131,7 @@ Enable the optional capabilities the bridge needs in the [@BotFather](https://t.
|
|
|
131
131
|
| Outbound artifacts | Return generated files through `telegram_attach` during active turns or explicit direct delivery. | Agents send real artifacts as files, not pasted blobs. |
|
|
132
132
|
| Voice input | Route audio through configured command-template handlers, programmatic handlers, or STT providers. | Voice notes become usable prompt context. |
|
|
133
133
|
| Voice output | Choose `manual`, `mirror`, or `always`; active automatic turns carry one compact `[voice] delivery: automatic voice` line, while explicit `telegram_voice` remains available. | Voice policy stays dynamic and model-legible without duplicating the full action contract in every prompt. |
|
|
134
|
-
| Buttons |
|
|
134
|
+
| Buttons | Use `telegram_button` comments for footer buttons or fenced blocks for native button rows between paragraphs. | Assistant-authored choices become native Telegram interactions. |
|
|
135
135
|
| Generative Apps | Install or explicitly replace a reviewed `.mjs` application whose generated JSON button view may mix direct `app::method` actions with ordinary model prompts. | Repeated games, controls, tutors, and adapters compile routine interaction without losing selective model interpretation, explanation, or adaptation. |
|
|
136
136
|
| Callback routing | Route known callbacks to the owner extension and unknown callbacks back into Pi. | Companion extensions can build UI without polling Telegram themselves. |
|
|
137
137
|
| Threaded Mode | Run one leader plus visible follower Pi instances through named private-chat threads. | One bot can host a local multi-instance Pi organism without hidden process spawning. |
|
|
@@ -222,7 +222,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
|
|
|
222
222
|
|
|
223
223
|
### Buttons And Callbacks
|
|
224
224
|
|
|
225
|
-
Assistant replies can
|
|
225
|
+
Assistant replies can place controls between paragraphs using standalone triple-backtick `telegram_button` blocks, or keep them in the footer using top-level hidden `telegram_button` comments. Both wrappers accept singleton cells and mixed JSON/CML matrices. Native in-body rows allow up to eight buttons; HTML compatibility moves these rows to the footer. In-body clicks acknowledge without recoloring the Rich body, while footer selection styles remain unchanged. Hidden comments accept a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal (CML). One adaptive matrix may mix named JSON objects with positional CML cells; separators are optional and one trailing comma is tolerated, including inside JSON objects. Top-level cells become full-width rows while nested rows group one or more buttons horizontally without an artificial parser-level width cap; generated surfaces default to five columns and use six to eight only for short position-bearing labels. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style` set to `primary`, `success`, or `danger`; omitting the first atom leaves the existing prompt-as-label fallback in charge, while the optional style still requires a non-empty prompt. A fourth atom adds disabled state: `{|Next||1}` or `{|Next||true}` disables, `0` or `false` enables (exact lowercase), and omission stays enabled; the third atom may be empty in this form. JSON uses boolean `disabled`. Disabled controls require no prompt or selected style: `{Next|||1}` shows only a label, while `{|||1}` is a blank disabled cell. Disabled controls remain visible without registering callbacks or invoking prompts/app methods. It trims atom boundaries, preserves non-structural text literally, and decodes only `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The bridge strips every assistant-authored HTML comment from Telegram previews and final replies regardless of Markdown position or owning extension, while only recognized top-level comments activate buttons or voice; comment-only output sends no text message and the Pi terminal transcript remains unchanged. It renders valid inline buttons and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `☑️ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
|
|
226
226
|
|
|
227
227
|
### Threaded Mode And Multi-Instance Bus
|
|
228
228
|
|
|
@@ -15,7 +15,7 @@ Living index of project documentation in `/docs`.
|
|
|
15
15
|
- [command-templates.md](./command-templates.md) — Portable command-template standard core
|
|
16
16
|
- [inbound.md](./inbound.md) — Local `pi-telegram` inbound text/media handler bus, programmatic inbound handlers, registered STT provider fallbacks, legacy `attachmentHandlers` compatibility, placeholders, and fallbacks
|
|
17
17
|
- [outbound.md](./outbound.md) — Local `pi-telegram` outbound-handler config, text/voice/button behavior, single-artifact Rich results, voice synthesis provider fallback priority, artifact outputs, and callback routing
|
|
18
|
-
- [compact-matrix-literal.md](./compact-matrix-literal.md) — Adaptive Button Literal / CML
|
|
18
|
+
- [compact-matrix-literal.md](./compact-matrix-literal.md) — Adaptive Button Literal / CML v4 standard for strict JSON objects, positional cells with disabled state, optional element commas, mixed bounded-depth matrices, atomic parsing, and renderer-owned width policy
|
|
19
19
|
- [generative-apps.md](./generative-apps.md) — Generative Apps runtime and wire contract for managed `.mjs` identity, inference-bypass bindings, persistent state timelines, bounded adapters, replacement, and lifecycle; agent operation lives in the bundled `generative-apps` Skill
|
|
20
20
|
- [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
|
|
21
21
|
- [updates.md](./updates.md) — Update classification and runtime handler registry that lets layered extensions observe and consume Telegram updates without owning their own polling connection
|
|
@@ -80,7 +80,7 @@ The repository uses a **Flat Domain DAG**:
|
|
|
80
80
|
- `preview` / `replies` / `rendering`: throttled native Rich Markdown draft delivery, native final reply delivery, reply parameters, transport-limit chunking, and remaining Telegram HTML rendering for bridge-owned UI/compatibility surfaces.
|
|
81
81
|
- `delivery`: public extension operational-view delivery, active-turn/instance/aggregate/authorized target policy, logical chunk handles, per-target ordering, runtime generation fencing, and the process-local runtime membrane. Its bridge adapter composes the established UI/compat reply renderer with narrow bus-aware Telegram API and ownership ports; it never exposes bot clients or Pi contexts.
|
|
82
82
|
- `activity`: public normalized Pi lifecycle registration, activity/source identity, assistant segment and reasoning normalization, executed-tool events, non-blocking per-handler queues, delivery contexts, compatibility adapters, and shutdown fencing. The same domain extends assistant-output observation for connected companion projection: eligible completed local/autonomous public segments retain source order and deduplicate event identity. `bindings` assembles observation, authority, sender, and failure-projection ports; routing owns exact delivery authority, outbound composes established transformations and reply delivery, and Bot API domains implement transport. No separate proactive state-machine domain exists.
|
|
83
|
-
- `outbound-markup`: top-level assistant action comment parsing, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
83
|
+
- `outbound-markup`: top-level assistant action comment/fence parsing, shared JSON/CML grammar, attribute parsing, voice reply planning, and preview/delivery stripping.
|
|
84
84
|
- `outbound`: outbound text transformations, voice/button artifact delivery, and generated callback actions.
|
|
85
85
|
- `generative-apps`: managed deterministic application identity, canonical installation and explicit replacement, content-addressed module loading, state timelines, cross-process transition serialization, bounded executable-plus-argv adaptation, `telegram_bind`, and pre-model-queue `app::method` invocation. It does not own Telegram transport, arbitrary shell execution, or the external application adapted by one Generative App.
|
|
86
86
|
- `outbound-attachments`: `telegram_attach`, queued outbound files, stat/limit checks, ordinary photo/document delivery, and narrow single-artifact Rich Message planning/sending for probe-confirmed photo/video/audio formats. It owns known-failure fallback eligibility and ambiguous-send no-replay classification through structural error contracts without importing Bot API helpers.
|
|
@@ -352,12 +352,14 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
|
|
|
352
352
|
|
|
353
353
|
Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
|
|
354
354
|
|
|
355
|
-
Assistant-authored final-message actions use hidden top-level comments:
|
|
355
|
+
Assistant-authored final-message actions use hidden top-level comments, with an additional fenced wrapper for in-body buttons:
|
|
356
356
|
|
|
357
357
|
- `telegram_voice` accepts one positional compact action cell or JSON object and creates one voice artifact through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
358
358
|
- `telegram_button` accepts a JSON object, adaptive JSON/CML matrix, or positional Compact Matrix Literal. Named JSON objects and positional cells may coexist, with commas optional only between completed matrix or row elements. Each top-level cell creates one full-width row, while a nested row groups buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. The JSON-first grammar, positional trim/escape rules, atomic rejection, and renderer-owned width policy are specified in [Adaptive Button Literal](./compact-matrix-literal.md).
|
|
359
359
|
|
|
360
|
-
|
|
360
|
+
Standalone column-zero triple-backtick `telegram_button` blocks reuse the same cell/matrix grammar and callback store, compiling to native Rich Markdown button rows between paragraphs. Rendering validates each complete block before registering any callbacks, escapes label text, preserves disabled cells, and leaves larger enclosing fences literal. Previews hide complete and unfinished action fences. HTML mode projects those controls into the footer. In-body callbacks acknowledge without rewriting the Rich message; selected-style highlighting remains footer-only.
|
|
361
|
+
|
|
362
|
+
Action recognition remains restricted to top-level column-zero comments and exact-name button fences so nested examples cannot trigger voice, buttons, or callbacks. The Telegram surface independently strips every complete assistant-authored HTML comment from previews and final delivery regardless of Markdown position or comment owner; an unclosed comment is withheld through the remaining tail, and a comment-only result sends no text message. Pi's terminal transcript and model context remain unchanged.
|
|
361
363
|
|
|
362
364
|
Unknown callback data outside owned prefixes is forwarded as `[callback] <data>` only after built-in and extension handlers decline it.
|
|
363
365
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Adaptive Button Literal
|
|
2
2
|
|
|
3
|
-
> Status: Portable CML
|
|
3
|
+
> Status: Portable CML v4 button standard; `pi-telegram` also reuses its positional-cell mechanics for compact voice actions.
|
|
4
4
|
|
|
5
5
|
Adaptive Button Literal is one bounded-depth matrix grammar over a shared button AST. It accepts strict JSON button objects, positional Compact Matrix Literal (CML) cells, or both in the same matrix and row. Commas between completed matrix or row elements are optional, so producers can progressively compress representation without changing runtime meaning.
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Adaptive Button Literal is one bounded-depth matrix grammar over a shared button
|
|
|
8
8
|
full named JSON → comma-optional adjacency → mixed named/positional cells → compact CML
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
The compact form is not JSON: `{label|prompt|
|
|
11
|
+
The compact form is not JSON: `{label|prompt|selected_style|disabled}` assigns meaning by position. The formats share semantics and topology, not syntax.
|
|
12
12
|
|
|
13
13
|
## Goals
|
|
14
14
|
|
|
@@ -23,7 +23,7 @@ The compact form is not JSON: `{label|prompt|variant}` assigns meaning by positi
|
|
|
23
23
|
Every accepted payload normalizes to a non-empty ordered list of non-empty rows:
|
|
24
24
|
|
|
25
25
|
```text
|
|
26
|
-
Cell = { label?: string, prompt?: string, value?: string, selected_style?: string }
|
|
26
|
+
Cell = { label?: string, prompt?: string, value?: string, selected_style?: string, disabled?: boolean }
|
|
27
27
|
Rows = Cell[][]
|
|
28
28
|
```
|
|
29
29
|
|
|
@@ -58,7 +58,7 @@ A two-atom cell separates label and prompt:
|
|
|
58
58
|
{Pause|music::pause}
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
The first label position may be empty. Prompt-only `{|e2}` is equivalent to JSON `{"prompt":"e2"}`: the existing button fallback uses `e2` as both visible text and queued prompt without requiring a separately authored label.
|
|
62
62
|
|
|
63
63
|
A three-atom cell adds the selected style and retains the same optional-label form:
|
|
64
64
|
|
|
@@ -67,7 +67,18 @@ A three-atom cell adds the selected style and retains the same optional-label fo
|
|
|
67
67
|
{|e2|success}
|
|
68
68
|
```
|
|
69
69
|
|
|
70
|
-
|
|
70
|
+
In a three-atom cell, the prompt and style remain required; style accepts only `primary`, `success`, or `danger`.
|
|
71
|
+
|
|
72
|
+
A four-atom cell adds disabled state:
|
|
73
|
+
|
|
74
|
+
```text
|
|
75
|
+
{Next|||1}
|
|
76
|
+
{|||1}
|
|
77
|
+
{Next|counter::next|success|1}
|
|
78
|
+
{Next|counter::next||0}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; omitting the fourth position keeps the button enabled. The third atom may be empty in this form to retain the default selected style. Disabled cells may omit the prompt, label, and selected style: `{Next|||1}` is a label-only control and `{|||1}` is a blank disabled cell. Enabled CML cells still require a prompt. Telegram requires a `text` field, so its renderer sends a non-breaking space (`U+00A0`) for a blank disabled cell; the notation itself does not invent a label or action. Client rendering of blank cells requires live verification. JSON uses a boolean `disabled`; other value types are invalid. Disabled cells remain visible in their original row but register no action and carry no callback data.
|
|
71
82
|
|
|
72
83
|
## Adaptive Grammar
|
|
73
84
|
|
|
@@ -83,6 +94,8 @@ boundary := ws [","] ws
|
|
|
83
94
|
positional-cell := "{" atom "}"
|
|
84
95
|
| "{" [atom] "|" atom "}"
|
|
85
96
|
| "{" [atom] "|" atom "|" atom "}"
|
|
97
|
+
| "{" [atom] "|" atom "|" [atom] "|" ("0" | "false") "}"
|
|
98
|
+
| "{" [atom] "|" [atom] "|" [atom] "|" ("1" | "true") "}"
|
|
86
99
|
atom := atom-unit+
|
|
87
100
|
atom-unit := ordinary | "\|" | "\}" | "\\"
|
|
88
101
|
ws := *(SP | HTAB | CR | LF)
|
|
@@ -117,7 +130,7 @@ A conforming parser:
|
|
|
117
130
|
3. Tries one complete strict JSON object, then bounded trailing-comma recovery, at each cell boundary before positional interpretation.
|
|
118
131
|
4. Keeps JSON-shaped named objects on the JSON path when validation fails instead of exposing their source as positional text.
|
|
119
132
|
5. Accepts at most one comma between elements or immediately before a closing row or matrix delimiter, while rejecting leading, repeated, or property-level omitted commas.
|
|
120
|
-
6. Rejects empty matrices, rows, one-atom cells, prompts,
|
|
133
|
+
6. Rejects empty matrices, rows, one-atom cells, enabled-cell prompts, and nesting deeper than one row. The label may be empty in multi-atom cells; the style may additionally be empty in four-atom cells. Disabled four-atom cells may omit all first three atoms. Rejects unknown styles, invalid disabled values, and more than four button atoms.
|
|
121
134
|
7. Decodes only `\|`, `\}`, and `\\` in positional cells.
|
|
122
135
|
8. Extracts the first complete valid payload from a tolerant comment envelope and ignores unrelated text or isolated unmatched matrix brackets around it.
|
|
123
136
|
9. Returns no partial rows or cells from a balanced malformed candidate.
|
|
@@ -127,17 +140,32 @@ Malformed JSON-looking input receives no generic recovery. It is accepted only i
|
|
|
127
140
|
|
|
128
141
|
## Telegram Profiles
|
|
129
142
|
|
|
130
|
-
For `telegram_button` comments:
|
|
143
|
+
For `telegram_button` hidden comments and fenced blocks:
|
|
131
144
|
|
|
132
145
|
- JSON `value` keeps its existing label/prompt fallback semantics.
|
|
133
146
|
- Positional `{value}` is equivalent to JSON `{"value":"value"}`; a lone JSON `label` or `prompt` has the same both-fields shorthand semantics.
|
|
134
147
|
- Positional `{|prompt}` is equivalent to JSON `{"prompt":"prompt"}` and therefore uses the prompt as both visible text and queued prompt.
|
|
135
148
|
- Positional `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
|
|
136
149
|
- Positional `{label|prompt|selected_style}` and `{|prompt|selected_style}` are equivalent to their corresponding JSON objects.
|
|
150
|
+
- Positional `{label|prompt|selected_style|1}` is equivalent to JSON `{"label":"label","prompt":"prompt","selected_style":"selected_style","disabled":true}` with a valid selected style; `{|prompt||1}` is equivalent to `{"prompt":"prompt","disabled":true}`. Fourth-position `0` or `false` matches `disabled: false`; `true` is equivalent to `1`.
|
|
151
|
+
- `{label|||1}` matches JSON `{"label":"label","disabled":true}`; `{|||1}` matches `{"disabled":true}`. Disabled cells require no prompt and do not retain action or selected-style semantics.
|
|
152
|
+
- Disabled buttons serialize as `{ text, disabled: {} }`, without `callback_data`; they neither enqueue prompts nor invoke bound app methods. Enabled controls keep existing selection and callback behavior.
|
|
137
153
|
- Top-level cells become full-width rows.
|
|
138
154
|
- Nested rows become horizontal keyboard rows.
|
|
139
155
|
- Invalid payloads are stripped with their recognized action comment and register no callbacks.
|
|
140
156
|
|
|
157
|
+
The wrapper selects placement without changing cell semantics. A hidden `telegram_button` HTML comment builds the footer keyboard. A standalone column-zero fenced block opened by exactly three backticks plus `telegram_button` renders rows between paragraphs in Native Rich Markdown. A singleton JSON/CML object needs no array in either wrapper. Fenced content must be one complete payload without trailing envelope text. Larger outer fences and ordinary code blocks remain literal examples; unclosed action fences are withheld. Native Rich rows support at most eight buttons and must fit one message chunk; these are renderer constraints, not grammar limits. HTML compatibility places fenced controls in the footer. See [Outbound](./outbound.md) for delivery and callback behavior.
|
|
158
|
+
|
|
159
|
+
````markdown
|
|
160
|
+
Description.
|
|
161
|
+
|
|
162
|
+
```telegram_button
|
|
163
|
+
[{Details|Explain this section.}[{Choose|Choose this option.}{Unavailable|||true}]]
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Next paragraph.
|
|
167
|
+
````
|
|
168
|
+
|
|
141
169
|
For `telegram_voice`, one positional cell maps `{text}`, `{text|lang}`, or `{text|lang|rate}` to one voice artifact. JSON object cells remain available for named fields, escaping, and multiline text. Voice comments do not accept matrix or row composition.
|
|
142
170
|
|
|
143
171
|
Example:
|
|
@@ -157,7 +185,7 @@ The grammar imposes no visual row-width maximum. Renderer and interaction policy
|
|
|
157
185
|
Accepted classes include:
|
|
158
186
|
|
|
159
187
|
- Strict JSON objects and matrices.
|
|
160
|
-
- Positional singleton, two-atom, prompt-only `{|prompt}`, and
|
|
188
|
+
- Positional singleton, two-atom, prompt-only `{|prompt}`, styled, and four-atom disabled-state cells.
|
|
161
189
|
- Matrices and rows with commas, without commas, or a mixture of boundaries.
|
|
162
190
|
- Named JSON and positional cells mixed in one matrix or row.
|
|
163
191
|
- Literal commas inside positional atoms and strict JSON strings.
|
|
@@ -166,12 +194,12 @@ Accepted classes include:
|
|
|
166
194
|
|
|
167
195
|
Rejected classes include:
|
|
168
196
|
|
|
169
|
-
- Empty payloads, matrices, rows, one-atom cells, prompts, or
|
|
197
|
+
- Empty payloads, matrices, rows, one-atom cells, enabled-cell prompts, or three-atom styles; disabled four-atom cells may omit the first three atoms but not the disabled flag.
|
|
170
198
|
- Leading or repeated element commas.
|
|
171
199
|
- Missing commas between properties inside a JSON object.
|
|
172
200
|
- Deeper row nesting.
|
|
173
201
|
- Missing, crossed, or mismatched delimiters.
|
|
174
|
-
- A
|
|
202
|
+
- A fourth positional separator, unknown style, invalid disabled flag, unknown escape, or trailing backslash.
|
|
175
203
|
- Internal control characters.
|
|
176
204
|
- Valid JSON that fails the existing JSON action schema.
|
|
177
205
|
|
|
@@ -179,4 +207,4 @@ Every rejected case proves zero callback registration.
|
|
|
179
207
|
|
|
180
208
|
## Versioning
|
|
181
209
|
|
|
182
|
-
This document defines CML
|
|
210
|
+
This document defines CML v4. V4 adds a fourth button atom for disabled state, permits an empty third atom in that form, and makes labels and prompts optional for disabled cells. Existing one-, two-, and three-atom button cells, JSON/CML mixing, optional element-boundary commas, and prompt-only fallback semantics remain unchanged. Voice cells retain their three-atom maximum. JSON object internals and nesting depth remain unchanged. Future versions must preserve strict-JSON-first routing, bounded depth, atomic rejection, and an explicit discriminator for any new meaning at a security or ownership boundary.
|
|
@@ -49,7 +49,7 @@ An app name is a unique lowercase ASCII identifier accepted by the runtime's pat
|
|
|
49
49
|
|
|
50
50
|
## Inference-Bypass Syntax
|
|
51
51
|
|
|
52
|
-
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. A bound action is encoded entirely in the prompt string:
|
|
52
|
+
Compact Matrix Literal and full JSON buttons keep their existing `label + prompt` contract. App output may use hidden `telegram_button` comments for footer controls or standalone triple-backtick `telegram_button` blocks between paragraphs; both singleton cells and mixed matrices retain the same generation/revision-bound routing. In-body controls acknowledge clicks without recoloring the message; HTML compatibility moves them to the footer. A bound action is encoded entirely in the prompt string:
|
|
53
53
|
|
|
54
54
|
```ebnf
|
|
55
55
|
bound-action = app "::" method [ "(" json-value ")" ]
|
|
@@ -83,7 +83,7 @@ Equivalent JSON:
|
|
|
83
83
|
]
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
`app` is not a button property. Both representations normalize to the same prompt string, and routing happens afterward.
|
|
86
|
+
`app` is not a button property. Both representations normalize to the same prompt string, and routing happens afterward. The shared [button grammar](./compact-matrix-literal.md) also supports disabled controls: `{Call|||1}` or JSON `{"label":"Call","disabled":true}`. A disabled cell needs no method prompt or selected style; `{|||1}` is an intentional blank disabled cell. These remain visible but register no callback or bound action. App methods must still validate current state when invoked through an enabled control or another supported entrypoint; disabled presentation is not an authorization boundary.
|
|
87
87
|
|
|
88
88
|
The double colon is the inference-bypass operator: it routes a generated prompt control to a registered deterministic owner before Pi queue admission. Native extension callbacks retain their existing single-colon grammar:
|
|
89
89
|
|
|
@@ -155,12 +155,14 @@ I can continue.
|
|
|
155
155
|
|
|
156
156
|
Rules:
|
|
157
157
|
|
|
158
|
-
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
158
|
+
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling, permitting an empty style atom in this form: `{|Next||1}`. An omitted fourth position stays enabled. JSON uses boolean `disabled`; legacy attributes accept `disabled="true"` or `disabled="false"`. Invalid disabled values reject the candidate matrix atomically. Disabled cells require no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank, equivalent to JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`. They retain their row and label, serialize as `{ text, disabled: {} }` (non-breaking-space text for a blank cell), and register no callback, prompt, or bound app action. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
159
159
|
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, one-field `{value}`, or prompt-only `{|prompt}` is present, that string supplies both visible label and queued prompt. An explicit counterpart takes precedence over `value`. Use JSON with `\n` escapes for multiline prompts.
|
|
160
160
|
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it does not activate a button action and is still removed from the Telegram surface.
|
|
161
|
-
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a
|
|
161
|
+
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a fourth unescaped CML separator, empty enabled-cell prompts or three-atom styles, invalid disabled flags, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Multi-atom cells may omit the label; four-atom cells may additionally omit the selected style, and disabled cells may omit the prompt. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
162
162
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
163
|
-
-
|
|
163
|
+
- Standalone column-zero fences opened by exactly three backticks and `telegram_button` use the same singleton/matrix JSON/CML grammar. Complete valid blocks become native `<tg-button-row>` elements in `InputRichMessage.markdown`, at their original positions. Each row supports at most eight buttons and must fit a Rich Message chunk; validation precedes callback registration for the entire block. Labels are escaped, prompts never enter the native markup, and disabled cells have no callback. Missing closures, trailing payload garbage, and invalid blocks produce no controls. Other language names and enclosing code examples stay literal. Draft previews withhold complete and unfinished action fences without registering actions. HTML compatibility projects fenced rows into the bottom keyboard before comment-authored footer rows; source ordering within each group is retained.
|
|
164
|
+
- In-body controls share prompt/app dispatch, TTL, generation/revision checks, and sent-message target ownership with footer controls. Their acknowledgement is the selection feedback; `selected_style` does not recolor the Rich body. No callback-triggered body rewrite is introduced, preventing a stale click from overwriting newer app output. The operator confirmed the post-reload current-client smoke: singleton CML and mixed JSON/CML in-body rows, their ordinary prompt callbacks, a labeled disabled control, and the separate footer callback work as intended. Client type was not recorded. The operator also confirmed that HTML mode projects the in-body groups into the bottom keyboard as intended. Second-client coverage, follower routing, blank cells, and app-method dispatch/revision rejection remain live-gated. Bot API evidence: `InputRichBlockButtons` / Rich Markdown `tg-button-row` and `RichMessageButton` use ordinary `callback_data` and support disabled controls.
|
|
165
|
+
- After Telegram accepts a generated footer button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
164
166
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `☑️ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
|
165
167
|
|
|
166
168
|
Do not emit inline comments after visible text, standalone button actions, or tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
|
|
@@ -176,7 +178,7 @@ The extension injects prompt guidance by context:
|
|
|
176
178
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
|
|
177
179
|
- For Telegram-originated turns, write the full technical answer as normal Markdown.
|
|
178
180
|
- Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
|
|
179
|
-
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style`
|
|
181
|
+
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone triple-backtick `telegram_button` block for placement between paragraphs. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` and boolean `disabled` are optional. Keep at least one useful enabled action on an interactive surface. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
|
|
180
182
|
- For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
|
|
181
183
|
- Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `☑️ **Choose an option:**` fallback rather than manufacturing duplicate text.
|
|
182
184
|
|
|
@@ -68,7 +68,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
|
|
|
68
68
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
69
69
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
70
70
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
|
|
71
|
-
- `telegram_button` hidden comments create
|
|
71
|
+
- `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
|
|
72
72
|
|
|
73
73
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
74
74
|
|
|
@@ -245,7 +245,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
245
245
|
Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
|
|
246
246
|
const buttonActionStore = Outbound.createTelegramButtonActionStore();
|
|
247
247
|
const planGenerativeAppOutput =
|
|
248
|
-
Outbound.createTelegramOutboundReplyPlanner(
|
|
248
|
+
Outbound.createTelegramOutboundReplyPlanner(
|
|
249
|
+
buttonActionStore,
|
|
250
|
+
configControls.getAssistantRenderingMode,
|
|
251
|
+
);
|
|
249
252
|
const pendingModelSwitchStore =
|
|
250
253
|
Model.createPendingModelSwitchStore<
|
|
251
254
|
Model.ScopedTelegramModel<ActivePiModel>
|
|
@@ -518,7 +518,10 @@ export function registerTelegramCommandsAndTools({
|
|
|
518
518
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
519
519
|
agentDir,
|
|
520
520
|
getActiveTurn: activeTurnRuntime.get,
|
|
521
|
-
planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
521
|
+
planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
522
|
+
buttonActionStore,
|
|
523
|
+
Config.createTelegramConfigControls(configStore).getAssistantRenderingMode,
|
|
524
|
+
),
|
|
522
525
|
sendMarkdownReply,
|
|
523
526
|
recordRuntimeEvent,
|
|
524
527
|
});
|
|
@@ -538,7 +541,10 @@ export function registerTelegramCommandsAndTools({
|
|
|
538
541
|
routeAgentMessage,
|
|
539
542
|
canSendDirect,
|
|
540
543
|
planMessage:
|
|
541
|
-
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
544
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
545
|
+
buttonActionStore,
|
|
546
|
+
Config.createTelegramConfigControls(configStore).getAssistantRenderingMode,
|
|
547
|
+
),
|
|
542
548
|
sendMarkdownMessage: (chatId, markdown, options) =>
|
|
543
549
|
sendMarkdownReply(chatId, undefined, markdown, options),
|
|
544
550
|
recordRuntimeEvent,
|
|
@@ -854,7 +860,10 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
854
860
|
});
|
|
855
861
|
};
|
|
856
862
|
const outboundReplyPlanner =
|
|
857
|
-
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
863
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
864
|
+
buttonActionStore,
|
|
865
|
+
getAssistantRenderingMode,
|
|
866
|
+
);
|
|
858
867
|
const voiceReplySenderDeps = {
|
|
859
868
|
execCommand: CommandTemplates.execCommandTemplate,
|
|
860
869
|
sendMultipart: callMultipart,
|
|
@@ -9,11 +9,13 @@ export type TelegramInlineKeyboardButtonStyle =
|
|
|
9
9
|
| "success"
|
|
10
10
|
| "primary";
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type TelegramInlineKeyboardButton = {
|
|
13
13
|
text: string;
|
|
14
|
-
callback_data: string;
|
|
15
14
|
style?: TelegramInlineKeyboardButtonStyle;
|
|
16
|
-
}
|
|
15
|
+
} & (
|
|
16
|
+
| { callback_data: string; disabled?: never }
|
|
17
|
+
| { disabled: Record<string, never>; callback_data?: never }
|
|
18
|
+
);
|
|
17
19
|
|
|
18
20
|
export interface TelegramInlineKeyboardMarkup {
|
|
19
21
|
inline_keyboard: TelegramInlineKeyboardButton[][];
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
} from "./keyboard.ts";
|
|
13
13
|
import {
|
|
14
14
|
parseTelegramActionPayloadRows,
|
|
15
|
+
parseTelegramButtonPayloadRows,
|
|
16
|
+
replaceTelegramButtonFences,
|
|
15
17
|
replaceTopLevelHtmlComments,
|
|
16
18
|
} from "./outbound-markup.ts";
|
|
17
19
|
import {
|
|
@@ -34,6 +36,7 @@ export interface TelegramOutboundButtonAction {
|
|
|
34
36
|
prompt: string;
|
|
35
37
|
binding?: TelegramOutboundButtonBinding;
|
|
36
38
|
selectedStyle?: TelegramInlineKeyboardButtonStyle;
|
|
39
|
+
disabled?: true;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export interface TelegramOutboundButtonStoredAction extends TelegramOutboundButtonAction {
|
|
@@ -115,6 +118,12 @@ function parseTelegramButtonAction(
|
|
|
115
118
|
const explicitLabel = getTelegramButtonString(payload, "label");
|
|
116
119
|
const explicitPrompt = getTelegramButtonString(payload, "prompt");
|
|
117
120
|
const label = explicitLabel ?? value ?? explicitPrompt;
|
|
121
|
+
if (payload.disabled !== undefined && typeof payload.disabled !== "boolean") {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (payload.disabled === true) {
|
|
125
|
+
return { text: label ?? "", prompt: "", disabled: true };
|
|
126
|
+
}
|
|
118
127
|
const prompt = explicitPrompt ?? value ?? explicitLabel;
|
|
119
128
|
if (!label || !prompt) return undefined;
|
|
120
129
|
const selectedStyle = payload.selected_style;
|
|
@@ -159,6 +168,7 @@ export function createTelegramButtonActionStore(
|
|
|
159
168
|
return {
|
|
160
169
|
text: action.text,
|
|
161
170
|
prompt: action.prompt,
|
|
171
|
+
...(action.disabled ? { disabled: true as const } : {}),
|
|
162
172
|
...(action.binding ? { binding: action.binding } : {}),
|
|
163
173
|
...(action.selectedStyle
|
|
164
174
|
? { selectedStyle: action.selectedStyle }
|
|
@@ -171,33 +181,75 @@ export function createTelegramButtonActionStore(
|
|
|
171
181
|
const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
|
|
172
182
|
"☑️ **Choose an option:**";
|
|
173
183
|
|
|
184
|
+
function escapeTelegramRichButtonText(text: string): string {
|
|
185
|
+
return text.replace(/[&<>"'`\\*_\[\]{}$~\r\n]/g, (character) =>
|
|
186
|
+
`&#${character.charCodeAt(0)};`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderTelegramRichButtonRow(
|
|
191
|
+
row: TelegramOutboundButtonMarkup["inline_keyboard"][number],
|
|
192
|
+
): string {
|
|
193
|
+
return `<tg-button-row>${row.map((button) => {
|
|
194
|
+
const attributes = button.disabled
|
|
195
|
+
? 'type="disabled"'
|
|
196
|
+
: `type="callback_data" data="${escapeTelegramRichButtonText(button.callback_data)}"`;
|
|
197
|
+
return `<tg-button ${attributes}>${escapeTelegramRichButtonText(button.text)}</tg-button>`;
|
|
198
|
+
}).join("")}</tg-button-row>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
174
201
|
export function planTelegramButtonReply(
|
|
175
202
|
markdown: string,
|
|
176
203
|
deps: {
|
|
177
204
|
registerAction: (action: TelegramOutboundButtonAction) => string;
|
|
178
205
|
binding?: TelegramOutboundButtonBinding;
|
|
206
|
+
rendering?: "rich" | "html";
|
|
179
207
|
},
|
|
180
208
|
): TelegramButtonReplyPlan {
|
|
181
209
|
const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
|
|
182
|
-
const
|
|
210
|
+
const buildRows = (
|
|
211
|
+
payloadRows: Record<string, unknown>[][],
|
|
212
|
+
rich: boolean,
|
|
213
|
+
): TelegramOutboundButtonMarkup["inline_keyboard"] | undefined => {
|
|
214
|
+
const actions = payloadRows.map((row) => row.map(parseTelegramButtonAction));
|
|
215
|
+
if (actions.some((row) => row.some((action) => !action))) return undefined;
|
|
216
|
+
if (rich && actions.some((row) => {
|
|
217
|
+
const projected = row.map((action) => action!.disabled
|
|
218
|
+
? { text: action!.text || "\u00a0", disabled: {} }
|
|
219
|
+
: { text: action!.text, callback_data: "x".repeat(64) });
|
|
220
|
+
return row.length > 8 || renderTelegramRichButtonRow(projected).length > 32768;
|
|
221
|
+
})) return undefined;
|
|
222
|
+
return actions.map((row) => row.map((action) => action!.disabled
|
|
223
|
+
? { text: action!.text || "\u00a0", disabled: {} }
|
|
224
|
+
: {
|
|
225
|
+
text: action!.text,
|
|
226
|
+
callback_data: deps.registerAction({
|
|
227
|
+
...action!,
|
|
228
|
+
...(deps.binding ? { binding: deps.binding } : {}),
|
|
229
|
+
}),
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
const withRichButtons = replaceTelegramButtonFences(markdown, (payload, closed) => {
|
|
233
|
+
if (!closed) return "";
|
|
234
|
+
const payloadRows = parseTelegramButtonPayloadRows(payload);
|
|
235
|
+
if (!payloadRows) return "";
|
|
236
|
+
const rich = deps.rendering !== "html";
|
|
237
|
+
const rows = buildRows(payloadRows, rich);
|
|
238
|
+
if (!rows) return "";
|
|
239
|
+
if (!rich) {
|
|
240
|
+
keyboard.push(...rows);
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
|
|
244
|
+
});
|
|
245
|
+
const stripped = replaceTopLevelHtmlComments(withRichButtons, (comment) => {
|
|
183
246
|
const command = "telegram_button";
|
|
184
247
|
const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
185
248
|
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
186
249
|
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
187
250
|
if (!payloadRows) return "";
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
-
if (actionRows.some((row) => row.some((action) => !action))) return "";
|
|
192
|
-
for (const actionRow of actionRows) {
|
|
193
|
-
keyboard.push(actionRow.map((action) => ({
|
|
194
|
-
text: action!.text,
|
|
195
|
-
callback_data: deps.registerAction({
|
|
196
|
-
...action!,
|
|
197
|
-
...(deps.binding ? { binding: deps.binding } : {}),
|
|
198
|
-
}),
|
|
199
|
-
})));
|
|
200
|
-
}
|
|
251
|
+
const rows = buildRows(payloadRows, false);
|
|
252
|
+
if (rows) keyboard.push(...rows);
|
|
201
253
|
return "";
|
|
202
254
|
});
|
|
203
255
|
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
|
|
@@ -254,7 +306,7 @@ export function markTelegramButtonSelected(
|
|
|
254
306
|
let matched = false;
|
|
255
307
|
const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
|
|
256
308
|
row.map((button) => {
|
|
257
|
-
if (button.callback_data !== callbackData) return { ...button };
|
|
309
|
+
if (button.disabled || button.callback_data !== callbackData) return { ...button };
|
|
258
310
|
matched = true;
|
|
259
311
|
return { ...button, style: selectedStyle };
|
|
260
312
|
}),
|
|
@@ -277,6 +329,11 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
|
|
|
277
329
|
return false;
|
|
278
330
|
}
|
|
279
331
|
|
|
332
|
+
if (action.disabled) {
|
|
333
|
+
await deps.answerCallbackQuery(query.id, "Button action unavailable.");
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
280
337
|
const chatId = query.message?.chat?.id;
|
|
281
338
|
const messageId = query.message?.message_id;
|
|
282
339
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
@@ -90,6 +90,50 @@ export function collectTopLevelHtmlComments(markdown: string): {
|
|
|
90
90
|
return { comments };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export function replaceTelegramButtonFences(
|
|
94
|
+
markdown: string,
|
|
95
|
+
replace: (payload: string, closed: boolean) => string,
|
|
96
|
+
): string {
|
|
97
|
+
let result = "";
|
|
98
|
+
let copied = 0;
|
|
99
|
+
let offset = 0;
|
|
100
|
+
let fence: TelegramTopLevelFenceState | undefined;
|
|
101
|
+
let actionStart: number | undefined;
|
|
102
|
+
let contentStart = 0;
|
|
103
|
+
while (offset < markdown.length) {
|
|
104
|
+
const end = getMarkdownLineEnd(markdown, offset);
|
|
105
|
+
const line = getMarkdownLineText(markdown, offset, end);
|
|
106
|
+
if (fence) {
|
|
107
|
+
if (isTopLevelClosingFence(line, fence)) {
|
|
108
|
+
if (actionStart !== undefined) {
|
|
109
|
+
result += markdown.slice(copied, actionStart);
|
|
110
|
+
result += replace(markdown.slice(contentStart, offset), true) + "\n";
|
|
111
|
+
copied = end;
|
|
112
|
+
actionStart = undefined;
|
|
113
|
+
}
|
|
114
|
+
fence = undefined;
|
|
115
|
+
}
|
|
116
|
+
} else if (line.includes("<!--")) {
|
|
117
|
+
const close = markdown.indexOf("-->", offset + line.indexOf("<!--") + 4);
|
|
118
|
+
if (close < 0) break;
|
|
119
|
+
offset = getMarkdownLineEnd(markdown, close + 3);
|
|
120
|
+
continue;
|
|
121
|
+
} else {
|
|
122
|
+
fence = getTopLevelOpeningFence(line);
|
|
123
|
+
if (fence && /^```telegram_button[ \t]*$/.test(line)) {
|
|
124
|
+
actionStart = offset;
|
|
125
|
+
contentStart = end;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
offset = end;
|
|
129
|
+
}
|
|
130
|
+
if (actionStart !== undefined) {
|
|
131
|
+
result += markdown.slice(copied, actionStart);
|
|
132
|
+
return result + replace(markdown.slice(contentStart), false);
|
|
133
|
+
}
|
|
134
|
+
return result + markdown.slice(copied);
|
|
135
|
+
}
|
|
136
|
+
|
|
93
137
|
export function replaceTopLevelHtmlComments(
|
|
94
138
|
markdown: string,
|
|
95
139
|
replacer: (comment: TelegramTopLevelHtmlComment) => string,
|
|
@@ -275,17 +319,26 @@ type TelegramCompactActionPayloadParser = (
|
|
|
275
319
|
function parseTelegramButtonCompactActionPayload(
|
|
276
320
|
atoms: readonly string[],
|
|
277
321
|
): Record<string, unknown> | undefined {
|
|
278
|
-
const [label, prompt, selectedStyle] = atoms;
|
|
322
|
+
const [label, prompt, selectedStyle, disabled] = atoms;
|
|
279
323
|
if (atoms.length === 1) return label ? { value: label } : undefined;
|
|
280
|
-
|
|
324
|
+
const isDisabled = disabled === "1" || disabled === "true";
|
|
325
|
+
if (!prompt && !(atoms.length === 4 && isDisabled)) return undefined;
|
|
281
326
|
const action = label ? { label, prompt } : { prompt };
|
|
282
327
|
if (atoms.length === 2) return action;
|
|
283
328
|
if (
|
|
284
329
|
selectedStyle !== "primary" &&
|
|
285
330
|
selectedStyle !== "success" &&
|
|
286
|
-
selectedStyle !== "danger"
|
|
331
|
+
selectedStyle !== "danger" &&
|
|
332
|
+
!(atoms.length === 4 && selectedStyle === "")
|
|
287
333
|
) return undefined;
|
|
288
|
-
|
|
334
|
+
if (atoms.length === 4 && !isDisabled && disabled !== "0" && disabled !== "false") {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
...action,
|
|
339
|
+
...(selectedStyle ? { selected_style: selectedStyle } : {}),
|
|
340
|
+
...(atoms.length === 4 ? { disabled: isDisabled } : {}),
|
|
341
|
+
};
|
|
289
342
|
}
|
|
290
343
|
|
|
291
344
|
function parseTelegramVoiceCompactActionPayload(
|
|
@@ -303,7 +356,7 @@ function parseTelegramVoiceCompactActionPayload(
|
|
|
303
356
|
function parseTelegramAdaptiveActionPayloadRows(
|
|
304
357
|
source: string,
|
|
305
358
|
parseCompactPayload: TelegramCompactActionPayloadParser,
|
|
306
|
-
options: { allowTrailing?: boolean } = {},
|
|
359
|
+
options: { allowTrailing?: boolean; maxCompactAtoms?: number } = {},
|
|
307
360
|
): { rows: Record<string, unknown>[][]; end: number } | undefined {
|
|
308
361
|
let offset = 0;
|
|
309
362
|
const isStructuralWhitespace = (character: string | undefined): boolean =>
|
|
@@ -343,7 +396,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
343
396
|
continue;
|
|
344
397
|
}
|
|
345
398
|
if (character === "|") {
|
|
346
|
-
if (atomSources.length >= 3) return undefined;
|
|
399
|
+
if (atomSources.length >= (options.maxCompactAtoms ?? 3)) return undefined;
|
|
347
400
|
atomSources.push([]);
|
|
348
401
|
offset += 1;
|
|
349
402
|
continue;
|
|
@@ -509,6 +562,16 @@ function isPlausibleTelegramMatrixStart(
|
|
|
509
562
|
);
|
|
510
563
|
}
|
|
511
564
|
|
|
565
|
+
export function parseTelegramButtonPayloadRows(
|
|
566
|
+
source: string,
|
|
567
|
+
): Record<string, unknown>[][] | undefined {
|
|
568
|
+
return parseTelegramAdaptiveActionPayloadRows(
|
|
569
|
+
source.trim(),
|
|
570
|
+
parseTelegramButtonCompactActionPayload,
|
|
571
|
+
{ maxCompactAtoms: 4 },
|
|
572
|
+
)?.rows;
|
|
573
|
+
}
|
|
574
|
+
|
|
512
575
|
export function parseTelegramActionPayloadRows(
|
|
513
576
|
comment: TelegramTopLevelHtmlComment,
|
|
514
577
|
command: string,
|
|
@@ -533,7 +596,7 @@ export function parseTelegramActionPayloadRows(
|
|
|
533
596
|
const parsed = parseTelegramAdaptiveActionPayloadRows(
|
|
534
597
|
content.slice(offset),
|
|
535
598
|
parseTelegramButtonCompactActionPayload,
|
|
536
|
-
{ allowTrailing: true },
|
|
599
|
+
{ allowTrailing: true, maxCompactAtoms: 4 },
|
|
537
600
|
);
|
|
538
601
|
if (parsed) return parsed.rows;
|
|
539
602
|
const end = findTelegramStructuredPayloadEnd(content, offset);
|
|
@@ -546,8 +609,15 @@ export function parseTelegramActionPayloadRows(
|
|
|
546
609
|
"prompt",
|
|
547
610
|
"value",
|
|
548
611
|
"selected_style",
|
|
612
|
+
"disabled",
|
|
549
613
|
]);
|
|
550
|
-
|
|
614
|
+
if (!attributes) return undefined;
|
|
615
|
+
return [[{
|
|
616
|
+
...attributes,
|
|
617
|
+
...(attributes.disabled === "true" || attributes.disabled === "false"
|
|
618
|
+
? { disabled: attributes.disabled === "true" }
|
|
619
|
+
: {}),
|
|
620
|
+
}]];
|
|
551
621
|
}
|
|
552
622
|
|
|
553
623
|
export function normalizeMarkdownAfterVoiceExtraction(
|
|
@@ -589,7 +659,9 @@ function stripTelegramHtmlCommentBlocks(markdown: string): string {
|
|
|
589
659
|
}
|
|
590
660
|
|
|
591
661
|
export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
592
|
-
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(
|
|
662
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(
|
|
663
|
+
replaceTelegramButtonFences(markdown, () => ""),
|
|
664
|
+
);
|
|
593
665
|
const openBlockIndex =
|
|
594
666
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
595
667
|
const previewMarkdown =
|
|
@@ -835,6 +835,7 @@ export {
|
|
|
835
835
|
|
|
836
836
|
export function createTelegramOutboundReplyPlanner(
|
|
837
837
|
store: Pick<TelegramButtonActionStore, "register">,
|
|
838
|
+
getRenderingMode: () => "rich" | "html" = () => "rich",
|
|
838
839
|
): (
|
|
839
840
|
markdown: string,
|
|
840
841
|
options?: { binding?: TelegramOutboundButtonBinding },
|
|
@@ -842,6 +843,7 @@ export function createTelegramOutboundReplyPlanner(
|
|
|
842
843
|
return (markdown, options) => {
|
|
843
844
|
const buttonReply = planTelegramButtonReply(markdown, {
|
|
844
845
|
registerAction: store.register,
|
|
846
|
+
rendering: getRenderingMode(),
|
|
845
847
|
...(options?.binding ? { binding: options.binding } : {}),
|
|
846
848
|
});
|
|
847
849
|
|
|
@@ -532,7 +532,7 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
|
532
532
|
function countTelegramNativeMarkdownBlocks(block: string): number {
|
|
533
533
|
if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
|
|
534
534
|
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
535
|
-
if (lines.some((line) => /^\s*([-*+] |\d+\.
|
|
535
|
+
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))) {
|
|
536
536
|
return Math.max(1, lines.length);
|
|
537
537
|
}
|
|
538
538
|
return 1;
|
|
@@ -40,7 +40,9 @@ A surface is an ordered ragged sequence of rows. Each button carries:
|
|
|
40
40
|
|
|
41
41
|
- A short, distinct label.
|
|
42
42
|
- The smallest self-contained next-request prompt.
|
|
43
|
-
- Optional presentation state supported by the transport.
|
|
43
|
+
- Optional presentation state supported by the transport, including disabled controls when their visible unavailability helps explain current state.
|
|
44
|
+
|
|
45
|
+
A disabled control is not an action: it needs no prompt or selected style and must not enqueue a prompt or invoke a bound method. Prefer a meaningful label; omit it only for an intentional blank cell in a spatial layout, never as decorative padding. Preserve its label and position when that makes a changing surface easier to understand; otherwise omit irrelevant controls. Explain non-obvious unavailability without relying on color alone. Retain at least one useful enabled action, such as refresh or navigation. Derive disabled state from the same evidence as the view; an old enabled control still requires current domain validation. Use the transport owner's disabled encoding rather than a dummy prompt or no-op callback.
|
|
44
46
|
|
|
45
47
|
Prompts must name any target, operation, constraint, or freshness identity whose omission could change the action. Reuse visible context only when it remains unambiguous under delayed or reordered clicks. Never encode volatile output that should be freshly inspected.
|
|
46
48
|
|
|
@@ -57,7 +59,7 @@ Every generated human-readable action label must use `emoji + space + text`; emo
|
|
|
57
59
|
|
|
58
60
|
For complex grids, navigation collections, or stateful repeated clicks, read [`references/layout-and-state.md`](./references/layout-and-state.md).
|
|
59
61
|
|
|
60
|
-
Serialize the resulting rows with the active transport contract. This Skill owns admission and composition, not transport syntax.
|
|
62
|
+
Place a control group beside the section it governs when the transport supports in-body blocks; use a footer for whole-answer actions. Placement must not change the matrix or action semantics. Serialize the resulting rows with the active transport contract. This Skill owns admission and composition, not transport syntax.
|
|
61
63
|
|
|
62
64
|
## Safety
|
|
63
65
|
|
|
@@ -10,7 +10,7 @@ Model the surface as ordered ragged rows, not a rectangle to fill. Infer indepen
|
|
|
10
10
|
- A horizontal pair is earned only by genuine peers with unmistakably compact labels and no plausible wrapping or truncation.
|
|
11
11
|
- Three through five columns are for short symbols, coordinates, glyphs, or codes whose position carries meaning.
|
|
12
12
|
- Six through eight columns require single-glyph or similarly minimal position-bearing labels. Never exceed eight columns on a phone surface.
|
|
13
|
-
- Vary row width intentionally; never pad with empty, duplicate, or no-op controls.
|
|
13
|
+
- Vary row width intentionally; never pad for symmetry with empty, duplicate, or no-op controls. A blank disabled cell is appropriate only when it represents a real unavailable position in a spatial grid.
|
|
14
14
|
- Preserve reading order: orientation/navigation, primary content or choices, secondary controls, then separated destructive actions.
|
|
15
15
|
- Rectangular grids require genuine spatial or coordinate correspondence. Vertical continuity may justify many rows; non-spatial button walls should paginate or group.
|
|
16
16
|
|
|
@@ -32,4 +32,6 @@ Keep trivial state in conversation. Persist a small human-auditable artifact whe
|
|
|
32
32
|
|
|
33
33
|
Evaluate repeated clicks against current state, not stale button appearance. Preserve tap-ahead when the transport queues each click independently. In source-then-destination interaction, retain the source selection without duplicating the whole surface; regenerate after a completed transition, invalid input, or evidence that the transport cannot preserve the intermediate view.
|
|
34
34
|
|
|
35
|
-
Omit unavailable controls when layout does not matter. Preserve occupied or selected cells when spatial topology depends on stable coordinates.
|
|
35
|
+
Omit unavailable controls when layout does not matter. Preserve occupied or selected cells when spatial topology depends on stable coordinates, using the transport's disabled state without a fabricated prompt. Keep a useful enabled navigation or inspection action.
|
|
36
|
+
|
|
37
|
+
Place each control group beside the content it governs when in-body blocks are supported; keep global navigation and whole-view actions in the footer. Do not duplicate one action in both positions merely for visibility. Row topology and current-state validation stay the same across compact and named representations and across placements; renderer limits and selection feedback belong to the transport.
|
|
@@ -29,9 +29,9 @@ Generated Control Surface → current context → model → one ephemeral surfac
|
|
|
29
29
|
Generative App → model → reusable program → many evolving surfaces
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Both Skills use the same logical button matrix and `label + prompt` interaction model. The Telegram runtime owns its full JSON/CML notation and callback routing; this Skill owns reusable program judgment, while `generated-control-surface` owns ephemeral agent-authored composition. Shared rendering needs no third button Skill and does not collapse those responsibilities.
|
|
32
|
+
Both Skills use the same logical button matrix and `label + prompt` interaction model. An app may place groups beside their related content using the transport's in-body button blocks or keep whole-view controls in the footer; placement does not change binding, disabled state, or method authority. The Telegram runtime owns its full JSON/CML notation and callback routing; this Skill owns reusable program judgment, while `generated-control-surface` owns ephemeral agent-authored composition. Shared rendering needs no third button Skill and does not collapse those responsibilities.
|
|
33
33
|
|
|
34
|
-
An app may mix deterministic `app::method` controls and ordinary prompts in one view. Compile only the stable transitions that benefit from inference bypass; keep explanation, interpretation, teaching, and adaptation on the model-mediated plane. When no reusable state or deterministic loop earns a script, load and use `generated-control-surface` instead.
|
|
34
|
+
An app may mix deterministic `app::method` controls and ordinary prompts in one view, including one control group. Compact and named cells may coexist under the shared transport grammar; neither representation nor placement creates another action plane. Compile only the stable transitions that benefit from inference bypass; keep explanation, interpretation, teaching, and adaptation on the model-mediated plane. When no reusable state or deterministic loop earns a script, load and use `generated-control-surface` instead.
|
|
35
35
|
|
|
36
36
|
The `generated` / `generative` distinction is intentional. Do not rename `generated-control-surface` to a competing generative term.
|
|
37
37
|
|
|
@@ -76,7 +76,7 @@ Another capability remains the authoritative real owner. The app stores validate
|
|
|
76
76
|
1. Identify the repeated feedback loop, real state owner, and actions that are truly deterministic.
|
|
77
77
|
2. Choose one stable lowercase app and one self-contained `<app>.mjs` source outside the managed installation directory.
|
|
78
78
|
3. Keep `init` and every exported method small, named, bounded, shell-free, and capability-specific.
|
|
79
|
-
4. Render one complete next view after each action.
|
|
79
|
+
4. Render one complete next view after each action. Derive disabled controls from current state using the shared transport contract; visible disabled controls neither invoke methods nor enqueue prompts. Revalidate domain preconditions when an enabled control is invoked, because previously rendered views may be stale.
|
|
80
80
|
5. Mix action planes intentionally:
|
|
81
81
|
|
|
82
82
|
```text
|
|
@@ -107,6 +107,7 @@ Before presenting an app as working:
|
|
|
107
107
|
- Inspect the installed initial view and persisted bounded state.
|
|
108
108
|
- Exercise at least one real bound action and prove it bypasses Pi queue/model admission.
|
|
109
109
|
- Exercise at least one ordinary prompt when the app intentionally uses the model plane.
|
|
110
|
+
- Verify the placements actually used by the app and prove disabled cells invoke nothing. A successful ordinary prompt-button smoke is not evidence of app-method dispatch or stale-revision rejection.
|
|
110
111
|
- Verify replacement rejects stale buttons and failed initialization preserves the prior app.
|
|
111
112
|
- For adapters, prove fresh external status and terminal mutation evidence.
|
|
112
113
|
- Confirm failures are bounded, redacted, and do not silently render success.
|
|
@@ -15,7 +15,8 @@ Use Telegram as a mobile companion to the current Pi session. Preserve the exact
|
|
|
15
15
|
| Attach a requested file to the current turn | `telegram_attach(path)` without targeting |
|
|
16
16
|
| Explicitly send from local/TUI to Telegram | `telegram_message` or `telegram_attach` |
|
|
17
17
|
| Explicitly send to a different live Thread | `telegram_message(thread=...)` |
|
|
18
|
-
| Add
|
|
18
|
+
| Add buttons | Hidden comment for footer; `telegram_button` fence for in-body rows |
|
|
19
|
+
| Add explicit voice | Top-level hidden action comment |
|
|
19
20
|
| Build a repeated deterministic interaction | Follow `generative-apps` |
|
|
20
21
|
|
|
21
22
|
A connected Telegram session proves capability, not user intent. Use Telegram features on Telegram-originated turns or explicit Telegram delivery requests only. Never call `telegram_message` for the current active target.
|
|
@@ -39,7 +40,7 @@ Reply in concise, phone-width Telegram Rich Markdown. Use `$...$` and `$$...$$`
|
|
|
39
40
|
|
|
40
41
|
## Assistant Actions
|
|
41
42
|
|
|
42
|
-
`telegram_button` and `telegram_voice` are
|
|
43
|
+
`telegram_button` and `telegram_voice` are markup, not tools. Emit action comments at column zero outside lists, quotes, code, and indentation. Comments create footer buttons or voice artifacts. For buttons between paragraphs, use a column-zero triple-backtick `telegram_button` block. Both button wrappers accept the same singleton JSON/CML cell or mixed matrix; the wrapper determines placement. Telegram removes every assistant-authored HTML comment from previews and final replies regardless of owner or Markdown position; only recognized top-level wrappers activate actions; comment-only output sends no text message.
|
|
43
44
|
|
|
44
45
|
### Shared Encoding Rule
|
|
45
46
|
|
|
@@ -48,25 +49,35 @@ Choose the least verbose sufficient representation:
|
|
|
48
49
|
1. Positional CML — default.
|
|
49
50
|
2. JSON — only when multiline content, named fields, or escaping earns it.
|
|
50
51
|
|
|
51
|
-
CML trims atom boundaries and decodes `\|`, `\}`, and `\\`. Keep
|
|
52
|
+
CML trims atom boundaries and decodes `\|`, `\}`, and `\\`. Keep each payload inside one complete wrapper.
|
|
52
53
|
|
|
53
54
|
### Prompt Buttons
|
|
54
55
|
|
|
55
|
-
Every button has a self-contained prompt and an optional selection style. Use a short distinct `emoji + space + text` label when separate human-readable labeling adds meaning; established coordinates or symbolic tokens may use the prompt itself as visible text. A click creates an ordinary user request; it never grants authority or bypasses confirmation.
|
|
56
|
+
Every enabled button has a self-contained prompt and an optional selection style. Use a short distinct `emoji + space + text` label when separate human-readable labeling adds meaning; established coordinates or symbolic tokens may use the prompt itself as visible text. A click creates an ordinary user request; it never grants authority or bypasses confirmation.
|
|
56
57
|
|
|
57
58
|
- `{prompt}` uses the same text for label and prompt.
|
|
58
59
|
- `{|prompt}` omits a separately authored label and uses the prompt as both visible text and queued prompt.
|
|
59
60
|
- `{label|prompt}` separates visible label from queued prompt.
|
|
60
61
|
- `{label|prompt|selected_style}` and `{|prompt|selected_style}` accept `primary`, `success`, or `danger`.
|
|
62
|
+
- Fourth-position `1`/`true` disables, `0`/`false` enables; omission means enabled. JSON uses boolean `disabled`. `{|Next||1}` omits label/style; `{Next|||1}` omits prompt/style; `{|||1}` is blank (Telegram receives a non-breaking space). Prefer meaningful labels and retain a useful enabled action. Enabled CML requires a prompt. Disabled controls stay visible but have no callback, queued prompt, or bound-method invocation.
|
|
61
63
|
- Top-level cells form vertical rows; one nested row groups horizontal peers.
|
|
62
|
-
- Prefer one matrix
|
|
64
|
+
- Prefer one matrix per related group. Fenced blocks stay in place in Rich mode; HTML compatibility moves them to the footer. Native rows allow eight buttons. Malformed/oversized/unclosed blocks activate nothing; drafts hide them. Outer code fences and quoted/indented examples remain literal.
|
|
65
|
+
- Both placements share prompt/app routing. In-body clicks acknowledge without recoloring; selected-style highlighting remains footer-only.
|
|
66
|
+
|
|
67
|
+
A single in-body button (the four-backtick wrapper below makes this a literal example):
|
|
68
|
+
|
|
69
|
+
````markdown
|
|
70
|
+
```telegram_button
|
|
71
|
+
{📖 Details|Explain this section.}
|
|
72
|
+
```
|
|
73
|
+
````
|
|
63
74
|
|
|
64
75
|
```html
|
|
65
76
|
<!-- telegram_button [{▶️ Continue|Continue the current plan.}[{✅ Approve|Approve this.}{❌ Reject|Reject this.}]] -->
|
|
66
77
|
<!-- telegram_button {"label":"💡 Explain","prompt":"Explain this.\nInclude the risks."} -->
|
|
67
78
|
```
|
|
68
79
|
|
|
69
|
-
Proactively use `generated-control-surface` whenever controls can materially shorten likely feedback; once active, it must emit useful buttons rather than prose alone. That Skill owns action composition; this Skill owns Telegram serialization and delivery.
|
|
80
|
+
Proactively use `generated-control-surface` whenever controls can materially shorten likely feedback; once active, it must emit useful buttons rather than prose alone. That Skill owns action composition; this Skill owns Telegram serialization and delivery. Footer-only replies receive the standard choice heading.
|
|
70
81
|
|
|
71
82
|
### Voice
|
|
72
83
|
|
|
@@ -113,6 +124,6 @@ Before replying:
|
|
|
113
124
|
|
|
114
125
|
- Use the ordinary path for the current target and direct tools only for explicit other delivery.
|
|
115
126
|
- Attach requested files rather than only mentioning them.
|
|
116
|
-
- Keep action comments top-level, complete, and canonical: CML first, JSON when necessary.
|
|
117
|
-
- Give every button a self-contained prompt; preserve confirmation for dangerous actions.
|
|
127
|
+
- Keep action comments and button fences top-level, complete, and canonical: CML first, JSON when necessary; either syntax may coexist within one matrix.
|
|
128
|
+
- Give every enabled button a self-contained prompt; preserve confirmation for dangerous actions.
|
|
118
129
|
- Expose no secret or hidden reasoning.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@llblab/pi-codex-usage": "0.9.4",
|
|
46
46
|
"@llblab/pi-grow-loop": "0.7.4",
|
|
47
47
|
"@llblab/pi-state-flow": "0.3.0",
|
|
48
|
-
"@llblab/pi-telegram": "0.
|
|
48
|
+
"@llblab/pi-telegram": "0.43.0",
|
|
49
49
|
"@llblab/skills": "1.14.0"
|
|
50
50
|
},
|
|
51
51
|
"bundledDependencies": [
|