@llblab/pi-telegram 0.34.1 → 0.35.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 CHANGED
@@ -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.35.0: Compact Matrix Literal
6
+
7
+ - `Compact Wire Format`: Adds portable Compact Matrix Literal alongside JSON and attributes: `{value}` copies label to prompt, `{label|prompt}` separates them, top-level cells remain full-width, nested rows preserve horizontal grouping, and atom boundaries trim automatically.
8
+ - `Literal Fidelity`: Preserves non-structural printable Unicode—including brackets, quotes, commas, colons, paths, and spaces—while decoding only `\|`, `\}`, and `\\`; unknown/trailing escapes, empty values, repeated separators, controls, malformed delimiters, empty rows, and deeper nesting fail closed before callback registration.
9
+ - `Renderer-Owned Width`: Removes the artificial parser-level row-width cap because Telegram documents none, while bundled Skills keep five columns as the proven default, admit six through eight only for very short position-bearing labels, and direct wider surfaces toward regrouping. A dedicated portable standard owns grammar, JSON-first routing, conformance, and versioning.
10
+
5
11
  ## 0.34.1: Compact Stateful Control Surfaces
6
12
 
7
13
  - `Prompt Compression`: Teaches Generated Control Surface to encode the smallest sufficient action delta—such as a coordinate, symbol, identifier, or short verb—when visible conversation establishes unambiguous state, while retaining stable identity when delivery or routing could separate action from context.
package/README.md CHANGED
@@ -192,7 +192,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
192
192
 
193
193
  ### Buttons And Callbacks
194
194
 
