@llblab/pi-telegram 0.24.7 → 0.24.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/AGENTS.md CHANGED
@@ -125,7 +125,7 @@ The core product loop is mobile continuation: start or supervise work in the ter
125
125
  - Voice reply mode Settings UI standard: the top-level Settings row is `👄 Voice reply: hidden|mirror|always`; `hidden` is the true default, persists no `voice.replyMode`, adds no voice context, and still allows explicit agent-authored `telegram_voice`; `mirror` activates automatic voice context only for voice/audio input, and `always` activates it for every Telegram turn. The submenu title is `👄 Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`🟢 hidden`, `🟢 mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
126
126
  - Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
127
127
  - `telegram_voice` text is arbitrary TTS-target text and the prompt should show only forms that include text to speak: `<!-- telegram_voice text="Short summary" -->` or `<!-- telegram_voice: Short summary -->`. Avoid raw Markdown/code/tables in voice text.
128
- - `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts. The bridge preserves agent-authored label text and emoji, and owns post-selection state through Telegram's green `success` button style without changing that label. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and add visible parent text when buttons would otherwise be the only output
128
+ - `telegram_button` has three canonical forms: `<!-- telegram_button: OK -->` for label-only buttons, `<!-- telegram_button label=Continue prompt="Continue with the current plan." -->` for one-line prompts, or `<!-- telegram_button label="Show risks"\nList the main risks first.\n-->` for multiline prompts. The bridge preserves agent-authored label text and emoji, and owns post-selection state without changing that label: optional `selected_style="primary|success|danger"` selects blue, green, or red after queue admission, with `primary` as the default, and color never suppresses the prompt. Do not author JSON button specs, inline-after-text comments, standalone button tools, or comments inside code/quotes/lists/indented examples; write normal Markdown plus top-level hidden comments, and let the bridge supply the standard `☑️ **Choose an option:**` Rich Markdown heading when buttons are the only output
129
129
 
130
130
  ## 6. Engineering Conventions
131
131
 
package/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.24.9: Button-Only Heading Hotfix
6
+
7
+ - `Prompt Buttons`: The automatic parent text for button-only assistant replies now follows the bridge-owned dialog heading grammar as `☑️ **Choose an option:**`. Impact: fallback button prompts match reasoning, model, and settings choice surfaces with a semantic icon, bold title, and trailing colon instead of appearing as unformatted assistant text.
8
+
9
+ ## 0.24.8: Prompt Button Reliability Hotfix
10
+
11
+ - `Prompt Buttons`: Button-only assistant replies now receive `Choose an option:` as visible fallback text after hidden markup extraction. Accepted choices use Telegram's blue `primary` style by default; agents may set `selected_style` to `success`, `danger`, or `primary` so quiz-like choices become green, red, or blue while preserving their labels and always queueing the selected prompt. Impact: Telegram can reliably create the keyboard message and retain agent-authored visual feedback instead of silently dropping an empty visible reply or forcing every accepted choice to appear green.
12
+ - `Status`: Threaded Mode identity now renders on a dedicated `Thread: <name> @<role>` row instead of appending `@leader` or `@follower` to the execution status. Impact: `Status` remains an unambiguous `active`/`idle` signal while the current Telegram thread and transport role stay visible separately.
13
+
5
14
  ## 0.24.7: Thread Cleanup Hotfix
6
15
 
7
16
  - `Thread Cleanup`: Renamed the Telegram Settings control from `Auto thread cleanup` to `Thread cleanup` while preserving `threads.automaticCleanup`. Graceful leader/follower teardown now persists exact generation-fenced cleanup intent before deletion and lets the current or successor leader retry until Telegram confirms removal. If a follower's graceful envelope never arrives, cleanup runs only after stale heartbeat, enabled policy, OS-confirmed death of the exact registered PID, serialized exclusion of replacement registration, an unpublished follower endpoint during durable replay, and cancellation of intents superseded by a current replacement binding. Impact: normal and missed Ctrl-C exits remove only the intended tab without turning heartbeat loss, live/unknown process state, reload, or IPC uncertainty into deletion authority.
package/README.md CHANGED
@@ -183,7 +183,7 @@ Voice notes, audio, images, PDFs, and other media can pass through configured in
183
183
 
184
184
  ### Buttons And Callbacks
185
185
 
186
- Assistant replies can include top-level hidden `telegram_button` comments. 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. Once a generated prompt button is accepted, only that exact button switches to Telegram's green success style so the chosen prompt remains visible without altering its agent-authored label or emoji.
186
+ Assistant replies can include top-level hidden `telegram_button` comments. 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.
187
187
 
188
188
  ### Threaded Mode And Multi-Instance Bus
189
189
 
package/api/keyboard.ts CHANGED
@@ -6,5 +6,6 @@
6
6
 
7
7
  export type {
8
8
  TelegramInlineKeyboardButton,
9
+ TelegramInlineKeyboardButtonStyle,
9
10
  TelegramInlineKeyboardMarkup,
10
11
  } from "../lib/keyboard.ts";
package/docs/outbound.md CHANGED
@@ -147,7 +147,8 @@ Rules:
147
147
  - Keep the canonical body form as `<!-- telegram_button label="Label"` + body + `-->`; closed heads must use `prompt="..."` or the colon shorthand to create a button.
148
148
  - Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
149
149
  - Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
150
- - After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to Telegram's green `success` style without changing its agent-authored text or emoji. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
150
+ - 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="primary"` (blue), `selected_style="success"` (green), or `selected_style="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.
151
+ - 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.
151
152
 
152
153
  Do not emit JSON button specs, 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)`.
