@llblab/pi-telegram 0.24.7 → 0.24.8
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 +1 -1
- package/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/api/keyboard.ts +1 -0
- package/docs/outbound.md +4 -3
- package/docs/public-api.md +1 -1
- package/docs/ui-style.md +1 -1
- package/lib/keyboard.ts +6 -1
- package/lib/outbound-buttons.ts +57 -9
- package/lib/prompts.ts +3 -2
- package/lib/status.ts +11 -10
- package/package.json +1 -1
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
|
|
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 fallback text when buttons are the only output
|
|
129
129
|
|
|
130
130
|
## 6. Engineering Conventions
|
|
131
131
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.24.8: Prompt Button Reliability Hotfix
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
- `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.
|
|
9
|
+
|
|
5
10
|
## 0.24.7: Thread Cleanup Hotfix
|
|
6
11
|
|
|
7
12
|
- `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
|
|
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 `Choose an option:` 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
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
|
|
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 `Choose an option:` 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.
|
|
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
|
-
-
|
|
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.
|
package/docs/public-api.md
CHANGED
|
@@ -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
|
@@ -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
|
|
180
|
+
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?:
|
|
15
|
+
style?: TelegramInlineKeyboardButtonStyle;
|
|
11
16
|
}
|
|
12
17
|
|
|
13
18
|
export interface TelegramInlineKeyboardMarkup {
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
|
|
9
|
-
import type {
|
|
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
|
-
? [
|
|
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
|
|
135
|
+
const attributes = parseButtonsCommentAttributes(head);
|
|
115
136
|
const prompt = body.trim();
|
|
116
|
-
if (!label || !prompt) return [];
|
|
117
|
-
return [
|
|
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,19 @@ export function createTelegramButtonActionStore(
|
|
|
144
175
|
const action = actions.get(callbackData);
|
|
145
176
|
if (!action) return undefined;
|
|
146
177
|
actions.delete(callbackData);
|
|
147
|
-
return {
|
|
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 = "Choose an option:";
|
|
190
|
+
|
|
152
191
|
export function planTelegramButtonReply(
|
|
153
192
|
markdown: string,
|
|
154
193
|
deps: { registerAction: (action: TelegramOutboundButtonAction) => string },
|
|
@@ -168,8 +207,12 @@ export function planTelegramButtonReply(
|
|
|
168
207
|
}
|
|
169
208
|
return "";
|
|
170
209
|
});
|
|
210
|
+
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
|
|
171
211
|
return {
|
|
172
|
-
markdown:
|
|
212
|
+
markdown:
|
|
213
|
+
keyboard.length > 0 && !visibleMarkdown
|
|
214
|
+
? DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN
|
|
215
|
+
: visibleMarkdown,
|
|
173
216
|
...(keyboard.length > 0
|
|
174
217
|
? { replyMarkup: { inline_keyboard: keyboard } }
|
|
175
218
|
: {}),
|
|
@@ -212,13 +255,14 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
212
255
|
export function markTelegramButtonSelected(
|
|
213
256
|
replyMarkup: TelegramOutboundButtonMarkup,
|
|
214
257
|
callbackData: string,
|
|
258
|
+
selectedStyle: TelegramInlineKeyboardButtonStyle = "primary",
|
|
215
259
|
): TelegramOutboundButtonMarkup | undefined {
|
|
216
260
|
let matched = false;
|
|
217
261
|
const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
|
|
218
262
|
row.map((button) => {
|
|
219
263
|
if (button.callback_data !== callbackData) return { ...button };
|
|
220
264
|
matched = true;
|
|
221
|
-
return { ...button, style:
|
|
265
|
+
return { ...button, style: selectedStyle };
|
|
222
266
|
}),
|
|
223
267
|
);
|
|
224
268
|
return matched ? { inline_keyboard: inlineKeyboard } : undefined;
|
|
@@ -253,7 +297,11 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
|
|
|
253
297
|
}
|
|
254
298
|
const selectedMarkup =
|
|
255
299
|
query.data && query.message?.reply_markup
|
|
256
|
-
? markTelegramButtonSelected(
|
|
300
|
+
? markTelegramButtonSelected(
|
|
301
|
+
query.message.reply_markup,
|
|
302
|
+
query.data,
|
|
303
|
+
action.selectedStyle,
|
|
304
|
+
)
|
|
257
305
|
: undefined;
|
|
258
306
|
if (selectedMarkup && deps.editMessageReplyMarkup) {
|
|
259
307
|
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
|
-
-
|
|
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 visible fallback text 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
|
|
1275
|
+
function buildTelegramStatusThreadSummary(
|
|
1276
1276
|
state: TelegramBridgeStatusLineState | undefined,
|
|
1277
|
-
): string {
|
|
1278
|
-
if (state?.botThreadMode !== "enabled" || !state.busRole) return
|
|
1279
|
-
|
|
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
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
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) {
|