@llblab/pi-telegram 0.16.6 → 0.17.1
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 +15 -9
- package/BACKLOG.md +8 -5
- package/CHANGELOG.md +12 -0
- package/README.md +7 -5
- package/docs/README.md +2 -1
- package/docs/architecture.md +16 -12
- 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/media.ts +92 -1
- package/lib/menu-model.ts +3 -3
- package/lib/menu-settings.ts +2 -2
- package/lib/menu.ts +4 -4
- package/lib/preview.ts +160 -195
- package/lib/prompts.ts +4 -4
- package/lib/rendering.ts +3 -349
- package/lib/replies.ts +227 -31
- package/lib/routing.ts +2 -2
- package/lib/sections.ts +10 -5
- package/lib/telegram-api.ts +79 -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
|
|
|
@@ -11,22 +11,15 @@ import type {
|
|
|
11
11
|
TelegramSentMessage,
|
|
12
12
|
} from "./telegram-api.ts";
|
|
13
13
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
renderTelegramMessage,
|
|
18
|
-
type TelegramPreviewRenderStrategy,
|
|
19
|
-
type TelegramPreviewSnapshot,
|
|
20
|
-
type TelegramRenderedChunk,
|
|
21
|
-
type TelegramRenderMode,
|
|
22
|
-
} from "./rendering.ts";
|
|
23
|
-
|
|
24
|
-
import { buildTelegramReplyParameters } from "./replies.ts";
|
|
14
|
+
buildTelegramReplyParameters,
|
|
15
|
+
normalizeTelegramNativeMarkdown,
|
|
16
|
+
} from "./replies.ts";
|
|
25
17
|
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
|
|
26
18
|
import { shouldSuppressPreviewForVoice } from "./voice.ts";
|
|
27
19
|
|
|
28
|
-
const TELEGRAM_PREVIEW_THROTTLE_MS =
|
|
20
|
+
const TELEGRAM_PREVIEW_THROTTLE_MS = 0;
|
|
29
21
|
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
|
22
|
+
const TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS = 4096;
|
|
30
23
|
|
|
31
24
|
export type TelegramDraftSupport = "unknown" | "supported" | "unsupported";
|
|
32
25
|
|
|
@@ -37,7 +30,6 @@ export interface TelegramPreviewState {
|
|
|
37
30
|
pendingText: string;
|
|
38
31
|
lastSentText: string;
|
|
39
32
|
lastSentParseMode?: "HTML";
|
|
40
|
-
lastSentStrategy?: TelegramPreviewRenderStrategy;
|
|
41
33
|
}
|
|
42
34
|
|
|
43
35
|
export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
|
|
@@ -56,7 +48,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
56
48
|
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
|
|
57
49
|
clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
|
|
58
50
|
maxMessageLength: number;
|
|
59
|
-
renderPreviewText: (markdown: string) => string;
|
|
60
51
|
getDraftSupport: () => TelegramDraftSupport;
|
|
61
52
|
setDraftSupport: (support: TelegramDraftSupport) => void;
|
|
62
53
|
allocateDraftId: () => number;
|
|
@@ -81,21 +72,6 @@ export interface TelegramPreviewRuntimeDeps<
|
|
|
81
72
|
text: string,
|
|
82
73
|
options?: { parseMode?: "HTML" },
|
|
83
74
|
) => 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
75
|
canSend?: () => boolean;
|
|
100
76
|
recordRuntimeEvent?: (
|
|
101
77
|
category: string,
|
|
@@ -162,7 +138,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
162
138
|
> {
|
|
163
139
|
getDefaultReplyToMessageId?: () => number | undefined;
|
|
164
140
|
maxMessageLength?: number;
|
|
165
|
-
renderPreviewText?: (markdown: string) => string;
|
|
166
141
|
initialDraftSupport?: TelegramDraftSupport;
|
|
167
142
|
sendDraft: (
|
|
168
143
|
chatId: number,
|
|
@@ -186,22 +161,6 @@ export interface TelegramPreviewControllerDeps<
|
|
|
186
161
|
text: string,
|
|
187
162
|
options?: { parseMode?: "HTML" },
|
|
188
163
|
) => 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
164
|
canSend?: () => boolean;
|
|
206
165
|
throttleMs?: number;
|
|
207
166
|
maxDraftId?: number;
|
|
@@ -229,12 +188,6 @@ export interface TelegramPreviewController<
|
|
|
229
188
|
flush: (chatId: number) => Promise<void>;
|
|
230
189
|
scheduleFlush: (chatId: number) => void;
|
|
231
190
|
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
191
|
}
|
|
239
192
|
|
|
240
193
|
export interface TelegramPreviewMessageTransportDeps {
|
|
@@ -271,55 +224,13 @@ export function createTelegramPreviewMessageTransport(
|
|
|
271
224
|
};
|
|
272
225
|
}
|
|
273
226
|
|
|
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
227
|
export type TelegramPreviewControllerRuntimeDeps<
|
|
313
228
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
314
229
|
> = Omit<
|
|
315
230
|
TelegramPreviewControllerDeps<TReplyMarkup>,
|
|
316
|
-
| "
|
|
317
|
-
| "editMessageText"
|
|
318
|
-
| "sendRenderedChunks"
|
|
319
|
-
| "editRenderedMessage"
|
|
231
|
+
"sendMessage" | "editMessageText"
|
|
320
232
|
> &
|
|
321
|
-
TelegramPreviewMessageTransportDeps
|
|
322
|
-
TelegramPreviewRenderedChunkTransportDeps<TReplyMarkup>;
|
|
233
|
+
TelegramPreviewMessageTransportDeps;
|
|
323
234
|
|
|
324
235
|
export function createTelegramPreviewControllerRuntime<
|
|
325
236
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
@@ -329,7 +240,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
329
240
|
return createTelegramPreviewController({
|
|
330
241
|
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
|
|
331
242
|
maxMessageLength: deps.maxMessageLength,
|
|
332
|
-
renderPreviewText: deps.renderPreviewText,
|
|
333
243
|
initialDraftSupport: deps.initialDraftSupport,
|
|
334
244
|
sendDraft: deps.sendDraft,
|
|
335
245
|
...createTelegramPreviewMessageTransport({
|
|
@@ -337,11 +247,6 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
337
247
|
editMessageText: deps.editMessageText,
|
|
338
248
|
buildReplyParameters: deps.buildReplyParameters,
|
|
339
249
|
}),
|
|
340
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
341
|
-
...createTelegramPreviewRenderedChunkTransport({
|
|
342
|
-
sendRenderedChunks: deps.sendRenderedChunks,
|
|
343
|
-
editRenderedMessage: deps.editRenderedMessage,
|
|
344
|
-
}),
|
|
345
250
|
throttleMs: deps.throttleMs,
|
|
346
251
|
maxDraftId: deps.maxDraftId,
|
|
347
252
|
setTimer: deps.setTimer,
|
|
@@ -357,13 +262,94 @@ export interface TelegramAssistantPreviewRuntimeDeps<
|
|
|
357
262
|
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
|
|
358
263
|
isAssistantMessage: (message: TMessage) => boolean;
|
|
359
264
|
getMessageText: (message: TMessage) => string;
|
|
265
|
+
sendMarkdownReply: (
|
|
266
|
+
chatId: number,
|
|
267
|
+
replyToMessageId: number | undefined,
|
|
268
|
+
markdown: string,
|
|
269
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
270
|
+
) => Promise<number | undefined>;
|
|
271
|
+
editMarkdownMessage?: (
|
|
272
|
+
chatId: number,
|
|
273
|
+
messageId: number,
|
|
274
|
+
markdown: string,
|
|
275
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
276
|
+
) => Promise<unknown>;
|
|
360
277
|
}
|
|
361
278
|
|
|
362
279
|
export type TelegramAssistantPreviewRuntime<
|
|
363
280
|
TMessage,
|
|
364
281
|
TReplyMarkup = TelegramPreviewReplyMarkup,
|
|
365
282
|
> = TelegramPreviewController<TReplyMarkup> &
|
|
366
|
-
TelegramAssistantMessagePreviewHooks<TMessage
|
|
283
|
+
TelegramAssistantMessagePreviewHooks<TMessage> & {
|
|
284
|
+
finalizeMarkdown: (
|
|
285
|
+
chatId: number,
|
|
286
|
+
markdown: string,
|
|
287
|
+
replyToMessageId?: number,
|
|
288
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
289
|
+
) => Promise<boolean>;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export function createTelegramNativeMarkdownMessageEditor<TReplyMarkup>(deps: {
|
|
293
|
+
editMessageText: (body: TelegramEditMessageTextBody) => Promise<unknown>;
|
|
294
|
+
}): (
|
|
295
|
+
chatId: number,
|
|
296
|
+
messageId: number,
|
|
297
|
+
markdown: string,
|
|
298
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
299
|
+
) => Promise<unknown> {
|
|
300
|
+
return (chatId, messageId, markdown, options) =>
|
|
301
|
+
deps.editMessageText({
|
|
302
|
+
chat_id: chatId,
|
|
303
|
+
message_id: messageId,
|
|
304
|
+
rich_message: {
|
|
305
|
+
markdown: normalizeTelegramNativeMarkdown(markdown),
|
|
306
|
+
skip_entity_detection: true,
|
|
307
|
+
},
|
|
308
|
+
...(options?.replyMarkup ? { reply_markup: options.replyMarkup } : {}),
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createTelegramNativeMarkdownPreviewFinalizer<TReplyMarkup>(deps: {
|
|
313
|
+
getState: () => TelegramPreviewRuntimeState | undefined;
|
|
314
|
+
clear: (chatId: number) => Promise<void>;
|
|
315
|
+
discard?: () => void;
|
|
316
|
+
sendMarkdownReply: (
|
|
317
|
+
chatId: number,
|
|
318
|
+
replyToMessageId: number | undefined,
|
|
319
|
+
markdown: string,
|
|
320
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
321
|
+
) => Promise<number | undefined>;
|
|
322
|
+
editMarkdownMessage?: (
|
|
323
|
+
chatId: number,
|
|
324
|
+
messageId: number,
|
|
325
|
+
markdown: string,
|
|
326
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
327
|
+
) => Promise<unknown>;
|
|
328
|
+
}): (
|
|
329
|
+
chatId: number,
|
|
330
|
+
markdown: string,
|
|
331
|
+
replyToMessageId?: number,
|
|
332
|
+
options?: { replyMarkup?: TReplyMarkup },
|
|
333
|
+
) => Promise<boolean> {
|
|
334
|
+
return async (chatId, markdown, replyToMessageId, options) => {
|
|
335
|
+
const state = deps.getState();
|
|
336
|
+
if (state?.mode === "message" && state.messageId !== undefined) {
|
|
337
|
+
try {
|
|
338
|
+
await deps.editMarkdownMessage?.(chatId, state.messageId, markdown, options);
|
|
339
|
+
await deps.clear(chatId);
|
|
340
|
+
return true;
|
|
341
|
+
} catch {
|
|
342
|
+
// Fall through to a fresh native Rich Message so final text is not lost.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (state?.flushPromise) {
|
|
346
|
+
await state.flushPromise.catch(() => {});
|
|
347
|
+
}
|
|
348
|
+
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
|
|
349
|
+
deps.discard?.();
|
|
350
|
+
return true;
|
|
351
|
+
};
|
|
352
|
+
}
|
|
367
353
|
|
|
368
354
|
export function createTelegramAssistantPreviewRuntime<
|
|
369
355
|
TMessage,
|
|
@@ -372,8 +358,16 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
372
358
|
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
|
|
373
359
|
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
|
|
374
360
|
const controller = createTelegramPreviewControllerRuntime(deps);
|
|
361
|
+
const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
|
|
362
|
+
getState: controller.getState,
|
|
363
|
+
clear: controller.clear,
|
|
364
|
+
discard: () => controller.setState(undefined),
|
|
365
|
+
sendMarkdownReply: deps.sendMarkdownReply,
|
|
366
|
+
editMarkdownMessage: deps.editMarkdownMessage,
|
|
367
|
+
});
|
|
375
368
|
return {
|
|
376
369
|
...controller,
|
|
370
|
+
finalizeMarkdown: finalizeMarkdownPreview,
|
|
377
371
|
...createTelegramAssistantMessagePreviewHooks({
|
|
378
372
|
getActiveTurn: deps.getActiveTurn,
|
|
379
373
|
isAssistantMessage: deps.isAssistantMessage,
|
|
@@ -381,7 +375,7 @@ export function createTelegramAssistantPreviewRuntime<
|
|
|
381
375
|
setState: controller.setState,
|
|
382
376
|
createPreviewState: controller.createState,
|
|
383
377
|
finalizePreview: controller.finalize,
|
|
384
|
-
finalizeMarkdownPreview
|
|
378
|
+
finalizeMarkdownPreview,
|
|
385
379
|
getMessageText: deps.getMessageText,
|
|
386
380
|
schedulePreviewFlush: controller.scheduleFlush,
|
|
387
381
|
}),
|
|
@@ -401,9 +395,8 @@ export function createTelegramPreviewController<
|
|
|
401
395
|
setTimeout(callback, ms));
|
|
402
396
|
const throttleMs = deps.throttleMs ?? TELEGRAM_PREVIEW_THROTTLE_MS;
|
|
403
397
|
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
|
|
404
|
-
const maxMessageLength =
|
|
405
|
-
|
|
406
|
-
const renderMessage = deps.renderTelegramMessage ?? renderTelegramMessage;
|
|
398
|
+
const maxMessageLength =
|
|
399
|
+
deps.maxMessageLength ?? TELEGRAM_FALLBACK_PREVIEW_MESSAGE_MAX_CHARS;
|
|
407
400
|
let draftSupport = deps.initialDraftSupport ?? "unknown";
|
|
408
401
|
let nextDraftId = 0;
|
|
409
402
|
const getRuntimeDeps = (
|
|
@@ -419,7 +412,6 @@ export function createTelegramPreviewController<
|
|
|
419
412
|
nextState.flushTimer = undefined;
|
|
420
413
|
},
|
|
421
414
|
maxMessageLength,
|
|
422
|
-
renderPreviewText: renderPreview,
|
|
423
415
|
getDraftSupport: () => draftSupport,
|
|
424
416
|
setDraftSupport: (support) => {
|
|
425
417
|
draftSupport = support;
|
|
@@ -437,15 +429,6 @@ export function createTelegramPreviewController<
|
|
|
437
429
|
replyToMessageId ?? deps.getDefaultReplyToMessageId?.(),
|
|
438
430
|
),
|
|
439
431
|
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
432
|
canSend: deps.canSend,
|
|
450
433
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
451
434
|
});
|
|
@@ -465,6 +448,10 @@ export function createTelegramPreviewController<
|
|
|
465
448
|
flush: (chatId) => flushTelegramPreview(chatId, getRuntimeDeps()),
|
|
466
449
|
scheduleFlush: (chatId) => {
|
|
467
450
|
if (!state || state.flushTimer) return;
|
|
451
|
+
if (throttleMs <= 0) {
|
|
452
|
+
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
468
455
|
state.flushTimer = setTimer(() => {
|
|
469
456
|
void flushTelegramPreview(chatId, getRuntimeDeps());
|
|
470
457
|
}, throttleMs);
|
|
@@ -472,13 +459,6 @@ export function createTelegramPreviewController<
|
|
|
472
459
|
},
|
|
473
460
|
finalize: (chatId, replyToMessageId) =>
|
|
474
461
|
finalizeTelegramPreview(chatId, getRuntimeDeps(replyToMessageId)),
|
|
475
|
-
finalizeMarkdown: (chatId, markdown, replyToMessageId, options) =>
|
|
476
|
-
finalizeTelegramMarkdownPreview(
|
|
477
|
-
chatId,
|
|
478
|
-
markdown,
|
|
479
|
-
getRuntimeDeps(replyToMessageId),
|
|
480
|
-
options,
|
|
481
|
-
),
|
|
482
462
|
};
|
|
483
463
|
}
|
|
484
464
|
|
|
@@ -554,10 +534,7 @@ export function buildTelegramPreviewFinalText(
|
|
|
554
534
|
): string | undefined {
|
|
555
535
|
const finalText = state.pendingText.trim();
|
|
556
536
|
if (finalText) return finalText;
|
|
557
|
-
if (
|
|
558
|
-
state.lastSentStrategy === "rich-stable-blocks" ||
|
|
559
|
-
state.lastSentParseMode === "HTML"
|
|
560
|
-
) {
|
|
537
|
+
if (state.lastSentParseMode === "HTML") {
|
|
561
538
|
return undefined;
|
|
562
539
|
}
|
|
563
540
|
return state.lastSentText.trim() || undefined;
|
|
@@ -580,14 +557,16 @@ export function allocateTelegramDraftId(
|
|
|
580
557
|
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
|
|
581
558
|
}
|
|
582
559
|
|
|
560
|
+
interface TelegramNativeMarkdownPreviewSnapshot {
|
|
561
|
+
text: string;
|
|
562
|
+
sourceText: string;
|
|
563
|
+
}
|
|
564
|
+
|
|
583
565
|
export function shouldUseTelegramDraftPreview(options: {
|
|
584
566
|
draftSupport: TelegramDraftSupport;
|
|
585
|
-
snapshot?:
|
|
567
|
+
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
|
|
586
568
|
}): boolean {
|
|
587
|
-
return
|
|
588
|
-
options.draftSupport !== "unsupported" &&
|
|
589
|
-
(options.snapshot === undefined || options.snapshot.strategy === "plain")
|
|
590
|
-
);
|
|
569
|
+
return options.draftSupport !== "unsupported";
|
|
591
570
|
}
|
|
592
571
|
|
|
593
572
|
export async function clearTelegramPreview<
|
|
@@ -595,12 +574,43 @@ export async function clearTelegramPreview<
|
|
|
595
574
|
>(
|
|
596
575
|
chatId: number,
|
|
597
576
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
577
|
+
options: { awaitFlush?: boolean } = {},
|
|
598
578
|
): Promise<void> {
|
|
599
|
-
void chatId;
|
|
600
579
|
const state = deps.getState();
|
|
601
580
|
if (!state) return;
|
|
602
581
|
deps.clearScheduledFlush(state);
|
|
582
|
+
if (state.flushPromise && options.awaitFlush !== false) {
|
|
583
|
+
state.flushRequested = false;
|
|
584
|
+
await state.flushPromise.catch(() => {});
|
|
585
|
+
if (deps.getState() !== state) return;
|
|
586
|
+
}
|
|
603
587
|
deps.setState(undefined);
|
|
588
|
+
if (state.mode === "draft" && state.draftId !== undefined) {
|
|
589
|
+
try {
|
|
590
|
+
await deps.sendDraft(chatId, state.draftId, undefined);
|
|
591
|
+
} catch (error) {
|
|
592
|
+
deps.recordRuntimeEvent?.("preview", error, {
|
|
593
|
+
phase: "clear-draft",
|
|
594
|
+
chatId,
|
|
595
|
+
draftId: state.draftId,
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function buildTelegramNativeMarkdownPreviewSnapshot(
|
|
602
|
+
state: TelegramPreviewState,
|
|
603
|
+
maxMessageLength: number,
|
|
604
|
+
): TelegramNativeMarkdownPreviewSnapshot | undefined {
|
|
605
|
+
const sourceText = state.pendingText.trim();
|
|
606
|
+
if (!sourceText || sourceText === state.lastSentText) return undefined;
|
|
607
|
+
return {
|
|
608
|
+
text:
|
|
609
|
+
sourceText.length > maxMessageLength
|
|
610
|
+
? sourceText.slice(0, maxMessageLength)
|
|
611
|
+
: sourceText,
|
|
612
|
+
sourceText,
|
|
613
|
+
};
|
|
604
614
|
}
|
|
605
615
|
|
|
606
616
|
async function performTelegramPreviewFlush<
|
|
@@ -611,15 +621,13 @@ async function performTelegramPreviewFlush<
|
|
|
611
621
|
deps: TelegramPreviewRuntimeDeps<TReplyMarkup>,
|
|
612
622
|
): Promise<void> {
|
|
613
623
|
if (deps.canSend && !deps.canSend()) {
|
|
614
|
-
await clearTelegramPreview(chatId, deps);
|
|
624
|
+
await clearTelegramPreview(chatId, deps, { awaitFlush: false });
|
|
615
625
|
return;
|
|
616
626
|
}
|
|
617
|
-
const snapshot =
|
|
627
|
+
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
|
|
618
628
|
state,
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
renderTelegramMessage: deps.renderTelegramMessage,
|
|
622
|
-
});
|
|
629
|
+
deps.maxMessageLength,
|
|
630
|
+
);
|
|
623
631
|
if (!snapshot) return;
|
|
624
632
|
if (
|
|
625
633
|
shouldUseTelegramDraftPreview({
|
|
@@ -630,35 +638,32 @@ async function performTelegramPreviewFlush<
|
|
|
630
638
|
const draftId = state.draftId ?? deps.allocateDraftId();
|
|
631
639
|
state.draftId = draftId;
|
|
632
640
|
try {
|
|
633
|
-
await deps.sendDraft(
|
|
641
|
+
await deps.sendDraft(
|
|
642
|
+
chatId,
|
|
643
|
+
draftId,
|
|
644
|
+
normalizeTelegramNativeMarkdown(snapshot.sourceText),
|
|
645
|
+
);
|
|
634
646
|
deps.setDraftSupport("supported");
|
|
635
647
|
state.mode = "draft";
|
|
636
648
|
state.lastSentText = snapshot.text;
|
|
637
|
-
state.lastSentParseMode =
|
|
638
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
649
|
+
state.lastSentParseMode = undefined;
|
|
639
650
|
return;
|
|
640
651
|
} catch {
|
|
641
652
|
deps.setDraftSupport("unsupported");
|
|
642
653
|
}
|
|
643
654
|
}
|
|
644
655
|
if (state.messageId === undefined) {
|
|
645
|
-
const sent = await deps.sendMessage(chatId, snapshot.text,
|
|
646
|
-
parseMode: snapshot.parseMode,
|
|
647
|
-
});
|
|
656
|
+
const sent = await deps.sendMessage(chatId, snapshot.text, undefined);
|
|
648
657
|
state.messageId = sent.message_id;
|
|
649
658
|
state.mode = "message";
|
|
650
659
|
state.lastSentText = snapshot.text;
|
|
651
|
-
state.lastSentParseMode =
|
|
652
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
660
|
+
state.lastSentParseMode = undefined;
|
|
653
661
|
return;
|
|
654
662
|
}
|
|
655
|
-
await deps.editMessageText(chatId, state.messageId, snapshot.text,
|
|
656
|
-
parseMode: snapshot.parseMode,
|
|
657
|
-
});
|
|
663
|
+
await deps.editMessageText(chatId, state.messageId, snapshot.text, undefined);
|
|
658
664
|
state.mode = "message";
|
|
659
665
|
state.lastSentText = snapshot.text;
|
|
660
|
-
state.lastSentParseMode =
|
|
661
|
-
state.lastSentStrategy = snapshot.strategy;
|
|
666
|
+
state.lastSentParseMode = undefined;
|
|
662
667
|
}
|
|
663
668
|
|
|
664
669
|
export async function flushTelegramPreview<
|
|
@@ -719,49 +724,9 @@ export async function finalizeTelegramPreview<
|
|
|
719
724
|
}
|
|
720
725
|
if (state.mode === "draft") {
|
|
721
726
|
await deps.sendMessage(chatId, finalText);
|
|
722
|
-
|
|
727
|
+
deps.setState(undefined);
|
|
723
728
|
return true;
|
|
724
729
|
}
|
|
725
730
|
deps.setState(undefined);
|
|
726
731
|
return state.messageId !== undefined;
|
|
727
732
|
}
|
|
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
|
-
-
|
|
33
|
-
-
|
|
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
|
+
- Use inline code for short copyable literals (commands, paths, IDs, symbols); avoid wide monospace blocks unless 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.
|