153
154
 
@@ -162,8 +163,8 @@ The extension injects prompt guidance by context:
162
163
  - For Telegram-originated turns, the prompt carries only minimal mobile/reply/file guidance; agents can call `telegram_help()` for full voice/button/direct-delivery/Threaded Mode/formatting/debug details.
163
164
  - For Telegram-originated turns, write the full technical answer as normal Markdown.
164
165
  - Add `telegram_voice` when a Telegram-native voice message is useful; use body text, `text="..."`, or colon shorthand for the text to synthesize. A companion summary is optional, no specific summary format is required.
165
- - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. If the reply contains only button/voice comment blocks, add a short visible marker (for example `Choose one:`) before them so Telegram always has a visible parent message for attachment.
166
+ - Add `telegram_button: ...` when label equals prompt, `telegram_button label="..." prompt="..."` for one-line prompts, or `telegram_button label="..."` with a body for multiline prompts. Use optional `selected_style="success|danger|primary"` for post-admission color. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
166
167
  - 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.
167
- - Never send buttons without visible parent text. If the answer would contain only hidden comments, add a compact line such as `Choose one:` first.
168
+ - 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.
168
169
 
169
170
  This keeps the agent focused on semantics, prevents Telegram action syntax from leaking into normal local replies, and lets the bridge handle low-latency Telegram adaptation.
@@ -336,7 +336,7 @@ Contract:
336
336
  - Providers are synchronous because `/start` status text is rendered inline with the menu.
337
337
  - Return `undefined` when the line is not relevant for the active model.
338
338
  - Provider failures are isolated and skipped so optional companion status cannot break the core Telegram menu.
339
- - The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Usage, Cost, and Context, capitalizing the first label character for Telegram UI consistency.
339
+ - The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, the optional Threaded Mode `Thread: <name> @<role>` row, Usage, Cost, and Context, capitalizing the first label character for Telegram UI consistency.
340
340
 
341
341
  ## Updates
342
342
 
package/docs/ui-style.md CHANGED
@@ -50,7 +50,7 @@ Use emoji as stable semantic markers, not decoration. Emoji carry transportable
50
50
  | `↪️` | Reroute to an existing target | Thread chooser buttons that send a captured command/message from one thread to another live thread | Curved arrow means the message arrived here but bends to another target. |
