Files
oh-my-pi-telegram/lib/preview.ts
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

847 lines
24 KiB
TypeScript

/**
* Telegram preview streaming helpers
* Zones: telegram outbound, native rich markdown drafts
* Owns safe draft preview selection, runtime updates, and preview finalization
*/
import { normalizeTelegramNativeMarkdown } from "./replies.ts";
import { stripTelegramCommentMarkupForPreview } from "./outbound.ts";
import {
getTelegramTargetThreadParams,
type TelegramTarget,
} from "./target.ts";
import { shouldSuppressPreviewForVoice } from "./voice.ts";
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
const TELEGRAM_DRAFT_PREVIEW_MAX_CHARS = 4096;
export type TelegramDraftSupport = "unknown" | "supported";
export interface TelegramPreviewState {
mode: "draft";
draftId?: number;
pendingText: string;
lastSentText: string;
}
export interface TelegramPreviewRuntimeState extends TelegramPreviewState {
flushPromise?: Promise<void>;
flushRequested?: boolean;
}
export type TelegramPreviewReplyMarkup = unknown;
export interface TelegramPreviewRuntimeDeps {
getState: () => TelegramPreviewRuntimeState | undefined;
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
maxMessageLength: number;
getDraftSupport: () => TelegramDraftSupport;
setDraftSupport: (support: TelegramDraftSupport) => void;
allocateDraftId: () => number;
sendDraft: (
chatId: number,
draftId: number,
text?: string,
options?: {
parse_mode?: string;
entities?: unknown[];
message_thread_id?: number;
},
) => Promise<unknown>;
canSend?: () => boolean;
recordRuntimeEvent?: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
}
export interface TelegramPreviewActiveTurn {
chatId: number;
replyToMessageId?: number;
target?: TelegramTarget;
voiceReplyPreferred?: boolean;
voiceReplyRequired?: boolean;
}
export interface TelegramAssistantMessagePreviewStartDeps<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
> {
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
isAssistantMessage: (message: TMessage) => boolean;
getState: () => TelegramPreviewRuntimeState | undefined;
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
createPreviewState: () => TelegramPreviewRuntimeState;
canSend?: () => boolean;
finalizePreview: (chatId: number) => Promise<boolean>;
finalizeMarkdownPreview: (
chatId: number,
markdown: string,
replyToMessageId?: number,
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
) => Promise<boolean>;
}
export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
isAssistantMessage: (message: TMessage) => boolean;
getState: () => TelegramPreviewRuntimeState | undefined;
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
createPreviewState: () => TelegramPreviewRuntimeState;
canSend?: () => boolean;
getMessageText: (message: TMessage) => string;
schedulePreviewFlush: (
chatId: number,
options?: { target?: TelegramTarget },
) => void;
}
export type TelegramAssistantMessagePreviewHookDeps<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
> = TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup> &
TelegramAssistantMessagePreviewUpdateDeps<TMessage>;
export interface TelegramAssistantMessagePreviewHookEvent<TMessage> {
message: TMessage;
}
export interface TelegramAssistantMessagePreviewHooks<TMessage> {
onMessageStart: (
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
) => Promise<void>;
onMessageUpdate: (
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
) => Promise<void>;
}
export interface TelegramPreviewControllerDeps {
getDefaultReplyToMessageId?: () => number | undefined;
maxMessageLength?: number;
initialDraftSupport?: TelegramDraftSupport;
sendDraft: (
chatId: number,
draftId: number,
text?: string,
options?: {
parse_mode?: string;
entities?: unknown[];
message_thread_id?: number;
},
) => Promise<unknown>;
canSend?: () => boolean;
maxDraftId?: number;
recordRuntimeEvent?: (
category: string,
error: unknown,
details?: Record<string, unknown>,
) => void;
}
export interface TelegramPreviewController {
getState: () => TelegramPreviewRuntimeState | undefined;
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
setPendingText: (text: string) => void;
createState: () => TelegramPreviewRuntimeState;
resetState: () => void;
invalidate: () => void;
clear: (
chatId: number,
options?: { awaitFlush?: boolean; target?: TelegramTarget },
) => Promise<void>;
flush: (
chatId: number,
options?: { target?: TelegramTarget },
) => Promise<void>;
scheduleFlush: (
chatId: number,
options?: { target?: TelegramTarget },
) => void;
finalize: (
chatId: number,
replyToMessageId?: number,
options?: { target?: TelegramTarget },
) => Promise<boolean>;
}
export type TelegramPreviewControllerRuntimeDeps =
TelegramPreviewControllerDeps;
export function createTelegramPreviewControllerRuntime(
deps: TelegramPreviewControllerRuntimeDeps,
): TelegramPreviewController {
return createTelegramPreviewController({
getDefaultReplyToMessageId: deps.getDefaultReplyToMessageId,
maxMessageLength: deps.maxMessageLength,
initialDraftSupport: deps.initialDraftSupport,
sendDraft: deps.sendDraft,
canSend: deps.canSend,
maxDraftId: deps.maxDraftId,
recordRuntimeEvent: deps.recordRuntimeEvent,
});
}
export interface TelegramAssistantPreviewRuntimeDeps<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
> extends TelegramPreviewControllerRuntimeDeps {
getActiveTurn: () => TelegramPreviewActiveTurn | undefined;
isAssistantMessage: (message: TMessage) => boolean;
getMessageText: (message: TMessage) => string;
sendMarkdownReply: (
chatId: number,
replyToMessageId: number | undefined,
markdown: string,
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
) => Promise<number | undefined>;
}
export type TelegramAssistantPreviewRuntime<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
> = TelegramPreviewController &
TelegramAssistantMessagePreviewHooks<TMessage> & {
finalizeMarkdown: (
chatId: number,
markdown: string,
replyToMessageId?: number,
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
) => Promise<boolean>;
};
export function createTelegramNativeMarkdownPreviewFinalizer<
TReplyMarkup,
>(deps: {
getState: () => TelegramPreviewRuntimeState | undefined;
clear: (
chatId: number,
options?: { awaitFlush?: boolean; target?: TelegramTarget },
) => Promise<void>;
discard?: () => void;
sendMarkdownReply: (
chatId: number,
replyToMessageId: number | undefined,
markdown: string,
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
) => Promise<number | undefined>;
}): (
chatId: number,
markdown: string,
replyToMessageId?: number,
options?: { replyMarkup?: TReplyMarkup; target?: TelegramTarget },
) => Promise<boolean> {
return async (chatId, markdown, replyToMessageId, options) => {
const state = deps.getState();
if (state?.flushPromise) {
await state.flushPromise.catch(() => {});
if (deps.getState() !== state) return false;
}
await deps.sendMarkdownReply(chatId, replyToMessageId, markdown, options);
if (deps.getState() === state) deps.discard?.();
return true;
};
}
export function createTelegramAssistantPreviewRuntime<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
>(
deps: TelegramAssistantPreviewRuntimeDeps<TMessage, TReplyMarkup>,
): TelegramAssistantPreviewRuntime<TMessage, TReplyMarkup> {
const controller = createTelegramPreviewControllerRuntime(deps);
const finalizeMarkdownPreview = createTelegramNativeMarkdownPreviewFinalizer({
getState: controller.getState,
clear: controller.clear,
discard: () => controller.setState(undefined),
sendMarkdownReply: deps.sendMarkdownReply,
});
return {
...controller,
finalizeMarkdown: finalizeMarkdownPreview,
...createTelegramAssistantMessagePreviewHooks({
getActiveTurn: deps.getActiveTurn,
isAssistantMessage: deps.isAssistantMessage,
getState: controller.getState,
setState: controller.setState,
createPreviewState: controller.createState,
canSend: deps.canSend,
finalizePreview: controller.finalize,
finalizeMarkdownPreview,
getMessageText: deps.getMessageText,
schedulePreviewFlush: controller.scheduleFlush,
}),
};
}
export function createTelegramPreviewController(
deps: TelegramPreviewControllerDeps,
): TelegramPreviewController {
let state: TelegramPreviewRuntimeState | undefined;
let generation = 0;
const maxDraftId = deps.maxDraftId ?? TELEGRAM_DRAFT_ID_MAX;
const maxMessageLength =
deps.maxMessageLength ?? TELEGRAM_DRAFT_PREVIEW_MAX_CHARS;
let draftSupport = deps.initialDraftSupport ?? "unknown";
let nextDraftId = 0;
const getRuntimeDeps = (
operationGeneration = generation,
): TelegramPreviewRuntimeDeps => ({
getState: () => state,
setState: (nextState) => {
state = nextState;
},
maxMessageLength,
getDraftSupport: () => draftSupport,
setDraftSupport: (support) => {
draftSupport = support;
},
allocateDraftId: () => {
nextDraftId = allocateTelegramDraftId(nextDraftId, maxDraftId);
return nextDraftId;
},
sendDraft: deps.sendDraft,
canSend: () =>
operationGeneration === generation && (deps.canSend?.() ?? true),
recordRuntimeEvent: deps.recordRuntimeEvent,
});
return {
getState: () => state,
setState: (nextState) => {
state = nextState;
},
setPendingText: (text) => {
if (state) state.pendingText = text;
},
createState: () => createTelegramPreviewRuntimeState(),
resetState: () => {
generation += 1;
state = createTelegramPreviewRuntimeState();
},
invalidate: () => {
generation += 1;
state = undefined;
},
clear: (chatId, options) =>
clearTelegramPreview(chatId, getRuntimeDeps(), options),
flush: (chatId, options) =>
flushTelegramPreview(chatId, getRuntimeDeps(), options),
scheduleFlush: (chatId, options) => {
if (!state) return;
void flushTelegramPreview(chatId, getRuntimeDeps(), options);
},
finalize: (chatId, _replyToMessageId, options) =>
finalizeTelegramPreview(chatId, getRuntimeDeps(), options),
};
}
export function createTelegramAssistantMessagePreviewHooks<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
>(
deps: TelegramAssistantMessagePreviewHookDeps<TMessage, TReplyMarkup>,
): TelegramAssistantMessagePreviewHooks<TMessage> {
return {
onMessageStart: async (
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
): Promise<void> => {
await handleTelegramAssistantMessagePreviewStart(event.message, deps);
},
onMessageUpdate: async (
event: TelegramAssistantMessagePreviewHookEvent<TMessage>,
): Promise<void> => {
await handleTelegramAssistantMessagePreviewUpdate(event.message, deps);
},
};
}
export async function handleTelegramAssistantMessagePreviewStart<
TMessage,
TReplyMarkup = TelegramPreviewReplyMarkup,
>(
message: TMessage,
deps: TelegramAssistantMessagePreviewStartDeps<TMessage, TReplyMarkup>,
): Promise<void> {
const turn = deps.getActiveTurn();
if (!turn || !deps.isAssistantMessage(message)) return;
if (deps.canSend && !deps.canSend()) {
deps.setState(undefined);
return;
}
if (shouldSuppressPreviewForVoice(turn)) {
deps.setState(undefined);
return;
}
const state = deps.getState();
if (
state &&
(state.pendingText.trim().length > 0 ||
state.lastSentText.trim().length > 0)
) {
const previousText = state.pendingText.trim();
if (previousText.length > 0) {
await deps.finalizeMarkdownPreview(
turn.chatId,
previousText,
turn.replyToMessageId,
{
target: turn.target,
},
);
} else {
await deps.finalizePreview(turn.chatId);
}
}
deps.setState(deps.createPreviewState());
}
export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
message: TMessage,
deps: TelegramAssistantMessagePreviewUpdateDeps<TMessage>,
): Promise<void> {
const turn = deps.getActiveTurn();
if (!turn || !deps.isAssistantMessage(message)) return;
if (deps.canSend && !deps.canSend()) {
deps.setState(undefined);
return;
}
if (shouldSuppressPreviewForVoice(turn)) return;
let state = deps.getState();
if (!state) {
state = deps.createPreviewState();
deps.setState(state);
}
state.pendingText = stripTelegramCommentMarkupForPreview(
deps.getMessageText(message),
);
deps.schedulePreviewFlush(turn.chatId, { target: turn.target });
}
export function buildTelegramPreviewFinalText(
state: TelegramPreviewState,
): string | undefined {
const finalText = state.pendingText.trim();
if (finalText) return finalText;
return state.lastSentText.trim() || undefined;
}
export function createTelegramPreviewRuntimeState(): TelegramPreviewRuntimeState {
return {
mode: "draft",
pendingText: "",
lastSentText: "",
};
}
export function allocateTelegramDraftId(
currentDraftId: number,
maxDraftId: number,
): number {
return currentDraftId >= maxDraftId ? 1 : currentDraftId + 1;
}
interface TelegramNativeMarkdownPreviewSnapshot {
text: string;
}
export function shouldUseTelegramDraftPreview(_options: {
draftSupport: TelegramDraftSupport;
snapshot?: TelegramNativeMarkdownPreviewSnapshot;
}): boolean {
return true;
}
export async function clearTelegramPreview(
chatId: number,
deps: TelegramPreviewRuntimeDeps,
options: { awaitFlush?: boolean; target?: TelegramTarget } = {},
): Promise<void> {
const state = deps.getState();
if (!state) return;
if (state.flushPromise && options.awaitFlush !== false) {
state.flushRequested = false;
await state.flushPromise.catch(() => {});
if (deps.getState() !== state) return;
}
deps.setState(undefined);
if (state.mode === "draft" && state.draftId !== undefined) {
try {
await deps.sendDraft(chatId, state.draftId, undefined, {
...getTelegramTargetThreadParams(options.target ?? { chatId }),
});
} catch (error) {
deps.recordRuntimeEvent?.("preview", error, {
phase: "clear-draft",
chatId,
draftId: state.draftId,
});
}
}
}
interface TelegramDraftInlineState {
codeTicks: number;
htmlComment: boolean;
displayMath: boolean;
fence?: { marker: "`" | "~"; length: number };
strongAsterisk: boolean;
emphasisAsterisk: boolean;
strongUnderscore: boolean;
emphasisUnderscore: boolean;
strike: boolean;
linkText: boolean;
linkDestination: boolean;
}
function createTelegramDraftInlineState(): TelegramDraftInlineState {
return {
codeTicks: 0,
htmlComment: false,
displayMath: false,
strongAsterisk: false,
emphasisAsterisk: false,
strongUnderscore: false,
emphasisUnderscore: false,
strike: false,
linkText: false,
linkDestination: false,
};
}
function isTelegramDraftInlineStateClosed(
state: TelegramDraftInlineState,
): boolean {
return (
state.codeTicks === 0 &&
!state.htmlComment &&
!state.displayMath &&
!state.fence &&
!state.strongAsterisk &&
!state.emphasisAsterisk &&
!state.strongUnderscore &&
!state.emphasisUnderscore &&
!state.strike &&
!state.linkText &&
!state.linkDestination
);
}
function countRepeatedChars(text: string, index: number, char: string): number {
let count = 0;
while (text[index + count] === char) count += 1;
return count;
}
function isEscapedMarkdownChar(text: string, index: number): boolean {
let slashCount = 0;
for (
let cursor = index - 1;
cursor >= 0 && text[cursor] === "\\";
cursor -= 1
) {
slashCount += 1;
}
return slashCount % 2 === 1;
}
function isInlineDelimiterCandidate(
text: string,
index: number,
length: number,
): boolean {
const previous = text[index - 1] ?? "";
const next = text[index + length] ?? "";
if (!next || /\s/.test(next))
return previous.length > 0 && !/\s/.test(previous);
if (!previous || /\s/.test(previous)) return true;
return /[\p{P}\p{S}]/u.test(previous) || /[\p{P}\p{S}]/u.test(next);
}
function updateTelegramDraftInlineStateForLine(
line: string,
state: TelegramDraftInlineState,
): void {
if (state.fence || state.displayMath) return;
for (let index = 0; index < line.length; index += 1) {
if (state.htmlComment) {
const closeIndex = line.indexOf("-->", index);
if (closeIndex === -1) return;
state.htmlComment = false;
index = closeIndex + 2;
continue;
}
if (state.codeTicks > 0) {
const ticks = countRepeatedChars(line, index, "`");
if (ticks >= state.codeTicks) {
state.codeTicks = 0;
index += ticks - 1;
}
continue;
}
if (isEscapedMarkdownChar(line, index)) continue;
if (line.startsWith("<!--", index)) {
const closeIndex = line.indexOf("-->", index + 4);
if (closeIndex === -1) {
state.htmlComment = true;
return;
}
index = closeIndex + 2;
continue;
}
const ticks = countRepeatedChars(line, index, "`");
if (ticks > 0) {
state.codeTicks = ticks;
index += ticks - 1;
continue;
}
if (line.startsWith("][", index) || line.startsWith("](", index)) {
state.linkText = false;
state.linkDestination = true;
index += 1;
continue;
}
if (line[index] === "[" && !state.linkDestination) {
state.linkText = true;
continue;
}
if (line[index] === ")" && state.linkDestination) {
state.linkDestination = false;
continue;
}
if (
line.startsWith("~~", index) &&
isInlineDelimiterCandidate(line, index, 2)
) {
state.strike = !state.strike;
index += 1;
continue;
}
if (
line.startsWith("**", index) &&
isInlineDelimiterCandidate(line, index, 2)
) {
state.strongAsterisk = !state.strongAsterisk;
index += 1;
continue;
}
if (line[index] === "*" && isInlineDelimiterCandidate(line, index, 1)) {
state.emphasisAsterisk = !state.emphasisAsterisk;
continue;
}
if (
line.startsWith("__", index) &&
isInlineDelimiterCandidate(line, index, 2)
) {
state.strongUnderscore = !state.strongUnderscore;
index += 1;
continue;
}
if (line[index] === "_" && isInlineDelimiterCandidate(line, index, 1)) {
state.emphasisUnderscore = !state.emphasisUnderscore;
}
}
}
function updateTelegramDraftBlockStateForLine(
line: string,
state: TelegramDraftInlineState,
): boolean {
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
if (state.fence) {
if (
new RegExp(
`^ {0,3}${state.fence.marker}{${state.fence.length},}\\s*$`,
).test(line)
) {
state.fence = undefined;
}
return true;
}
if (state.displayMath) {
if (line.trim() === "$$") state.displayMath = false;
return true;
}
if (fenceMatch) {
const markerText = fenceMatch[1] ?? "```";
state.fence = {
marker: markerText[0] as "`" | "~",
length: markerText.length,
};
return true;
}
if (line.trim() === "$$") {
state.displayMath = true;
return true;
}
return false;
}
function findSafeTelegramRichMarkdownDraftEnd(markdown: string): number {
const state = createTelegramDraftInlineState();
let offset = 0;
let safeEnd = 0;
for (const line of markdown.split("\n")) {
const lineEnd = offset + line.length;
const consumedAsBlock = updateTelegramDraftBlockStateForLine(line, state);
if (!consumedAsBlock) updateTelegramDraftInlineStateForLine(line, state);
const nextOffset = lineEnd + 1;
if (isTelegramDraftInlineStateClosed(state)) safeEnd = lineEnd;
offset = nextOffset;
}
if (isTelegramDraftInlineStateClosed(state)) return markdown.length;
return safeEnd;
}
function hasTelegramPreviewVisibleContent(markdown: string): boolean {
return /[\p{L}\p{N}]/u.test(markdown);
}
export function getSafeTelegramRichMarkdownDraftPrefix(
markdown: string,
maxMessageLength: number,
): string | undefined {
const source = markdown.trim();
if (!source) return undefined;
const limited =
source.length > maxMessageLength
? source.slice(0, maxMessageLength)
: source;
const safeEnd = findSafeTelegramRichMarkdownDraftEnd(limited);
if (safeEnd > 0) {
const safePrefix = limited.slice(0, safeEnd).trimEnd();
return hasTelegramPreviewVisibleContent(safePrefix)
? safePrefix
: undefined;
}
let candidateEnd = limited.length;
while (candidateEnd > 0) {
candidateEnd = limited.lastIndexOf(" ", candidateEnd - 1);
if (candidateEnd <= 0) return undefined;
const candidate = limited.slice(0, candidateEnd).trimEnd();
if (
hasTelegramPreviewVisibleContent(candidate) &&
findSafeTelegramRichMarkdownDraftEnd(candidate) === candidate.length
) {
return candidate || undefined;
}
}
return undefined;
}
function buildTelegramNativeMarkdownPreviewSnapshot(
state: TelegramPreviewState,
maxMessageLength: number,
): TelegramNativeMarkdownPreviewSnapshot | undefined {
const safeText = getSafeTelegramRichMarkdownDraftPrefix(
state.pendingText,
maxMessageLength,
);
if (!safeText || safeText === state.lastSentText) return undefined;
return { text: safeText };
}
async function performTelegramPreviewFlush(
chatId: number,
state: TelegramPreviewRuntimeState,
deps: TelegramPreviewRuntimeDeps,
options: { target?: TelegramTarget } = {},
): Promise<void> {
if (deps.canSend && !deps.canSend()) {
await clearTelegramPreview(chatId, deps, {
awaitFlush: false,
target: options.target,
});
return;
}
const snapshot = buildTelegramNativeMarkdownPreviewSnapshot(
state,
deps.maxMessageLength,
);
if (!snapshot) return;
if (
shouldUseTelegramDraftPreview({
draftSupport: deps.getDraftSupport(),
snapshot,
})
) {
const draftId = state.draftId ?? deps.allocateDraftId();
state.draftId = draftId;
try {
await deps.sendDraft(
chatId,
draftId,
normalizeTelegramNativeMarkdown(snapshot.text),
{ ...getTelegramTargetThreadParams(options.target ?? { chatId }) },
);
deps.setDraftSupport("supported");
state.mode = "draft";
state.lastSentText = snapshot.text;
return;
} catch (error) {
deps.recordRuntimeEvent?.("preview", error, {
phase: "draft",
chatId,
draftId,
});
return;
}
}
}
export async function flushTelegramPreview(
chatId: number,
deps: TelegramPreviewRuntimeDeps,
options: { target?: TelegramTarget } = {},
): Promise<void> {
const state = deps.getState();
if (!state) return;
if (state.flushPromise) {
state.flushRequested = true;
await state.flushPromise;
return;
}
state.flushPromise = (async () => {
do {
state.flushRequested = false;
try {
await performTelegramPreviewFlush(chatId, state, deps, options);
} catch (error) {
deps.recordRuntimeEvent?.("preview", error, {
phase: "flush",
chatId,
draftId: state.draftId,
});
break;
}
} while (deps.getState() === state && state.flushRequested);
})();
try {
await state.flushPromise;
} finally {
if (deps.getState() === state) {
state.flushPromise = undefined;
}
}
}
export async function finalizeTelegramPreview(
chatId: number,
deps: TelegramPreviewRuntimeDeps,
options: { target?: TelegramTarget } = {},
): Promise<boolean> {
const state = deps.getState();
if (!state) return false;
if (deps.canSend && !deps.canSend()) {
await clearTelegramPreview(chatId, deps, options);
return false;
}
await flushTelegramPreview(chatId, deps, options);
const finalText = buildTelegramPreviewFinalText(state);
if (!finalText) {
await clearTelegramPreview(chatId, deps, options);
return false;
}
deps.setState(undefined);
return false;
}