@llblab/pi-telegram 0.16.6 → 0.17.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/AGENTS.md +14 -8
- package/BACKLOG.md +8 -5
- package/CHANGELOG.md +8 -0
- package/README.md +7 -5
- package/docs/README.md +2 -1
- package/docs/architecture.md +13 -11
- package/docs/outbound.md +2 -2
- package/docs/public-api.md +3 -0
- package/docs/sections.md +4 -2
- package/docs/telegram-bot-api-rich-messages.md +890 -0
- package/docs/ui-style.md +1 -1
- package/index.ts +11 -2
- package/lib/commands.ts +2 -2
- package/lib/menu-model.ts +3 -3
- package/lib/menu-settings.ts +2 -2
- package/lib/menu.ts +4 -4
- package/lib/preview.ts +150 -195
- package/lib/prompts.ts +3 -3
- package/lib/rendering.ts +3 -349
- package/lib/replies.ts +186 -31
- package/lib/routing.ts +2 -2
- package/lib/sections.ts +10 -5
- package/lib/telegram-api.ts +72 -9
- package/package.json +1 -1
package/lib/preview.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Telegram preview streaming helpers
|
|
3
|
-
* Zones: telegram outbound,
|
|
3
|
+
* Zones: telegram outbound, native rich markdown drafts, fallback preview transport
|
|
4
4
|
* Owns preview transport selection, runtime updates, and preview finalization
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -10,23 +10,13 @@ import type {
|
|
|
10
10
|
TelegramSendMessageBody,
|
|
11
11
|
TelegramSentMessage,
|
|
12
12
|
} from "./telegram-api.ts";
|
|
13
|
-
import {
|
|
14
|
-
buildTelegramPreviewSnapshot,
|
|
15
|
-
MAX_MESSAGE_LENGTH,
|
|
16
|
-
renderMarkdownPreviewText,
|
|
17
|
-
renderTelegramMessage,
|
|
18
|
-
type TelegramPreviewRenderStrategy,
|
|
19
|
-
type TelegramPreviewSnapshot,
|
|
20
|
-
type TelegramRenderedChunk,
|
|
21
|
-
type TelegramRenderMode,
|
|
22
|
-
} from "./rendering.ts";
|
|
23
|
-
|
|
24
13
|
import { buildTelegramReplyParameters } from "./replies.ts";
|
|
25
14
|
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
|
26
15
|
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
27
16
|
|
|
28
|
-
const TELEGRAM_PREVIEW_THROTTLE_MS =
|
|
17
|
+
const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
|
|
29
18
|
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
19
|
+
const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
|
|
30
20
|
|
|
31
21
|
export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
|
|
32
22
|
|
|
@@ -37,7 +27,6 @@ export interface TelegramPreviewState {
|
|
|
37
27
|
pendingText: string;
|
|
38
28
|
lastSentText: string;
|
|
39
29
|
lastSentParseMode?: "HTML";
|
|
40
|
-
lastSentStrategy?: TelegramPreviewRenderStrategy;
|
|
41
30
|
}
|
|
42
31
|
|
|
43
32
|
export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
|
|
@@ -56,7 +45,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
56
45
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
57
46
|
clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
|
|
58
47
|
maxMessageLength: number;
|
|
59
|
-
renderPreviewText: (markdown: string) => string;
|
|
60
48
|
getDraftSupport: () => TelegramDraftSupport;
|
|
61
49
|
setDraftSupport: (support: TelegramDraftSupport) => void;
|
|
62
50
|
allocateDraftId: () => number;
|
|
@@ -81,21 +69,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
81
69
|
text: string,
|
|
82
70
|
options?: { parseMode?: "HTML" },
|
|
83
71
|
) => Promise<unknown>;
|
|
84
|
-
renderTelegramMessage: (
|
|
85
|
-
text: string,
|
|
86
|
-
options?: { mode?: TelegramRenderMode },
|
|
87
|
-
) => TelegramRenderedChunk[];
|
|
88
|
-
sendRenderedChunks: (
|
|
89
|
-
chatId: number,
|
|
90
|
-
chunks: TelegramRenderedChunk[],
|
|
91
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
92
|
-
) => Promise<number | undefined>;
|
|
93
|
-
editRenderedMessage: (
|
|
94
|
-
chatId: number,
|
|
95
|
-
messageId: number,
|
|
96
|
-
chunks: TelegramRenderedChunk[],
|
|
97
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
98
|
-
) => Promise<number | undefined>;
|
|
99
72
|
canSend?: () => boolean;
|
|
100
73
|
recordRuntimeEvent?: (
|
|
101
74
|
category: string,
|
|
@@ -162,7 +135,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
162
135
|
> {
|
|
163
136
|
getDefaultReplyToMessageId?: () => number | undefined;
|
|
164
137
|
maxMessageLength?: number;
|
|
165
|
-
renderPreviewText?: (markdown: string) => string;
|
|
166
138
|
initialDraftSupport?: TelegramDraftSupport;
|
|
167
139
|
sendDraft: (
|
|
168
140
|
chatId: number,
|
|
@@ -186,22 +158,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
186
158
|
text: string,
|
|
187
159
|
options?: { parseMode?: "HTML" },
|
|
188
160
|
) => Promise<unknown>;
|
|
189
|
-
renderTelegramMessage?: (
|
|
190
|
-
text: string,
|
|
191
|
-
options?: { mode?: TelegramRenderMode },
|
|
192
|
-
) => TelegramRenderedChunk[];
|
|
193
|
-
sendRenderedChunks: (
|
|
194
|
-
chatId: number,
|
|
195
|
-
chunks: TelegramRenderedChunk[],
|
|
196
|
-
replyToMessageId: number | undefined,
|
|
197
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
198
|
-
) => Promise<number | undefined>;
|
|
199
|
-
editRenderedMessage: (
|
|
200
|
-
chatId: number,
|
|
201
|
-
messageId: number,
|
|
202
|
-
chunks: TelegramRenderedChunk[],
|
|
203
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
204
|
-
) => Promise<number | undefined>;
|
|
205
161
|
canSend?: () => boolean;
|
|
206
162
|
throttleMs?: number;
|
|
207
163
|
maxDraftId?: number;
|
|
@@ -229,12 +185,6 @@ export interface TelegramPreviewController<
|
|
|
229
185
|
flush: (chatId: number) => Promise<void>;
|
|
230
186
|
scheduleFlush: (chatId: number) => void;
|
|
231
187
|
finalize: (chatId: number, replyToMessageId?: number) => Promise<boolean>;
|
|
232
|
-
finalizeMarkdown: (
|
|
233
|
-
chatId: number,
|
|
234
|
-
markdown: string,
|
|
235
|
-
replyToMessageId?: number,
|
|
236
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
237
|
-
) => Promise<boolean>;
|
|
238
188
|
}
|
|
239
189
|
|
|
240
190
|
export interface TelegramPreviewMessageTransportDeps {
|
|
@@ -271,55 +221,13 @@ export function createTelegramPreviewMessageTransport(
|
|
|
271
221
|
};
|
|
272
222
|
}
|
|
273
223
|
|
|
274
|
-
export interface TelegramPreviewRenderedChunkTransportDeps<
|
|
275
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
276
|
-
> {
|
|
277
|
-
sendRenderedChunks: (
|
|
278
|
-
chatId: number,
|
|
279
|
-
chunks: TelegramRenderedChunk[],
|
|
280
|
-
options?: {
|
|
281
|
-
replyToMessageId?: number;
|
|
282
|
-
replyMarkup?: TReplyMarkup;
|
|
283
|
-
},
|
|
284
|
-
) => Promise<number | undefined>;
|
|
285
|
-
editRenderedMessage: (
|
|
286
|
-
chatId: number,
|
|
287
|
-
messageId: number,
|
|
288
|
-
chunks: TelegramRenderedChunk[],
|
|
289
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
290
|
-
) => Promise<number | undefined>;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
export function createTelegramPreviewRenderedChunkTransport<
|
|
294
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
295
|
-
>(
|
|
296
|
-
deps: TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>,
|
|
297
|
-
): Pick<
|
|
298
|
-
TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
299
|
-
"sendRenderedChunks" | "editRenderedMessage"
|
|
300
|
-
> {
|
|
301
|
-
return {
|
|
302
|
-
sendRenderedChunks: (chatId, chunks, replyToMessageId, options) =>
|
|
303
|
-
deps.sendRenderedChunks(chatId, chunks, {
|
|
304
|
-
replyToMessageId,
|
|
305
|
-
...(options?.replyMarkup ? { replyMarkup: options.replyMarkup } : {}),
|
|
306
|
-
}),
|
|
307
|
-
editRenderedMessage: (chatId, messageId, chunks, options) =>
|
|
308
|
-
deps.editRenderedMessage(chatId, messageId, chunks, options),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
224
|
export type TelegramPreviewControllerRuntimeDeps<
|
|
313
225
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
314
226
|
> = Omit<
|
|
315
227
|
TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
316
|
-
| "
|
|
317
|
-
| "editMessageText"
|
|
318
|
-
| "sendRenderedChunks"
|
|
319
|
-
| "editRenderedMessage"
|
|
228
|
+
"sendMessage" | "editMessageText"
|
|
320
229
|
> &
|
|
321
|
-
TelegramPreviewMessageTransportDeps
|
|
322
|
-
TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
|
|
230
|
+
TelegramPreviewMessageTransportDeps;
|
|
323
231
|
|
|
324
232
|
export function createTelegramPreviewControllerRuntime<
|
|
325
233
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
@@ -329,7 +237,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
329
237
|
return createTelegramPreviewController({
|
|
330
238
|
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
331
239
|
maxMessageLength: deps.maxMessageLength,
|
|
332
|
-
renderPreviewText: deps.renderPreviewText,
|
|
333
240
|
initialDraftSupport: deps.initialDraftSupport,
|
|
334
241
|
sendDraft: deps.sendDraft,
|
|
335
242
|
...createTelegramPreviewMessageTransport({
|
|
@@ -337,11 +244,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
337
244
|
editMessageText: deps.editMessageText,
|
|
338
245
|
buildReplyParameters: deps.buildReplyParameters,
|
|
339
246
|
}),
|
|
340
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
341
|
-
...createTelegramPreviewRenderedChunkTransport({
|
|
342
|
-
sendRenderedChunks: deps.sendRenderedChunks,
|
|
343
|
-
editRenderedMessage: deps.editRenderedMessage,
|
|
344
|
-
}),
|
|
345
247
|
throttleMs: deps.throttleMs,
|
|
346
248
|
maxDraftId: deps.maxDraftId,
|
|
347
249
|
setTimer: deps.setTimer,
|
|
@@ -357,13 +259,91 @@ export interface TelegramAssistantPreviewRuntimeDeps<
|
|
|
357
259
|
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
358
260
|
isAssistantMessage: (message: TMessage) => boolean;
|
|
359
261
|
getMessageText: (message: TMessage) => string;
|
|
262
|
+
sendMarkdownReply: (
|
|
263
|
+
chatId: number,
|
|
264
|
+
replyToMessageId: number | undefined,
|
|
265
|
+
markdown: string,
|
|
266
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
267
|
+
) => Promise<number | undefined>;
|
|
268
|
+
editMarkdownMessage?: (
|
|
269
|
+
chatId: number,
|
|
270
|
+
messageId: number,
|
|
271
|
+
markdown: string,
|
|
272
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
273
|
+
) => Promise<unknown>;
|
|
360
274
|
}
|
|
361
275
|
|
|
362
276
|
export type TelegramAssistantPreviewRuntime<
|
|
363
277
|
TMessage,
|
|
364
278
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
365
279
|
> = TelegramPreviewController<TReplyMarkup> &
|
|
366
|
-
TelegramAssistantMessagePreviewHooks<TMessage
|
|
280
|
+
TelegramAssistantMessagePreviewHooks<TMessage> & {
|
|
281
|
+
finalizeMarkdown: (
|
|
282
|
+
chatId: number,
|
|
283
|
+
markdown: string,
|
|
284
|
+
replyToMessageId?: number,
|
|
285
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
286
|
+
) => Promise<boolean>;
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
|
|
290
|
+
editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
|
|
291
|
+
}): (
|
|
292
|
+
chatId: number,
|
|
293
|
+
messageId: number,
|
|
294
|
+
markdown: string,
|
|
295
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
296
|
+
) => Promise<unknown> {
|
|
297
|
+
return (chatId, messageId, markdown, options) =>
|
|
298
|
+
deps.editMessageText({
|
|
299
|
+
chat_id: chatId,
|
|
300
|
+
message_id: messageId,
|
|
301
|
+
rich_message: { markdown, skip_entity_detection: true },
|
|
302
|
+
...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
|
|
307
|
+
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
308
|
+
clear: (chatId: number) => Promise<void>;
|
|
309
|
+
discard?: () => void;
|
|
310
|
+
sendMarkdownReply: (
|
|
311
|
+
chatId: number,
|
|
312
|
+
replyToMessageId: number | undefined,
|
|
313
|
+
markdown: string,
|
|
314
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
315
|
+
) => Promise<number | undefined>;
|
|
316
|
+
editMarkdownMessage?: (
|
|
317
|
+
chatId: number,
|
|
318
|
+
messageId: number,
|
|
319
|
+
markdown: string,
|
|
320
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
321
|
+
) => Promise<unknown>;
|
|
322
|
+
}): (
|
|
323
|
+
chatId: number,
|
|
324
|
+
markdown: string,
|
|
325
|
+
replyToMessageId?: number,
|
|
326
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
327
|
+
) => Promise<boolean> {
|
|
328
|
+
return async (chatId, markdown, replyToMessageId, options) => {
|
|
329
|
+
const state = deps.getState();
|
|
330
|
+
if (state?.mode === "message" && state.messageId !== undefined) {
|
|
331
|
+
try {
|
|
332
|
+
await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
|
|
333
|
+
await deps.clear(chatId);
|
|
334
|
+
return true;
|
|
335
|
+
} catch {
|
|
336
|
+
// Fall through to a fresh native Rich Message so final text is not lost.
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (state?.flushPromise) {
|
|
340
|
+
await state.flushPromise.catch(() => {});
|
|
341
|
+
}
|
|
342
|
+
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
343
|
+
deps.discard?.();
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
367
347
|
|
|
368
348
|
export function createTelegramAssistantPreviewRuntime<
|
|
369
349
|
TMessage,
|
|
@@ -372,8 +352,16 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
372
352
|
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
|
|
373
353
|
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
|
|
374
354
|
const controller = createTelegramPreviewControllerRuntime(deps);
|
|
355
|
+
const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
|
|
356
|
+
getState: controller.getState,
|
|
357
|
+
clear: controller.clear,
|
|
358
|
+
discard: () => controller.setState(undefined),
|
|
359
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
360
|
+
editMarkdownMessage: deps.editMarkdownMessage,
|
|
361
|
+
});
|
|
375
362
|
return {
|
|
376
363
|
...controller,
|
|
364
|
+
finalizeMarkdown: finalizeMarkdownPreview,
|
|
377
365
|
...createTelegramAssistantMessagePreviewHooks({
|
|
378
366
|
getActiveTurn: deps.getActiveTurn,
|
|
379
367
|
isAssistantMessage: deps.isAssistantMessage,
|
|
@@ -381,7 +369,7 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
381
369
|
setState: controller.setState,
|
|
382
370
|
createPreviewState: controller.createState,
|
|
383
371
|
finalizePreview: controller.finalize,
|
|
384
|
-
finalizeMarkdownPreview
|
|
372
|
+
finalizeMarkdownPreview,
|
|
385
373
|
getMessageText: deps.getMessageText,
|
|
386
374
|
schedulePreviewFlush: controller.scheduleFlush,
|
|
387
375
|
}),
|
|
@@ -401,9 +389,8 @@ export function createTelegramPreviewController<
|
|
|
401
389
|
setTimeout(callback, ms));
|
|
402
390
|
const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
|
|
403
391
|
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
|
404
|
-
const maxMessageLength =
|
|
405
|
-
|
|
406
|
-
const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
|
|
392
|
+
const maxMessageLength =
|
|
393
|
+
deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
|
|
407
394
|
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
408
395
|
let nextDraftId = 0;
|
|
409
396
|
const getRuntimeDeps = (
|
|
@@ -419,7 +406,6 @@ export function createTelegramPreviewController<
|
|
|
419
406
|
nextState.flushTimer = undefined;
|
|
420
407
|
},
|
|
421
408
|
maxMessageLength,
|
|
422
|
-
renderPreviewText: renderPreview,
|
|
423
409
|
getDraftSupport: () => draftSupport,
|
|
424
410
|
setDraftSupport: (support) => {
|
|
425
411
|
draftSupport = support;
|
|
@@ -437,15 +423,6 @@ export function createTelegramPreviewController<
|
|
|
437
423
|
replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
|
|
438
424
|
),
|
|
439
425
|
editMessageText: deps.editMessageText,
|
|
440
|
-
renderTelegramMessage: renderMessage,
|
|
441
|
-
sendRenderedChunks: (chatId, chunks, options) =>
|
|
442
|
-
deps.sendRenderedChunks(
|
|
443
|
-
chatId,
|
|
444
|
-
chunks,
|
|
445
|
-
replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
|
|
446
|
-
options,
|
|
447
|
-
),
|
|
448
|
-
editRenderedMessage: deps.editRenderedMessage,
|
|
449
426
|
canSend: deps.canSend,
|
|
450
427
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
451
428
|
});
|
|
@@ -465,6 +442,10 @@ export function createTelegramPreviewController<
|
|
|
465
442
|
flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
|
|
466
443
|
scheduleFlush: (chatId) => {
|
|
467
444
|
if (!state || state.flushTimer) return;
|
|
445
|
+
if (throttleMs <= 0) {
|
|
446
|
+
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
468
449
|
state.flushTimer = setTimer(() => {
|
|
469
450
|
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
470
451
|
}, throttleMs);
|
|
@@ -472,13 +453,6 @@ export function createTelegramPreviewController<
|
|
|
472
453
|
},
|
|
473
454
|
finalize: (chatId, replyToMessageId) =>
|
|
474
455
|
finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
|
|
475
|
-
finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
|
|
476
|
-
finalizeTelegramMarkdownPreview(
|
|
477
|
-
chatId,
|
|
478
|
-
markdown,
|
|
479
|
-
getRuntimeDeps(replyToMessageId),
|
|
480
|
-
options,
|
|
481
|
-
),
|
|
482
456
|
};
|
|
483
457
|
}
|
|
484
458
|
|
|
@@ -554,10 +528,7 @@ export function buildTelegramPreviewFinalText(
|
|
|
554
528
|
): string | undefined {
|
|
555
529
|
const finalText = state.pendingText.trim();
|
|
556
530
|
if (finalText) return finalText;
|
|
557
|
-
if (
|
|
558
|
-
state.lastSentStrategy === "rich-stable-blocks" ||
|
|
559
|
-
state.lastSentParseMode === "HTML"
|
|
560
|
-
) {
|
|
531
|
+
if (state.lastSentParseMode === "HTML") {
|
|
561
532
|
return undefined;
|
|
562
533
|
}
|
|
563
534
|
return state.lastSentText.trim() || undefined;
|
|
@@ -580,14 +551,16 @@ export function allocateTelegramDraftId(
|
|
|
580
551
|
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
|
581
552
|
}
|
|
582
553
|
|
|
554
|
+
interface TelegramNativeMarkdownPreviewSnapshot {
|
|
555
|
+
text: string;
|
|
556
|
+
sourceText: string;
|
|
557
|
+
}
|
|
558
|
+
|
|
583
559
|
export function shouldUseTelegramDraftPreview(options: {
|
|
584
560
|
draftSupport: TelegramDraftSupport;
|
|
585
|
-
snapshot?:
|
|
561
|
+
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
|
|
586
562
|
}): boolean {
|
|
587
|
-
return
|
|
588
|
-
options.draftSupport !== "unsupported" &&
|
|
589
|
-
(options.snapshot === undefined || options.snapshot.strategy === "plain")
|
|
590
|
-
);
|
|
563
|
+
return options.draftSupport !== "unsupported";
|
|
591
564
|
}
|
|
592
565
|
|
|
593
566
|
export async function clearTelegramPreview<
|
|
@@ -595,12 +568,43 @@ export async function clearTelegramPreview<
|
|
|
595
568
|
>(
|
|
596
569
|
chatId: number,
|
|
597
570
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
571
|
+
options: { awaitFlush?: boolean } = {},
|
|
598
572
|
): Promise<void> {
|
|
599
|
-
void chatId;
|
|
600
573
|
const state = deps.getState();
|
|
601
574
|
if (!state) return;
|
|
602
575
|
deps.clearScheduledFlush(state);
|
|
576
|
+
if (state.flushPromise && options.awaitFlush !== false) {
|
|
577
|
+
state.flushRequested = false;
|
|
578
|
+
await state.flushPromise.catch(() => {});
|
|
579
|
+
if (deps.getState() !== state) return;
|
|
580
|
+
}
|
|
603
581
|
deps.setState(undefined);
|
|
582
|
+
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
583
|
+
try {
|
|
584
|
+
await deps.sendDraft(chatId, state.draftId, undefined);
|
|
585
|
+
} catch (error) {
|
|
586
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
587
|
+
phase: "clear-draft",
|
|
588
|
+
chatId,
|
|
589
|
+
draftId: state.draftId,
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
596
|
+
state: TelegramPreviewState,
|
|
597
|
+
maxMessageLength: number,
|
|
598
|
+
): TelegramNativeMarkdownPreviewSnapshot | undefined {
|
|
599
|
+
const sourceText = state.pendingText.trim();
|
|
600
|
+
if (!sourceText || sourceText === state.lastSentText) return undefined;
|
|
601
|
+
return {
|
|
602
|
+
text:
|
|
603
|
+
sourceText.length > maxMessageLength
|
|
604
|
+
? sourceText.slice(0, maxMessageLength)
|
|
605
|
+
: sourceText,
|
|
606
|
+
sourceText,
|
|
607
|
+
};
|
|
604
608
|
}
|
|
605
609
|
|
|
606
610
|
async function performTelegramPreviewFlush<
|
|
@@ -611,15 +615,13 @@ async function performTelegramPreviewFlush<
|
|
|
611
615
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
612
616
|
): Promise<void> {
|
|
613
617
|
if (deps.canSend && !deps.canSend()) {
|
|
614
|
-
await clearTelegramPreview(chatId, deps);
|
|
618
|
+
await clearTelegramPreview(chatId, deps, { awaitFlush: false });
|
|
615
619
|
return;
|
|
616
620
|
}
|
|
617
|
-
const snapshot =
|
|
621
|
+
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
618
622
|
state,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
622
|
-
});
|
|
623
|
+
deps.maxMessageLength,
|
|
624
|
+
);
|
|
623
625
|
if (!snapshot) return;
|
|
624
626
|
if (
|
|
625
627
|
shouldUseTelegramDraftPreview({
|
|
@@ -630,35 +632,28 @@ async function performTelegramPreviewFlush<
|
|
|
630
632
|
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
631
633
|
state.draftId = draftId;
|
|
632
634
|
try {
|
|
633
|
-
await deps.sendDraft(chatId, draftId, snapshot.
|
|
635
|
+
await deps.sendDraft(chatId, draftId, snapshot.sourceText);
|
|
634
636
|
deps.setDraftSupport("supported");
|
|
635
637
|
state.mode = "draft";
|
|
636
638
|
state.lastSentText = snapshot.text;
|
|
637
|
-
state.lastSentParseMode =
|
|
638
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
639
|
+
state.lastSentParseMode = undefined;
|
|
639
640
|
return;
|
|
640
641
|
} catch {
|
|
641
642
|
deps.setDraftSupport("unsupported");
|
|
642
643
|
}
|
|
643
644
|
}
|
|
644
645
|
if (state.messageId === undefined) {
|
|
645
|
-
const sent = await deps.sendMessage(chatId, snapshot.text,
|
|
646
|
-
parseMode: snapshot.parseMode,
|
|
647
|
-
});
|
|
646
|
+
const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
|
|
648
647
|
state.messageId = sent.message_id;
|
|
649
648
|
state.mode = "message";
|
|
650
649
|
state.lastSentText = snapshot.text;
|
|
651
|
-
state.lastSentParseMode =
|
|
652
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
650
|
+
state.lastSentParseMode = undefined;
|
|
653
651
|
return;
|
|
654
652
|
}
|
|
655
|
-
await deps.editMessageText(chatId, state.messageId, snapshot.text,
|
|
656
|
-
parseMode: snapshot.parseMode,
|
|
657
|
-
});
|
|
653
|
+
await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
|
|
658
654
|
state.mode = "message";
|
|
659
655
|
state.lastSentText = snapshot.text;
|
|
660
|
-
state.lastSentParseMode =
|
|
661
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
656
|
+
state.lastSentParseMode = undefined;
|
|
662
657
|
}
|
|
663
658
|
|
|
664
659
|
export async function flushTelegramPreview<
|
|
@@ -719,49 +714,9 @@ export async function finalizeTelegramPreview<
|
|
|
719
714
|
}
|
|
720
715
|
if (state.mode === "draft") {
|
|
721
716
|
await deps.sendMessage(chatId, finalText);
|
|
722
|
-
|
|
717
|
+
deps.setState(undefined);
|
|
723
718
|
return true;
|
|
724
719
|
}
|
|
725
720
|
deps.setState(undefined);
|
|
726
721
|
return state.messageId !== undefined;
|
|
727
722
|
}
|
|
728
|
-
|
|
729
|
-
export async function finalizeTelegramMarkdownPreview<
|
|
730
|
-
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
731
|
-
>(
|
|
732
|
-
chatId: number,
|
|
733
|
-
markdown: string,
|
|
734
|
-
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
735
|
-
options?: { replyMarkup?: TReplyMarkup },
|
|
736
|
-
): Promise<boolean> {
|
|
737
|
-
const state = deps.getState();
|
|
738
|
-
if (!state) return false;
|
|
739
|
-
if (deps.canSend && !deps.canSend()) {
|
|
740
|
-
await clearTelegramPreview(chatId, deps);
|
|
741
|
-
return false;
|
|
742
|
-
}
|
|
743
|
-
await flushTelegramPreview(chatId, deps);
|
|
744
|
-
const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
|
|
745
|
-
if (chunks.length === 0) {
|
|
746
|
-
await clearTelegramPreview(chatId, deps);
|
|
747
|
-
return false;
|
|
748
|
-
}
|
|
749
|
-
try {
|
|
750
|
-
if (state.mode === "draft") {
|
|
751
|
-
await deps.sendRenderedChunks(chatId, chunks, options);
|
|
752
|
-
await clearTelegramPreview(chatId, deps);
|
|
753
|
-
return true;
|
|
754
|
-
}
|
|
755
|
-
if (state.messageId === undefined) return false;
|
|
756
|
-
await deps.editRenderedMessage(chatId, state.messageId, chunks, options);
|
|
757
|
-
deps.setState(undefined);
|
|
758
|
-
return true;
|
|
759
|
-
} catch (error) {
|
|
760
|
-
deps.recordRuntimeEvent?.("preview", error, {
|
|
761
|
-
phase: "finalize-markdown",
|
|
762
|
-
chatId,
|
|
763
|
-
messageId: state.messageId,
|
|
764
|
-
});
|
|
765
|
-
return false;
|
|
766
|
-
}
|
|
767
|
-
}
|
package/lib/prompts.ts
CHANGED
|
@@ -28,13 +28,13 @@ Telegram-originated turn context:
|
|
|
28
28
|
- Unknown \`[callback] ...\` messages may be intended for another extension; if you see one, say the callback was not handled and the environment may be misconfigured.
|
|
29
29
|
|
|
30
30
|
Telegram-visible output:
|
|
31
|
-
- Telegram is
|
|
32
|
-
-
|
|
31
|
+
- Telegram is mobile-first: keep answers easy to scan, use headings/lists when useful, and avoid unnecessarily huge blocks of text.
|
|
32
|
+
- For formulas, use math delimiters like \`$E = mc^2$\` for inline formulas and \`$$\\nE = mc^2\\n$$\` for block formulas; do not wrap formulas in backticks unless they should render as literal code.
|
|
33
33
|
- Wide monospace blocks can become unreadable on mobile; use them only when structure or literal code requires them.
|
|
34
34
|
- For requested/generated files, call \`telegram_attach(local_path)\`; during Telegram turns it attaches files to the active reply, and during explicit local/TUI Telegram-delivery requests it sends files directly to the paired/default chat or an explicit \`chat_id\`. If a local/TUI user explicitly asks to send a text message to Telegram, use \`telegram_message\` with Markdown text; embed the same top-level \`telegram_button\` comments when inline prompt buttons are needed, because Telegram buttons must belong to a message. Direct local/TUI Telegram delivery requires this π instance to own \`/telegram-connect\`; if ownership is elsewhere, connect/take over first instead of bypassing the lock.
|
|
35
35
|
|
|
36
36
|
Native outbound actions:
|
|
37
|
-
- Use normal Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
|
|
37
|
+
- Use normal Rich Markdown for visible text. Use top-level column-zero hidden Markdown comments outside code, quotes, and lists only for native actions; the bridge strips them after agent_end and turns them into Telegram-native artifacts/reply_markup. Do not render button JSON, do not invent standalone button tools, and do not call/register transport/TTS/text-to-OGG tools for ordinary Telegram-turn voice/buttons.
|
|
38
38
|
- \`telegram_voice\`: text is synthesized by the registered voice synthesis provider and delivered by pi-telegram. Use body text for multiline voice, \`<!-- telegram_voice text="Short summary" -->\` for explicit one-line text, or \`<!-- telegram_voice: Short summary -->\` for one-line text with no attributes. A companion summary is optional, no specific summary format is required. Keep it TTS-friendly; avoid raw Markdown, code, formulas, tables, or long lists.
|
|
39
39
|
- \`telegram_button\`: callback prompt is routed back as a normal Telegram turn. Use \`<!-- telegram_button: OK -->\` when prompt equals label, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\` for one-line prompts, or body form \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\` for multiline prompts. Do not put button comments inline after visible text, inside code fences, block quotes, lists, or indented examples; those are literal Markdown, not buttons.
|
|
40
40
|
- If only hidden action comments would remain, add visible parent text like "Choose one:" so Telegram has a message to attach buttons to.
|