51
51
  | `🔁` | Replace/restore mode | Thread replace/restore chooser entrypoints | Opens a second step for moving a Pi instance binding to the current source thread. |
52
52
  | `➡️` | Choose replacement target | Thread replace/restore target buttons that select which Pi instance should move to the current thread | Use inside the second replace/restore chooser, not for ordinary reroutes. |
53
- | `☑️` | Activate / choose this item | Model detail activation action | Positive action; use `🟢 Active` for already-current state. |
53
+ | `☑️` | Activate / choose this item | Model detail activation action, generated button-only choice heading | Positive selection cue; use `🟢 Active` for already-current state. |
54
54
  | `❌` | No / cancel | Confirmation cancel buttons | Use for safe cancellation, not destructive removal. |
55
55
  | `🗑` | Delete / remove | Queue delete actions, destructive confirmations, remove reaction | Use only when something is removed/closed/deleted. |
56
56
 
@@ -177,7 +177,7 @@ Examples:
177
177
 
178
178
  ## Generated Prompt Buttons
179
179
 
180
- Assistant-generated prompt buttons use the default app style before selection. After queue admission, edit only the selected button to Telegram's `success` style; preserve its agent-authored text and emoji, and leave other choices at their default style. The callback acknowledgement remains the compatibility fallback when a client does not render button styles.
180
+ A button-only assistant reply uses the standard Rich Markdown heading `☑️ **Choose an option:**`: semantic icon first, one space, bold heading text, and a final colon. Assistant-generated prompt buttons use the default app style before selection. After queue admission, edit only the selected button to its agent-configured `selected_style`: `primary` (default/blue), `success` (green), or `danger` (red). Preserve its agent-authored text and emoji, leave other choices at their default style, and always queue the selected prompt regardless of color. The callback acknowledgement remains the compatibility fallback when a client does not render button styles.
181
181
 
182
182
  ## Navigation
183
183
 
package/lib/keyboard.ts CHANGED
@@ -4,10 +4,15 @@
4
4
  * Owns the shared Bot API reply-markup shape while feature domains own their button semantics
5
5
  */
6
6
 
7
+ export type TelegramInlineKeyboardButtonStyle =
8
+ | "danger"
9
+ | "success"
10
+ | "primary";
11
+
7
12
  export interface TelegramInlineKeyboardButton {
8
13
  text: string;
9
14
  callback_data: string;
10
- style?: "danger" | "success" | "primary";
15
+ style?: TelegramInlineKeyboardButtonStyle;
11
16
  }
12
17
 