195
- Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, or compact double-quoted attributes; `telegram_buttons` is a plural alias. In a matrix, top-level objects become full-width rows while nested arrays group one to five buttons horizontally. Prefer one array comment for multiple buttons. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders 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.
195
+ Assistant replies can include top-level hidden `telegram_button` comments using a JSON object, JSON matrix, Compact Matrix Literal (CML), or compact double-quoted attributes; `telegram_buttons` is a plural alias. In JSON and CML matrices, 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}` or `{label|prompt}`, 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 action marker is colon-free for every payload form. The bridge strips the comments from visible text, renders 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.
196
196
 
197
197
  ### Threaded Mode And Multi-Instance Bus
198
198
 
package/docs/README.md CHANGED
@@ -15,6 +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) — Portable Compact Matrix Literal v1 standard for trimmed key-value cells, minimal escapes, bounded-depth rows, JSON coexistence, and renderer-owned width policy
18
19
  - [callback-namespaces.md](./callback-namespaces.md) — Shared Telegram `callback_data` namespace standard for layered extensions
19
20
  - [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
20
21
  - [multi-instance-bus.md](./multi-instance-bus.md) — Optional multi-instance Telegram bus architecture: profile-scoped transport, leader/follower routing, thread targets, instance slots, manual follower registration, and recovery semantics
@@ -327,7 +327,7 @@ Outbound files staged during an active Telegram turn are delivered after that tu
327
327
  Assistant-authored final-message actions use hidden top-level comments with colon-free markers; colon-prefixed payloads are rejected:
328
328
 
329
329
  - `telegram_voice` accepts a JSON object or double-quoted HTML-like attributes and creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
330
- - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix object creates one full-width row, while a nested array groups one to five buttons horizontally; every callback enqueues its configured prompt text as a normal Telegram prompt turn.
330
+ - `telegram_button` accepts a JSON object, JSON matrix, Compact Matrix Literal, or double-quoted attributes; `telegram_buttons` aliases the same parser. Each top-level matrix cell creates one full-width row, while a nested row groups one or more buttons horizontally without an artificial parser-width cap; every callback enqueues its configured prompt text as a normal Telegram prompt turn. CML's portable grammar, trim/escape rules, JSON-first routing, and renderer-owned width policy are specified in [Compact Matrix Literal](./compact-matrix-literal.md).
331
331
 
332
332
  Preview delivery strips top-level action comments before streaming draft Markdown. Comments inside code fences, quotes, lists, or indented examples stay literal.
333
333
 
@@ -0,0 +1,189 @@
1
+ # Compact Matrix Literal
2
+
3
+ > Status: Portable v1 standard implemented by the `pi-telegram` 0.35.0 release candidate.
4
+
5
+ Compact Matrix Literal (CML) is a bounded-depth text format for ordered key-value cells arranged as singleton or compact rows. It optimizes repeated interactive controls where JSON field names, quotes, and commas dominate the payload.
6
+
7
+ CML is transport-neutral. An embedding maps each decoded cell's `key` and `value` to its own domain. The `pi-telegram` profile maps `key` to button label and `value` to button prompt.
8
+
9
+ ## Goals
10
+
11
+ - Encode common key-value matrices with minimal punctuation.
12
+ - Preserve ordered singleton and compact rows.
13
+ - Preserve non-structural Unicode text literally.
14
+ - Admit deterministic linear-time parsing without evaluation or recovery.
15
+ - Remain deterministic beside an existing JSON form.
16
+ - Fail closed on malformed or deeper structures.
17
+
18
+ ## Non-Goals
19
+
20
+ - Replacing JSON for arbitrary objects, metadata, styles, or extensible schemas.
21
+ - Defining callback ownership, application state, rendering policy, or transport behavior.
22
+ - Recovering partial intent from malformed input.
23
+ - Defining one universal visual row-width limit for every renderer.
24
+
25
+ ## Data Model
26
+
27
+ A decoded payload is an ordered non-empty list of non-empty rows:
28
+
29
+ ```text
30
+ Cell = { key: string, value: string }
31
+ Rows = Cell[][]
32
+ ```
33
+
34
+ A top-level cell normalizes to a singleton row. A nested row preserves its compact grouping.
35
+
36
+ A cell with one atom copies its key into its value:
37
+
38
+ ```text
39
+ {7} == { key: "7", value: "7" }
40
+ ```
41
+
42
+ A cell with two atoms separates key and value with one unescaped vertical bar:
43
+
44
+ ```text
45
+ {🟥|2,5} == { key: "🟥", value: "2,5" }
46
+ ```
47
+
48
+ ## Grammar
49
+
50
+ The normative structural grammar is:
51
+
52
+ ```text
53
+ payload := cell | matrix
54
+ matrix := "[" ws element (ws element)* ws "]"
55
+ element := cell | row
56
+ row := "[" ws cell (ws cell)* ws "]"
57
+ cell := "{" atom "}"
58
+ | "{" atom "|" atom "}"
59
+ atom := atom-unit+
60
+ atom-unit := ordinary | "\\|" | "\\}" | "\\\\"
61
+ ws := *(SP | HTAB | CR | LF)
62
+ ```
63
+
64
+ `ordinary` is any printable Unicode scalar other than unescaped `|`, unescaped `}`, or `\`. No commas separate elements. A matrix and every nested row must contain at least one element. A row cannot contain another row.
65
+
66
+ Examples:
67
+
68
+ ```text
69
+ {Continue}
70
+ {Open|/tmp}
71
+ [{Up|/}[{Prev|page-1}{Next|page-3}]{etc|/etc}]
72
+ [[{1}{2}{3}{4}{5}{6}{7}{8}]]
73
+ {A \| B|C:\\Games\}}
74
+ ```
75
+
76
+ These normalize respectively to one copied singleton cell, one key-value singleton cell, a mixed singleton/compact matrix, one eight-cell row, and `{ key: "A | B", value: "C:\\Games}" }`.
77
+
78
+ ## Atoms, Whitespace, And Escapes
79
+
80
+ Leading and trailing whitespace in each decoded key and value is trimmed. Internal ordinary spaces are preserved. CR, LF, HTAB, C0 controls, DEL, and C1 controls that remain inside an atom after trimming are invalid.
81
+
82
+ Only three escape sequences exist:
83
+
84
+ ```text
85
+ \| → literal |
86
+ \} → literal }
87
+ \\ → literal \
88
+ ```
89
+
90
+ Unknown escapes and a trailing backslash are invalid. No character is silently dropped.
91
+
92
+ Every other printable character is literal inside a cell, including:
93
+
94
+ ```text
95
+ { [ ] " : , / emoji and ordinary spaces
96
+ ```
97
+
98
+ An opening `{` has no structural meaning after a cell has begun. Square brackets are structural only outside a cell. A second unescaped vertical bar is invalid. When multiline text or additional metadata is needed, the producer uses the embedding's JSON or other full-fidelity form.
99
+
100
+ ## Width Policy
101
+
102
+ CML Core does not impose a visual row-width maximum. Width is a renderer and interaction-policy concern, not a property of the key-value matrix wire format.
103
+
104
+ An embedding may enforce a documented host limit. The `pi-telegram` parser does not add an artificial per-row width cap because Telegram Bot API does not document one and existing top-level matrices already admit host-bounded action counts. Its bundled Generated Control Surface Skill owns UX policy: five columns are the proven default for short position-bearing labels, six to eight may be used only when labels remain compact and readable, and wider surfaces should normally be regrouped.
105
+
106
+ ## Parsing Contract
107
+
108
+ A conforming parser:
109
+
110
+ 1. Consumes Unicode text without executing, interpolating, or evaluating it.
111
+ 2. Parses exactly one `payload` and rejects trailing non-whitespace input.
112
+ 3. Rejects empty atoms, empty matrices, empty rows, and nesting deeper than one row inside the top-level matrix.
113
+ 4. Rejects missing, extra, crossed, or mismatched delimiters.
114
+ 5. Rejects a second unescaped vertical bar in a cell.
115
+ 6. Decodes only `\|`, `\}`, and `\\`; unknown or trailing escapes fail.
116
+ 7. Trims atom boundaries, then rejects empty values and remaining control characters.
117
+ 8. Returns no partial rows or cells after any failure.
118
+ 9. Runs in linear time over a host-bounded payload and does not recurse beyond the fixed grammar depth.
119
+
120
+ Implementations may report diagnostics internally, but an invalid payload must not register or execute any action.
121
+
122
+ ## JSON Coexistence
123
+
124
+ An embedding that already accepts JSON uses deterministic routing:
125
+
126
+ 1. Attempt strict JSON parsing first for payloads beginning with `{` or `[`.
127
+ 2. If JSON parsing succeeds, validate only against the embedding's JSON schema. A JSON shape failure must not fall back to CML.
128
+ 3. If JSON parsing fails, attempt CML from the original source.
129
+ 4. Accept CML only after complete grammar and embedding validation.
130
+
131
+ Valid JSON behavior therefore remains unchanged. A malformed JSON-looking source receives no tolerant recovery: it is accepted only when it independently forms a complete valid CML payload.
132
+
133
+ ## `pi-telegram` Profile
134
+
135
+ For `telegram_button` and its exact `telegram_buttons` alias:
136
+
137
+ - `Cell.key` becomes the visible button label.
138
+ - `Cell.value` becomes the queued prompt.
139
+ - A top-level cell becomes one full-width inline-keyboard row.
140
+ - A nested row becomes one horizontal row.
141
+ - `{value}` is equivalent to JSON `{"value":"value"}`.
142
+ - `{label|prompt}` is equivalent to JSON `{"label":"label","prompt":"prompt"}`.
143
+ - JSON and double-quoted attributes remain the full-fidelity forms.
144
+ - `selected_style` and future metadata are not represented by CML v1.
145
+ - Invalid CML is stripped with its enclosing recognized action comment and registers no callbacks, matching existing fail-closed action behavior.
146
+
147
+ Example embedding:
148
+
149
+ ```html
150
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
151
+ ```
152
+
153
+ The enclosing HTML-comment transport still owns its own delimiter boundary; content containing the comment terminator cannot reach the CML parser and must use another supported delivery representation.
154
+
155
+ ## Conformance Classes
156
+
157
+ A conformance suite covers properties rather than incident-specific strings.
158
+
159
+ ### Accepted
160
+
161
+ - Singular copied and key-value cells.
162
+ - Top-level singleton rows.
163
+ - Nested rows at widths one, five, and eight.
164
+ - Mixed singleton and compact rows.
165
+ - Unicode, punctuation, brackets, quotes, commas, colons, and internal spaces.
166
+ - Trimmed atom boundaries.
167
+ - Each defined escape sequence.
168
+ - Structural whitespace between tokens.
169
+ - Semantic equivalence with supported JSON cell forms.
170
+
171
+ ### Rejected
172
+
173
+ - Empty payload, matrix, row, key, or value.
174
+ - Deeper nesting.
175
+ - Missing or mismatched delimiters.
176
+ - A second unescaped separator.
177
+ - Unknown or trailing escapes.
178
+ - Internal control characters.
179
+ - Commas between cells.
180
+ - Trailing garbage.
181
+ - JSON that parses but fails the JSON action schema.
182
+
183
+ Every rejected case proves zero callback registration.
184
+
185
+ ## Versioning
186
+
187
+ This document defines CML v1. Compatible embeddings may impose documented host-level byte, cell-count, or width limits without changing the core grammar, but must preserve bounded-depth and fail-closed semantics.
188
+
189
+ Future versions must not assign new meaning to input rejected by a security or ownership boundary without an explicit version discriminator. Metadata fields, styles, and deeper structures require a revised standard rather than permissive v1 parsing.
package/docs/outbound.md CHANGED
@@ -158,15 +158,17 @@ I can continue.
158
158
  <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
159
159
 
160
160
  <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
161
+
162
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
161
163
  ```
162
164
 
163
165
  Rules:
164
166
 
165
- - `telegram_button` accepts a JSON object, JSON matrix, or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
167
+ - `telegram_button` accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted HTML-like attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims both atoms, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`. Shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
166
168
  - A colon after either button marker is rejected so every payload form shares one unambiguous action marker.
167
169
  - Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
168
170
  - The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
169
- - Prefer one JSON array comment for multiple buttons. Each top-level object becomes one full-width inline-keyboard row in source order; a nested array groups one to five button objects into one horizontal row. Empty rows, rows above five buttons, and deeper nesting are rejected. Repeated singular comments remain valid.
171
+ - 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 second unescaped CML separator, and deeper nesting are rejected atomically. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
170
172
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
171
173
  - After Telegram accepts a generated 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.
172
174
  - 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.
@@ -65,7 +65,7 @@ This command surface is a mobile companion subset, not a raw terminal-command br
65
65
  - `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.
66
66
  - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach` 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.
67
67
  - `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
68
- - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix objects become full-width rows, while nested arrays group one to five buttons horizontally. Prefer one array comment for multiple buttons. Use `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 comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
68
+ - `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. One marker accepts a JSON object, JSON matrix, [Compact Matrix Literal](./compact-matrix-literal.md), or double-quoted attributes; `telegram_buttons` is an exact plural alias. Top-level matrix cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}` or `{label|prompt}`, 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 comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
69
69
 
70
70
  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.
71
71
 
@@ -205,6 +205,122 @@ export function parseTelegramActionPayload(
205
205
  return parseCanonicalTelegramActionAttributes(payload.source);
206
206
  }
207
207
 
208
+ const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
209
+
210
+ function parseTelegramCompactActionPayloadRows(
211
+ source: string,
212
+ ): Record<string, unknown>[][] | undefined {
213
+ let offset = 0;
214
+ const isStructuralWhitespace = (character: string | undefined): boolean =>
215
+ character === " " ||
216
+ character === "\t" ||
217
+ character === "\r" ||
218
+ character === "\n";
219
+ const skipWhitespace = (): void => {
220
+ while (isStructuralWhitespace(source[offset])) offset += 1;
221
+ };
222
+ const normalizeAtom = (value: string): string | undefined => {
223
+ const normalized = value.trim();
224
+ return normalized && !TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN.test(normalized)
225
+ ? normalized
226
+ : undefined;
227
+ };
228
+ const parseCell = (): Record<string, unknown> | undefined => {
229
+ if (source[offset] !== "{") return undefined;
230
+ offset += 1;
231
+ const keySource: string[] = [];
232
+ const valueSource: string[] = [];
233
+ let hasSeparator = false;
234
+ while (offset < source.length) {
235
+ const character = source[offset]!;
236
+ if (character === "\\") {
237
+ const escaped = source[offset + 1];
238
+ if (escaped !== "|" && escaped !== "}" && escaped !== "\\") {
239
+ return undefined;
240
+ }
241
+ if (hasSeparator) valueSource.push(escaped);
242
+ else keySource.push(escaped);
243
+ offset += 2;
244
+ continue;
245
+ }
246
+ if (character === "|") {
247
+ if (hasSeparator) return undefined;
248
+ hasSeparator = true;
249
+ offset += 1;
250
+ continue;
251
+ }
252
+ if (character === "}") {
253
+ offset += 1;
254
+ const key = normalizeAtom(keySource.join(""));
255
+ if (!key) return undefined;
256
+ if (!hasSeparator) return { value: key };
257
+ const value = normalizeAtom(valueSource.join(""));
258
+ return value ? { label: key, prompt: value } : undefined;
259
+ }
260
+ if (hasSeparator) valueSource.push(character);
261
+ else keySource.push(character);
262
+ offset += 1;
263
+ }
264
+ return undefined;
265
+ };
266
+ const parseRow = (): Record<string, unknown>[] | undefined => {
267
+ if (source[offset] !== "[") return undefined;
268
+ offset += 1;
269
+ const row: Record<string, unknown>[] = [];
270
+ while (offset < source.length) {
271
+ skipWhitespace();
272
+ if (source[offset] === "]") {
273
+ offset += 1;
274
+ return row.length > 0 ? row : undefined;
275
+ }
276
+ if (source[offset] !== "{") return undefined;
277
+ const cell = parseCell();
278
+ if (!cell) return undefined;
279
+ row.push(cell);
280
+ }
281
+ return undefined;
282
+ };
283
+ const parseMatrix = (): Record<string, unknown>[][] | undefined => {
284
+ if (source[offset] !== "[") return undefined;
285
+ offset += 1;
286
+ const rows: Record<string, unknown>[][] = [];
287
+ while (offset < source.length) {
288
+ skipWhitespace();
289
+ const character = source[offset];
290
+ if (character === "]") {
291
+ offset += 1;
292
+ return rows.length > 0 ? rows : undefined;
293
+ }
294
+ if (character === "{") {
295
+ const cell = parseCell();
296
+ if (!cell) return undefined;
297
+ rows.push([cell]);
298
+ continue;
299
+ }
300
+ if (character === "[") {
301
+ const row = parseRow();
302
+ if (!row) return undefined;
303
+ rows.push(row);
304
+ continue;
305
+ }
306
+ return undefined;
307
+ }
308
+ return undefined;
309
+ };
310
+
311
+ skipWhitespace();
312
+ let rows: Record<string, unknown>[][] | undefined;
313
+ if (source[offset] === "{") {
314
+ const cell = parseCell();
315
+ rows = cell ? [[cell]] : undefined;
316
+ } else {
317
+ rows = parseMatrix();
318
+ }
319
+ if (!rows) return undefined;
320
+ skipWhitespace();
321
+ return offset === source.length ? rows : undefined;
322
+ }
323
+
208
324
  export function parseTelegramActionPayloadRows(
209
325
  comment: TelegramTopLevelHtmlComment,
210
326
  command: string,
@@ -225,7 +341,6 @@ export function parseTelegramActionPayloadRows(
225
341
  if (
226
342
  !Array.isArray(entry) ||
227
343
  entry.length === 0 ||
228
- entry.length > 5 ||
229
344
  !entry.every(isTelegramActionPayload)
230
345
  ) {
231
346
  return undefined;
@@ -234,7 +349,7 @@ export function parseTelegramActionPayloadRows(
234
349
  }
235
350
  return rows;
236
351
  } catch {
237
- return undefined;
352
+ return parseTelegramCompactActionPayloadRows(payload.source);
238
353
  }
239
354
  }
240
355
  if (payload.hasBody) return undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.34.1",
3
+ "version": "0.35.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -118,9 +118,9 @@ Re-check mutable targets immediately before execution. Access denial never autho
118
118
 
119
119
  ## Prompt Buttons
120
120
 
121
- Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, or a JSON matrix. A top-level object becomes one full-width row; a nested array groups one to five objects into one compact horizontal row. Prefer one array comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format. Use compact rows only when the controls form one coherent peer group such as Previous/Next, approval alternatives, or view modes; keep ordinary actions full-width.
121
+ Use the transport's canonical prompt-button syntax. For pi-telegram, one top-level `telegram_button` comment accepts one JSON object, double-quoted attributes, a JSON matrix, or Compact Matrix Literal (CML). CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`; use JSON for multiline prompts, styles, or metadata. A top-level cell becomes one full-width row, while a nested row groups one or more controls horizontally without a parser-level width cap. Prefer one matrix comment for multiple controls instead of repeating the marker; `telegram_buttons` is a plural alias, not a different format. Use compact rows only when the controls form one coherent peer group such as Previous/Next, approval alternatives, or view modes; keep ordinary actions full-width.
122
122
 
123
- Treat the five-wide matrix as an interaction primitive when spatial position carries meaning and acting through the surface communicates better than prose. Five-wide rows are proven usable with short, distinct labels; use fewer columns or full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose. Preserve the ordinary admission test: proactively offer an interactive surface even when the user did not request buttons when it materially reduces effort or demonstrates an available capability, but do not manufacture decorative interaction.
123
+ Treat a matrix as an interaction primitive when spatial position carries meaning and acting through the surface communicates better than prose. Five columns are proven usable with short, distinct labels and remain the default maximum. Six through eight columns may be used only for very short position-bearing labels whose grouping materially improves the interaction; regroup wider surfaces and use fewer columns or full-width rows when labels need explanation, wrap ambiguously, or lose meaning without prose. Preserve the ordinary admission test: proactively offer an interactive surface even when the user did not request buttons when it materially reduces effort or demonstrates an available capability, but do not manufacture decorative interaction.
124
124
 
125
125
  ### Interaction State And Prompt Compression
126
126
 
@@ -132,7 +132,7 @@ Treat the five-wide matrix as an interaction primitive when spatial position car
132
132
 
133
133
  ```html
134
134
  <!-- telegram_button {"label":"🔍 Inspect run","prompt":"Inspect Run run:example read-only, summarize its current status and latest material evidence, then regenerate relevant supervision controls."} -->
135
- <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
135
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️ Previous}{➡️ Next}]{📁 etc|/etc}] -->
136
136
  ```
137
137
 
138
138
  Button prompts must:
@@ -57,9 +57,10 @@ Button forms:
57
57
  <!-- telegram_button value="Continue" -->
58
58
  <!-- telegram_button [{"label":"⬆️ Up","prompt":"/"},[{"value":"⬅️ Previous"},{"value":"➡️ Next"}],{"label":"📁 etc","prompt":"/etc"}] -->
59
59
  <!-- telegram_buttons [[{"value":"Approve"},{"value":"Reject"}]] -->
60
+ <!-- telegram_button [{⬆️ Up|/}[{⬅️|page-1}{➡️|page-3}]{📁 etc|/etc}] -->
60
61
  ```
61
62
 
62
- - `telegram_button` accepts one JSON object, a JSON matrix, or double-quoted attributes; `telegram_buttons` is an exact plural alias. In a matrix, each top-level object becomes a full-width row and each nested array groups one to five buttons into one horizontal row. Prefer one array comment for multiple buttons. Keep the complete action in one top-level comment and encode JSON line breaks as `\n`.
63
+ - `telegram_button` accepts one JSON object, a JSON matrix, Compact Matrix Literal (CML), or double-quoted attributes; `telegram_buttons` is an exact plural alias. CML uses `{value}` or `{label|prompt}`, trims atom boundaries, preserves other printable text literally, and decodes only `\|`, `\}`, and `\\`. In a matrix, each top-level cell becomes a full-width row and each nested row groups one or more buttons horizontally without a parser-level width cap. Prefer one matrix comment for multiple buttons, normally keep generated rows at five columns or fewer, and use six through eight only for short position-bearing labels. Keep the complete action in one top-level comment and encode multiline content with JSON `\n`.
63
64
  - Use `label` plus a self-contained `prompt`, or non-empty `value` when both are identical.
64
65
  - Optional `selected_style` is `primary` (default), `success`, or `danger`; style never suppresses prompt admission.
65
66
  - If button comments form the whole reply, the bridge supplies the standard choice heading.