13
18
  export interface TelegramInlineKeyboardMarkup {
@@ -6,7 +6,10 @@
6
6
 
7
7
  import { randomUUID } from "node:crypto";
8
8
 
9
- import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
9
+ import type {
10
+ TelegramInlineKeyboardButtonStyle,
11
+ TelegramInlineKeyboardMarkup,
12
+ } from "./keyboard.ts";
10
13
  import {
11
14
  parseTelegramCommentAttributes,
12
15
  parseTopLevelTelegramComment,
@@ -24,6 +27,7 @@ const TELEGRAM_BUTTON_ACTION_TTL_MS = 24 * 60 * 60 * 1000;
24
27
  export interface TelegramOutboundButtonAction {
25
28
  text: string;
26
29
  prompt: string;
30
+ selectedStyle?: TelegramInlineKeyboardButtonStyle;
27
31
  }
28
32
 
29
33
  export interface TelegramOutboundButtonStoredAction extends TelegramOutboundButtonAction {
@@ -86,11 +90,18 @@ function normalizeMarkdownAfterButtonExtraction(markdown: string): string {
86
90
  function parseButtonsCommentAttributes(input: string): {
87
91
  label?: string;
88
92
  prompt?: string;
93
+ selectedStyle?: TelegramInlineKeyboardButtonStyle;
89
94
  } {
90
95
  const attributes = parseTelegramCommentAttributes(input);
96
+ const selectedStyle = attributes.selected_style;
91
97
  return {
92
98
  ...(attributes.label ? { label: attributes.label } : {}),
93
99
  ...(attributes.prompt ? { prompt: attributes.prompt } : {}),
100
+ ...(selectedStyle === "success" ||
101
+ selectedStyle === "danger" ||
102
+ selectedStyle === "primary"
103
+ ? { selectedStyle }
104
+ : {}),
94
105
  };
95
106
  }
96
107
 
@@ -107,14 +118,34 @@ function parseButtonsCommentRows(
107
118
  }
108
119
  const attributes = parseButtonsCommentAttributes(head);
109
120
  return attributes.label && attributes.prompt
110
- ? [[{ text: attributes.label, prompt: attributes.prompt }]]
121
+ ? [
122
+ [
123
+ {
124
+ text: attributes.label,
125
+ prompt: attributes.prompt,
126
+ ...(attributes.selectedStyle
127
+ ? { selectedStyle: attributes.selectedStyle }
128
+ : {}),
129
+ },
130
+ ],
131
+ ]
111
132
  : [];
112
133
  }
113
134
 
114
- const label = parseButtonsCommentAttributes(head).label;
135
+ const attributes = parseButtonsCommentAttributes(head);
115
136
  const prompt = body.trim();
116
- if (!label || !prompt) return [];
117
- return [[{ text: label, prompt }]];
137
+ if (!attributes.label || !prompt) return [];
138
+ return [
139
+ [
140
+ {
141
+ text: attributes.label,
142
+ prompt,
143
+ ...(attributes.selectedStyle
144
+ ? { selectedStyle: attributes.selectedStyle }
145
+ : {}),
146
+ },
147
+ ],
148
+ ];
118
149
  }
119
150
 
120
151
  export function createTelegramButtonActionStore(
@@ -144,11 +175,20 @@ export function createTelegramButtonActionStore(
144
175
  const action = actions.get(callbackData);
145
176
  if (!action) return undefined;
146
177
  actions.delete(callbackData);
147
- return { text: action.text, prompt: action.prompt };
178
+ return {
179
+ text: action.text,
180
+ prompt: action.prompt,
181
+ ...(action.selectedStyle
182
+ ? { selectedStyle: action.selectedStyle }
183
+ : {}),
184
+ };
148
185
  },
149
186
  };
150
187
  }
151
188
 
189
+ const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
190
+ "☑️ **Choose an option:**";
191
+
152
192
  export function planTelegramButtonReply(
153
193
  markdown: string,
154
194
  deps: { registerAction: (action: TelegramOutboundButtonAction) => string },
@@ -168,8 +208,12 @@ export function planTelegramButtonReply(
168
208
  }
169
209
  return "";
170
210
  });
211
+ const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
171
212
  return {
172
- markdown: normalizeMarkdownAfterButtonExtraction(stripped),
213
+ markdown:
214
+ keyboard.length > 0 && !visibleMarkdown
215
+ ? DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN
216
+ : visibleMarkdown,
173
217
  ...(keyboard.length > 0
174
218
  ? { replyMarkup: { inline_keyboard: keyboard } }
175
219
  : {}),
@@ -212,13 +256,14 @@ export function createTelegramButtonPromptTurn(options: {
212
256
  export function markTelegramButtonSelected(
213
257
  replyMarkup: TelegramOutboundButtonMarkup,
214
258
  callbackData: string,
259
+ selectedStyle: TelegramInlineKeyboardButtonStyle = "primary",
215
260
  ): TelegramOutboundButtonMarkup | undefined {
216
261
  let matched = false;
217
262
  const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
218
263
  row.map((button) => {
219
264
  if (button.callback_data !== callbackData) return { ...button };
220
265
  matched = true;
221
- return { ...button, style: "success" as const };
266
+ return { ...button, style: selectedStyle };
222
267
  }),
223
268
  );
224
269
  return matched ? { inline_keyboard: inlineKeyboard } : undefined;
@@ -253,7 +298,11 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
253
298
  }
254
299
  const selectedMarkup =
255
300
  query.data && query.message?.reply_markup
256
- ? markTelegramButtonSelected(query.message.reply_markup, query.data)
301
+ ? markTelegramButtonSelected(
302
+ query.message.reply_markup,
303
+ query.data,
304
+ action.selectedStyle,
305
+ )
257
306
  : undefined;
258
307
  if (selectedMarkup && deps.editMessageReplyMarkup) {
259
308
  await deps.editMessageReplyMarkup(chatId, messageId, selectedMarkup);
package/lib/prompts.ts CHANGED
@@ -143,8 +143,9 @@ Assistant-authored Telegram actions:
143
143
  - Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
144
144
  - Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
145
145
  - Voice reply modes are compact: \`hidden\` emits no automatic context, \`mirror\` emits it for voice/audio input, and \`always\` emits it for every Telegram turn. Explicit \`telegram_voice\` remains available for an intentionally distinct spoken payload.
146
- - Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\`.
147
- - If hidden comments would be the whole reply, add visible text such as \`Choose one:\`.
146
+ - Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks" selected_style="danger"\nList the main risks first.\n-->\`.
147
+ - Optional \`selected_style\` controls the button after queue admission: \`primary\` (default, blue), \`success\` (green), or \`danger\` (red). It never suppresses the prompt.
148
+ - If hidden button comments form the whole reply, the bridge supplies the standard \`☑️ **Choose an option:**\` heading automatically.
148
149
 
149
150
  Local/TUI direct delivery:
150
151
  - Do not send Telegram actions from local/TUI prompts unless explicitly asked.
package/lib/status.ts CHANGED
@@ -1272,11 +1272,13 @@ function buildStatusSummary(ctx: TelegramStatusContext): string {
1272
1272
  return "unknown";
1273
1273
  }
1274
1274
 
1275
- function buildTelegramStatusRoleSuffix(
1275
+ function buildTelegramStatusThreadSummary(
1276
1276
  state: TelegramBridgeStatusLineState | undefined,
1277
- ): string {
1278
- if (state?.botThreadMode !== "enabled" || !state.busRole) return "";
1279
- return ` @${state.busRole}`;
1277
+ ): string | undefined {
1278
+ if (state?.botThreadMode !== "enabled" || !state.busRole) return undefined;
1279
+ const threadName = state.instanceThreadName?.trim();
1280
+ const identity = threadName || (state.instanceSlot ? `[${state.instanceSlot}]` : "");
1281
+ return identity ? `${identity} @${state.busRole}` : undefined;
1280
1282
  }
1281
1283
 
1282
1284
  export function buildStatusHtml(
@@ -1288,12 +1290,11 @@ export function buildStatusHtml(
1288
1290
  const usesSubscription = activeModel
1289
1291
  ? ctx.modelRegistry.isUsingOAuth(activeModel)
1290
1292
  : false;
1291
- const lines: string[] = [
1292
- buildStatusRow(
1293
- "Status",
1294
- `${buildStatusSummary(ctx)}${buildTelegramStatusRoleSuffix(bridgeStatus)}`,
1295
- ),
1296
- ];
1293
+ const lines: string[] = [buildStatusRow("Status", buildStatusSummary(ctx))];
1294
+ const threadSummary = buildTelegramStatusThreadSummary(bridgeStatus);
1295
+ if (threadSummary) {
1296
+ lines.push(buildStatusRow("Thread", threadSummary));
1297
+ }
1297
1298
  const usageSummary = buildUsageSummary(stats);
1298
1299
  const costSummary = buildCostSummary(stats, usesSubscription);
1299
1300
  if (usageSummary) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.24.7",
3
+ "version": "0.24.9",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"