mirror of
https://github.com/wassname/pi-telegram.git
synced 2026-06-27 16:46:21 +08:00
tool verbose
This commit is contained in:
@@ -13,7 +13,6 @@ import type {
|
||||
ExtensionAPI,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { SettingsManager } from "@mariozechner/pi-coding-agent";
|
||||
|
||||
import {
|
||||
createTelegramApiClient,
|
||||
@@ -82,8 +81,11 @@ import {
|
||||
} from "./lib/registration.ts";
|
||||
import {
|
||||
MAX_MESSAGE_LENGTH,
|
||||
buildTelegramAssistantPreviewText,
|
||||
buildTelegramAssistantTranscriptMarkdown,
|
||||
renderMarkdownPreviewText,
|
||||
renderTelegramMessage,
|
||||
type TelegramAssistantDisplayBlock,
|
||||
type TelegramRenderMode,
|
||||
} from "./lib/rendering.ts";
|
||||
import {
|
||||
@@ -399,6 +401,9 @@ export default function (pi: ExtensionAPI) {
|
||||
let compactionInProgress = false;
|
||||
let setupInProgress = false;
|
||||
let previewState: TelegramPreviewState | undefined;
|
||||
let traceVisible = true;
|
||||
let activeTelegramTraceBlocks: TelegramAssistantDisplayBlock[] = [];
|
||||
let activeTelegramMessageBlocks: TelegramAssistantDisplayBlock[] = [];
|
||||
let draftSupport: "unknown" | "supported" | "unsupported" = "unknown";
|
||||
let nextDraftId = 0;
|
||||
let currentTelegramModel: Model<any> | undefined;
|
||||
@@ -611,17 +616,74 @@ export default function (pi: ExtensionAPI) {
|
||||
return (message as unknown as { role?: string }).role === "assistant";
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
function stringifyToolArgs(args: unknown): string | undefined {
|
||||
if (args === undefined) return undefined;
|
||||
if (typeof args === "string") return args.trim() || undefined;
|
||||
const encoded = JSON.stringify(args, null, 2);
|
||||
return encoded?.trim() || undefined;
|
||||
}
|
||||
|
||||
function normalizeAssistantDisplayBlock(
|
||||
block: unknown,
|
||||
): TelegramAssistantDisplayBlock | undefined {
|
||||
if (typeof block !== "object" || block === null || !("type" in block)) {
|
||||
return undefined;
|
||||
}
|
||||
const candidate = block as Record<string, unknown>;
|
||||
if (candidate.type === "text" && typeof candidate.text === "string") {
|
||||
return { type: "text", text: candidate.text };
|
||||
}
|
||||
if (candidate.type === "thinking") {
|
||||
const text =
|
||||
typeof candidate.text === "string"
|
||||
? candidate.text
|
||||
: typeof candidate.thinking === "string"
|
||||
? candidate.thinking
|
||||
: undefined;
|
||||
if (!text) return undefined;
|
||||
return { type: "thinking", text };
|
||||
}
|
||||
if (candidate.type === "tool_call" || candidate.type === "tool_use") {
|
||||
const name =
|
||||
typeof candidate.name === "string"
|
||||
? candidate.name
|
||||
: typeof candidate.tool === "string"
|
||||
? candidate.tool
|
||||
: undefined;
|
||||
if (!name) return undefined;
|
||||
return {
|
||||
type: "tool_call",
|
||||
name,
|
||||
argsText: stringifyToolArgs(
|
||||
"input" in candidate
|
||||
? candidate.input
|
||||
: "arguments" in candidate
|
||||
? candidate.arguments
|
||||
: "args" in candidate
|
||||
? candidate.args
|
||||
: undefined,
|
||||
),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function extractAssistantDisplayBlocks(
|
||||
content: unknown,
|
||||
): TelegramAssistantDisplayBlock[] {
|
||||
const blocks = Array.isArray(content) ? content : [];
|
||||
return blocks
|
||||
.map(normalizeAssistantDisplayBlock)
|
||||
.filter((block): block is TelegramAssistantDisplayBlock => !!block);
|
||||
}
|
||||
|
||||
function extractTextContent(content: unknown): string {
|
||||
return extractAssistantDisplayBlocks(content)
|
||||
.filter(
|
||||
(block): block is { type: string; text?: string } =>
|
||||
typeof block === "object" && block !== null && "type" in block,
|
||||
(block): block is Extract<TelegramAssistantDisplayBlock, { type: "text" }> =>
|
||||
block.type === "text",
|
||||
)
|
||||
.filter(
|
||||
(block) => block.type === "text" && typeof block.text === "string",
|
||||
)
|
||||
.map((block) => block.text as string)
|
||||
.map((block) => block.text)
|
||||
.join("")
|
||||
.trim();
|
||||
}
|
||||
@@ -632,6 +694,69 @@ export default function (pi: ExtensionAPI) {
|
||||
);
|
||||
}
|
||||
|
||||
function getMessageBlocks(message: AgentMessage): TelegramAssistantDisplayBlock[] {
|
||||
return extractAssistantDisplayBlocks(
|
||||
(message as unknown as Record<string, unknown>).content,
|
||||
);
|
||||
}
|
||||
|
||||
function getActiveTracePreviewBlocks(): TelegramAssistantDisplayBlock[] {
|
||||
return [...activeTelegramTraceBlocks, ...activeTelegramMessageBlocks];
|
||||
}
|
||||
|
||||
function extractAssistantTurn(messages: AgentMessage[]): {
|
||||
blocks: TelegramAssistantDisplayBlock[];
|
||||
text?: string;
|
||||
stopReason?: string;
|
||||
errorMessage?: string;
|
||||
} {
|
||||
const blocks: TelegramAssistantDisplayBlock[] = [];
|
||||
let text: string | undefined;
|
||||
let stopReason: string | undefined;
|
||||
let errorMessage: string | undefined;
|
||||
for (const next of messages) {
|
||||
const message = next as unknown as Record<string, unknown>;
|
||||
if (message.role !== "assistant") continue;
|
||||
const nextBlocks = extractAssistantDisplayBlocks(message.content);
|
||||
blocks.push(...nextBlocks);
|
||||
const nextText = extractTextContent(message.content);
|
||||
if (nextText) {
|
||||
text = nextText;
|
||||
}
|
||||
stopReason =
|
||||
typeof message.stopReason === "string" ? message.stopReason : stopReason;
|
||||
errorMessage =
|
||||
typeof message.errorMessage === "string"
|
||||
? message.errorMessage
|
||||
: errorMessage;
|
||||
}
|
||||
return { blocks, text, stopReason, errorMessage };
|
||||
}
|
||||
|
||||
async function refreshOpenStatusMenus(ctx: ExtensionContext): Promise<void> {
|
||||
for (const state of modelMenus.values()) {
|
||||
if (state.mode !== "status") continue;
|
||||
await showStatusMessage(state, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
function setTraceVisible(nextTraceVisible: boolean, ctx: ExtensionContext): void {
|
||||
traceVisible = nextTraceVisible;
|
||||
if (activeTelegramTurn && previewState) {
|
||||
previewState.pendingText = buildTelegramAssistantPreviewText(
|
||||
getActiveTracePreviewBlocks(),
|
||||
nextTraceVisible,
|
||||
);
|
||||
if (previewState.pendingText.trim().length > 0) {
|
||||
schedulePreviewFlush(activeTelegramTurn.chatId);
|
||||
} else {
|
||||
void clearPreview(activeTelegramTurn.chatId);
|
||||
}
|
||||
}
|
||||
updateStatus(ctx);
|
||||
void refreshOpenStatusMenus(ctx);
|
||||
}
|
||||
|
||||
function createPreviewState(): TelegramPreviewState {
|
||||
return {
|
||||
mode: draftSupport === "unsupported" ? "message" : "draft",
|
||||
@@ -796,24 +921,13 @@ export default function (pi: ExtensionAPI) {
|
||||
});
|
||||
}
|
||||
|
||||
function extractAssistantText(messages: AgentMessage[]): {
|
||||
function extractAssistantSummary(messages: AgentMessage[]): {
|
||||
blocks: TelegramAssistantDisplayBlock[];
|
||||
text?: string;
|
||||
stopReason?: string;
|
||||
errorMessage?: string;
|
||||
} {
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i] as unknown as Record<string, unknown>;
|
||||
if (message.role !== "assistant") continue;
|
||||
const stopReason =
|
||||
typeof message.stopReason === "string" ? message.stopReason : undefined;
|
||||
const errorMessage =
|
||||
typeof message.errorMessage === "string"
|
||||
? message.errorMessage
|
||||
: undefined;
|
||||
const text = extractTextContent(message.content);
|
||||
return { text: text || undefined, stopReason, errorMessage };
|
||||
}
|
||||
return {};
|
||||
return extractAssistantTurn(messages);
|
||||
}
|
||||
|
||||
// --- Bridge Setup ---
|
||||
@@ -876,6 +990,10 @@ export default function (pi: ExtensionAPI) {
|
||||
command: "status",
|
||||
description: "Show model, usage, cost, and context status",
|
||||
},
|
||||
{
|
||||
command: "trace",
|
||||
description: "Toggle thinking and tool-call visibility",
|
||||
},
|
||||
{ command: "model", description: "Open the interactive model selector" },
|
||||
{ command: "compact", description: "Compact the current pi session" },
|
||||
{ command: "stop", description: "Abort the current pi task" },
|
||||
@@ -895,6 +1013,7 @@ export default function (pi: ExtensionAPI) {
|
||||
chatId: number,
|
||||
ctx: ExtensionContext,
|
||||
): Promise<TelegramModelMenuState> {
|
||||
const { SettingsManager } = await import("@mariozechner/pi-coding-agent");
|
||||
const settingsManager = SettingsManager.create(ctx.cwd);
|
||||
await settingsManager.reload();
|
||||
ctx.modelRegistry.refresh();
|
||||
@@ -981,9 +1100,10 @@ export default function (pi: ExtensionAPI) {
|
||||
): Promise<void> {
|
||||
await updateTelegramStatusMessage(
|
||||
state,
|
||||
buildStatusHtml(ctx, getCurrentTelegramModel(ctx)),
|
||||
buildStatusHtml(ctx, getCurrentTelegramModel(ctx), traceVisible),
|
||||
getCurrentTelegramModel(ctx),
|
||||
pi.getThinkingLevel(),
|
||||
traceVisible,
|
||||
{ editInteractiveMessage, sendInteractiveMessage },
|
||||
);
|
||||
}
|
||||
@@ -1003,9 +1123,10 @@ export default function (pi: ExtensionAPI) {
|
||||
const state = await getModelMenuState(chatId, ctx);
|
||||
const messageId = await sendTelegramStatusMessage(
|
||||
state,
|
||||
buildStatusHtml(ctx, getCurrentTelegramModel(ctx)),
|
||||
buildStatusHtml(ctx, getCurrentTelegramModel(ctx), traceVisible),
|
||||
getCurrentTelegramModel(ctx),
|
||||
pi.getThinkingLevel(),
|
||||
traceVisible,
|
||||
{ editInteractiveMessage, sendInteractiveMessage },
|
||||
);
|
||||
if (messageId === undefined) return;
|
||||
@@ -1165,6 +1286,11 @@ export default function (pi: ExtensionAPI) {
|
||||
updateModelMenuMessage: async () => updateModelMenuMessage(state, ctx),
|
||||
updateThinkingMenuMessage: async () =>
|
||||
updateThinkingMenuMessage(state, ctx),
|
||||
updateStatusMessage: async () => showStatusMessage(state, ctx),
|
||||
setTraceVisible: (nextTraceVisible) => {
|
||||
setTraceVisible(nextTraceVisible, ctx);
|
||||
},
|
||||
getTraceVisible: () => traceVisible,
|
||||
answerCallbackQuery,
|
||||
},
|
||||
);
|
||||
@@ -1542,13 +1668,26 @@ export default function (pi: ExtensionAPI) {
|
||||
);
|
||||
}
|
||||
|
||||
async function handleTraceCommand(
|
||||
message: TelegramMessage,
|
||||
ctx: ExtensionContext,
|
||||
): Promise<void> {
|
||||
const nextTraceVisible = !traceVisible;
|
||||
setTraceVisible(nextTraceVisible, ctx);
|
||||
await sendTextReply(
|
||||
message.chat.id,
|
||||
message.message_id,
|
||||
`Trace visibility: ${nextTraceVisible ? "on" : "off"}.`,
|
||||
);
|
||||
}
|
||||
|
||||
async function handleHelpCommand(
|
||||
message: TelegramMessage,
|
||||
commandName: string,
|
||||
ctx: ExtensionContext,
|
||||
): Promise<void> {
|
||||
let helpText =
|
||||
"Send me a message and I will forward it to pi. Commands: /status, /model, /compact, /stop.";
|
||||
"Send me a message and I will forward it to pi. Commands: /status, /trace, /model, /compact, /stop.";
|
||||
if (commandName === "start") {
|
||||
try {
|
||||
await registerTelegramBotCommands();
|
||||
@@ -1576,6 +1715,7 @@ export default function (pi: ExtensionAPI) {
|
||||
stop: () => handleStopCommand(message, ctx),
|
||||
compact: () => handleCompactCommand(message, ctx),
|
||||
status: () => handleStatusCommand(message, ctx),
|
||||
trace: () => handleTraceCommand(message, ctx),
|
||||
model: () => handleModelCommand(message, ctx),
|
||||
help: () => handleHelpCommand(message, commandName, ctx),
|
||||
start: () => handleHelpCommand(message, commandName, ctx),
|
||||
@@ -1856,6 +1996,8 @@ export default function (pi: ExtensionAPI) {
|
||||
}
|
||||
if (startPlan.activeTurn) {
|
||||
activeTelegramTurn = { ...startPlan.activeTurn };
|
||||
activeTelegramTraceBlocks = [];
|
||||
activeTelegramMessageBlocks = [];
|
||||
previewState = createPreviewState();
|
||||
startTypingLoop(ctx);
|
||||
}
|
||||
@@ -1880,6 +2022,16 @@ export default function (pi: ExtensionAPI) {
|
||||
onMessageStart: async (event, _ctx) => {
|
||||
const nextEvent = event as { message: AgentMessage };
|
||||
if (!activeTelegramTurn || !isAssistantMessage(nextEvent.message)) return;
|
||||
if (traceVisible) {
|
||||
if (activeTelegramMessageBlocks.length > 0) {
|
||||
activeTelegramTraceBlocks.push(...activeTelegramMessageBlocks);
|
||||
activeTelegramMessageBlocks = [];
|
||||
}
|
||||
if (!previewState) {
|
||||
previewState = createPreviewState();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (
|
||||
previewState &&
|
||||
(previewState.pendingText.trim().length > 0 ||
|
||||
@@ -1903,7 +2055,15 @@ export default function (pi: ExtensionAPI) {
|
||||
if (!previewState) {
|
||||
previewState = createPreviewState();
|
||||
}
|
||||
previewState.pendingText = getMessageText(nextEvent.message);
|
||||
if (traceVisible) {
|
||||
activeTelegramMessageBlocks = getMessageBlocks(nextEvent.message);
|
||||
previewState.pendingText = buildTelegramAssistantPreviewText(
|
||||
getActiveTracePreviewBlocks(),
|
||||
true,
|
||||
);
|
||||
} else {
|
||||
previewState.pendingText = getMessageText(nextEvent.message);
|
||||
}
|
||||
schedulePreviewFlush(activeTelegramTurn.chatId);
|
||||
},
|
||||
onAgentEnd: async (event, ctx) => {
|
||||
@@ -1911,14 +2071,18 @@ export default function (pi: ExtensionAPI) {
|
||||
currentAbort = undefined;
|
||||
stopTypingLoop();
|
||||
activeTelegramTurn = undefined;
|
||||
activeTelegramTraceBlocks = [];
|
||||
activeTelegramMessageBlocks = [];
|
||||
activeTelegramToolExecutions = 0;
|
||||
pendingTelegramModelSwitch = undefined;
|
||||
telegramTurnDispatchPending = false;
|
||||
updateStatus(ctx);
|
||||
const assistant = turn
|
||||
? extractAssistantText((event as { messages: AgentMessage[] }).messages)
|
||||
: {};
|
||||
const finalText = assistant.text;
|
||||
? extractAssistantSummary((event as { messages: AgentMessage[] }).messages)
|
||||
: { blocks: [] };
|
||||
const finalText = traceVisible
|
||||
? buildTelegramAssistantTranscriptMarkdown(assistant.blocks, true)
|
||||
: assistant.text;
|
||||
const endPlan = buildTelegramAgentEndPlan({
|
||||
hasTurn: !!turn,
|
||||
stopReason: assistant.stopReason,
|
||||
@@ -1936,12 +2100,31 @@ export default function (pi: ExtensionAPI) {
|
||||
await clearPreview(turn.chatId);
|
||||
}
|
||||
if (endPlan.shouldSendErrorMessage) {
|
||||
await sendTextReply(
|
||||
turn.chatId,
|
||||
turn.replyToMessageId,
|
||||
const errorText =
|
||||
assistant.errorMessage ||
|
||||
"Telegram bridge: pi failed while processing the request.",
|
||||
);
|
||||
"Telegram bridge: pi failed while processing the request.";
|
||||
const errorTranscript = traceVisible && assistant.blocks.length > 0
|
||||
? `${buildTelegramAssistantTranscriptMarkdown(assistant.blocks, true)}\n\n**Error**\n> ${errorText}`
|
||||
: undefined;
|
||||
if (errorTranscript) {
|
||||
if (previewState) {
|
||||
previewState.pendingText = errorTranscript;
|
||||
}
|
||||
const finalized = await finalizeMarkdownPreview(
|
||||
turn.chatId,
|
||||
errorTranscript,
|
||||
);
|
||||
if (!finalized) {
|
||||
await clearPreview(turn.chatId);
|
||||
await sendMarkdownReply(
|
||||
turn.chatId,
|
||||
turn.replyToMessageId,
|
||||
errorTranscript,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await sendTextReply(turn.chatId, turn.replyToMessageId, errorText);
|
||||
}
|
||||
if (endPlan.shouldDispatchNext) {
|
||||
dispatchNextQueuedTelegramTurn(ctx);
|
||||
}
|
||||
|
||||
+39
-3
@@ -62,6 +62,8 @@ export interface TelegramMenuEffectPort {
|
||||
setCurrentModel: (model: Model<any>) => void;
|
||||
setThinkingLevel: (level: ThinkingLevel) => void;
|
||||
getCurrentThinkingLevel: () => ThinkingLevel;
|
||||
setTraceVisible: (traceVisible: boolean) => void;
|
||||
getTraceVisible: () => boolean;
|
||||
stagePendingModelSwitch: (selection: ScopedTelegramModel) => void;
|
||||
restartInterruptedTelegramTurn: (
|
||||
selection: ScopedTelegramModel,
|
||||
@@ -70,7 +72,12 @@ export interface TelegramMenuEffectPort {
|
||||
|
||||
export type TelegramStatusMenuCallbackDeps = Pick<
|
||||
TelegramMenuEffectPort,
|
||||
"updateModelMenuMessage" | "updateThinkingMenuMessage" | "answerCallbackQuery"
|
||||
| "updateModelMenuMessage"
|
||||
| "updateThinkingMenuMessage"
|
||||
| "updateStatusMessage"
|
||||
| "setTraceVisible"
|
||||
| "getTraceVisible"
|
||||
| "answerCallbackQuery"
|
||||
>;
|
||||
|
||||
export type TelegramThinkingMenuCallbackDeps = Pick<
|
||||
@@ -121,7 +128,7 @@ export interface BuildTelegramModelMenuStateParams {
|
||||
|
||||
export type TelegramMenuCallbackAction =
|
||||
| { kind: "ignore" }
|
||||
| { kind: "status"; action: "model" | "thinking" }
|
||||
| { kind: "status"; action: "model" | "thinking" | "trace" }
|
||||
| { kind: "thinking:set"; level: string }
|
||||
| {
|
||||
kind: "model";
|
||||
@@ -451,6 +458,9 @@ export function parseTelegramMenuCallbackAction(
|
||||
if (data === "status:thinking") {
|
||||
return { kind: "status", action: "thinking" };
|
||||
}
|
||||
if (data === "status:trace") {
|
||||
return { kind: "status", action: "trace" };
|
||||
}
|
||||
if (data?.startsWith("thinking:set:")) {
|
||||
return {
|
||||
kind: "thinking:set",
|
||||
@@ -665,6 +675,16 @@ export async function handleTelegramStatusMenuCallbackAction(
|
||||
await deps.answerCallbackQuery(callbackQueryId);
|
||||
return true;
|
||||
}
|
||||
if (action.kind === "status" && action.action === "trace") {
|
||||
const nextTraceVisible = !deps.getTraceVisible();
|
||||
deps.setTraceVisible(nextTraceVisible);
|
||||
await deps.updateStatusMessage();
|
||||
await deps.answerCallbackQuery(
|
||||
callbackQueryId,
|
||||
`Trace: ${nextTraceVisible ? "on" : "off"}`,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (!(action.kind === "status" && action.action === "thinking")) {
|
||||
return false;
|
||||
}
|
||||
@@ -793,6 +813,7 @@ export function buildThinkingMenuReplyMarkup(
|
||||
export function buildStatusReplyMarkup(
|
||||
activeModel: Model<any> | undefined,
|
||||
currentThinkingLevel: ThinkingLevel,
|
||||
traceVisible: boolean,
|
||||
): TelegramReplyMarkup {
|
||||
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
|
||||
rows.push([
|
||||
@@ -804,6 +825,12 @@ export function buildStatusReplyMarkup(
|
||||
callback_data: "status:model",
|
||||
},
|
||||
]);
|
||||
rows.push([
|
||||
{
|
||||
text: formatStatusButtonLabel("Trace", traceVisible ? "on" : "off"),
|
||||
callback_data: "status:trace",
|
||||
},
|
||||
]);
|
||||
if (activeModel?.reasoning) {
|
||||
rows.push([
|
||||
{
|
||||
@@ -847,12 +874,17 @@ export function buildTelegramStatusMenuRenderPayload(
|
||||
statusText: string,
|
||||
activeModel: Model<any> | undefined,
|
||||
currentThinkingLevel: ThinkingLevel,
|
||||
traceVisible: boolean,
|
||||
): TelegramMenuRenderPayload {
|
||||
return {
|
||||
nextMode: "status",
|
||||
text: statusText,
|
||||
mode: "html",
|
||||
replyMarkup: buildStatusReplyMarkup(activeModel, currentThinkingLevel),
|
||||
replyMarkup: buildStatusReplyMarkup(
|
||||
activeModel,
|
||||
currentThinkingLevel,
|
||||
traceVisible,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -897,12 +929,14 @@ export async function updateTelegramStatusMessage(
|
||||
statusText: string,
|
||||
activeModel: Model<any> | undefined,
|
||||
currentThinkingLevel: ThinkingLevel,
|
||||
traceVisible: boolean,
|
||||
deps: TelegramMenuMessageRuntimeDeps,
|
||||
): Promise<void> {
|
||||
const payload = buildTelegramStatusMenuRenderPayload(
|
||||
statusText,
|
||||
activeModel,
|
||||
currentThinkingLevel,
|
||||
traceVisible,
|
||||
);
|
||||
state.mode = payload.nextMode;
|
||||
await deps.editInteractiveMessage(
|
||||
@@ -919,12 +953,14 @@ export async function sendTelegramStatusMessage(
|
||||
statusText: string,
|
||||
activeModel: Model<any> | undefined,
|
||||
currentThinkingLevel: ThinkingLevel,
|
||||
traceVisible: boolean,
|
||||
deps: TelegramMenuMessageRuntimeDeps,
|
||||
): Promise<number | undefined> {
|
||||
const payload = buildTelegramStatusMenuRenderPayload(
|
||||
statusText,
|
||||
activeModel,
|
||||
currentThinkingLevel,
|
||||
traceVisible,
|
||||
);
|
||||
state.mode = payload.nextMode;
|
||||
return deps.sendInteractiveMessage(
|
||||
|
||||
+19
-7
@@ -8,11 +8,28 @@ import type {
|
||||
ExtensionCommandContext,
|
||||
ExtensionContext,
|
||||
} from "@mariozechner/pi-coding-agent";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
|
||||
import { queueTelegramAttachments } from "./attachments.ts";
|
||||
import type { PendingTelegramTurn } from "./queue.ts";
|
||||
|
||||
function buildAttachmentParametersSchema(maxAttachmentsPerTurn: number) {
|
||||
return {
|
||||
type: "object",
|
||||
properties: {
|
||||
paths: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "string",
|
||||
description: "Local file path to attach",
|
||||
},
|
||||
minItems: 1,
|
||||
maxItems: maxAttachmentsPerTurn,
|
||||
},
|
||||
},
|
||||
required: ["paths"],
|
||||
};
|
||||
}
|
||||
|
||||
// --- Tool Registration ---
|
||||
|
||||
export interface TelegramAttachmentToolRegistrationDeps {
|
||||
@@ -34,12 +51,7 @@ export function registerTelegramAttachmentTool(
|
||||
promptGuidelines: [
|
||||
"When handling a [telegram] message and the user asked for a file or generated artifact, call telegram_attach with the local path instead of only mentioning the path in text.",
|
||||
],
|
||||
parameters: Type.Object({
|
||||
paths: Type.Array(
|
||||
Type.String({ description: "Local file path to attach" }),
|
||||
{ minItems: 1, maxItems: deps.maxAttachmentsPerTurn },
|
||||
),
|
||||
}),
|
||||
parameters: buildAttachmentParametersSchema(deps.maxAttachmentsPerTurn),
|
||||
async execute(_toolCallId, params) {
|
||||
return queueTelegramAttachments({
|
||||
activeTurn: deps.getActiveTurn(),
|
||||
|
||||
@@ -5,6 +5,100 @@
|
||||
|
||||
export const MAX_MESSAGE_LENGTH = 4096;
|
||||
|
||||
export type TelegramAssistantDisplayBlock =
|
||||
| { type: "text"; text: string }
|
||||
| { type: "thinking"; text: string }
|
||||
| { type: "tool_call"; name: string; argsText?: string };
|
||||
|
||||
function truncateDisplayText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
|
||||
}
|
||||
|
||||
function normalizePreviewInlineText(text: string): string {
|
||||
return renderMarkdownPreviewText(text).replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
function renderTracePreviewLine(block: TelegramAssistantDisplayBlock): string | undefined {
|
||||
if (block.type === "text") return undefined;
|
||||
if (block.type === "thinking") {
|
||||
const summary = normalizePreviewInlineText(block.text);
|
||||
if (!summary) return undefined;
|
||||
return `[thinking] ${truncateDisplayText(summary, 120)}`;
|
||||
}
|
||||
const parts = [`[tool] ${block.name}`];
|
||||
if (block.argsText?.trim()) {
|
||||
const summary = normalizePreviewInlineText(block.argsText);
|
||||
if (summary) parts.push(summary);
|
||||
}
|
||||
return truncateDisplayText(parts.join(" "), 160);
|
||||
}
|
||||
|
||||
function renderMarkdownQuote(text: string): string {
|
||||
return text
|
||||
.split(/\r?\n/)
|
||||
.map((line) => `> ${line.length > 0 ? line : "\u00A0"}`)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function renderToolArgsMarkdown(argsText: string): string {
|
||||
const trimmed = argsText.trim();
|
||||
if (trimmed.length === 0) return "";
|
||||
if (trimmed.includes("\n") || trimmed.length > 120) {
|
||||
return `\n\n\`\`\`json\n${trimmed}\n\`\`\``;
|
||||
}
|
||||
return ` ${"`"}${trimmed}${"`"}`;
|
||||
}
|
||||
|
||||
export function buildTelegramAssistantPreviewText(
|
||||
blocks: TelegramAssistantDisplayBlock[],
|
||||
traceVisible: boolean,
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
const text = blocks
|
||||
.filter((block): block is Extract<TelegramAssistantDisplayBlock, { type: "text" }> => block.type === "text")
|
||||
.map((block) => block.text)
|
||||
.join("")
|
||||
.trim();
|
||||
if (text) {
|
||||
sections.push(text);
|
||||
}
|
||||
if (traceVisible) {
|
||||
const traceLines = blocks
|
||||
.map(renderTracePreviewLine)
|
||||
.filter((line): line is string => !!line);
|
||||
if (traceLines.length > 0) {
|
||||
sections.push(traceLines.join("\n"));
|
||||
}
|
||||
}
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
|
||||
export function buildTelegramAssistantTranscriptMarkdown(
|
||||
blocks: TelegramAssistantDisplayBlock[],
|
||||
traceVisible: boolean,
|
||||
): string {
|
||||
const sections: string[] = [];
|
||||
for (const block of blocks) {
|
||||
if (block.type === "text") {
|
||||
const trimmed = block.text.trim();
|
||||
if (trimmed) sections.push(trimmed);
|
||||
continue;
|
||||
}
|
||||
if (!traceVisible) continue;
|
||||
if (block.type === "thinking") {
|
||||
const trimmed = block.text.trim();
|
||||
if (!trimmed) continue;
|
||||
sections.push(`**Thinking**\n${renderMarkdownQuote(trimmed)}`);
|
||||
continue;
|
||||
}
|
||||
sections.push(
|
||||
`**Tool call** ${"`"}${block.name}${"`"}${block.argsText ? renderToolArgsMarkdown(block.argsText) : ""}`,
|
||||
);
|
||||
}
|
||||
return sections.join("\n\n").trim();
|
||||
}
|
||||
|
||||
// --- Escaping ---
|
||||
|
||||
function escapeHtml(text: string): string {
|
||||
|
||||
@@ -87,6 +87,7 @@ function buildContextSummary(
|
||||
export function buildStatusHtml(
|
||||
ctx: ExtensionContext,
|
||||
activeModel: Model<any> | undefined,
|
||||
traceVisible: boolean,
|
||||
): string {
|
||||
const stats = collectUsageStats(ctx);
|
||||
const usesSubscription = activeModel
|
||||
@@ -102,6 +103,7 @@ export function buildStatusHtml(
|
||||
lines.push(buildStatusRow("Cost", costSummary));
|
||||
}
|
||||
lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
|
||||
lines.push(buildStatusRow("Trace", traceVisible ? "on" : "off"));
|
||||
if (lines.length === 0) {
|
||||
lines.push(buildStatusRow("Status", "No usage data yet."));
|
||||
}
|
||||
|
||||
+61
-7
@@ -111,6 +111,10 @@ test("Menu helpers build model menu state and parse callback actions", () => {
|
||||
kind: "status",
|
||||
action: "model",
|
||||
});
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction("status:trace"), {
|
||||
kind: "status",
|
||||
action: "trace",
|
||||
});
|
||||
assert.deepEqual(parseTelegramMenuCallbackAction("thinking:set:high"), {
|
||||
kind: "thinking:set",
|
||||
level: "high",
|
||||
@@ -443,6 +447,7 @@ test("Menu helpers execute model callback actions across update, switch, and res
|
||||
|
||||
test("Menu helpers handle status and thinking callback actions", async () => {
|
||||
const events: string[] = [];
|
||||
let traceVisible = true;
|
||||
const reasoningModel = {
|
||||
provider: "openai",
|
||||
id: "gpt-5",
|
||||
@@ -465,6 +470,41 @@ test("Menu helpers handle status and thinking callback actions", async () => {
|
||||
updateThinkingMenuMessage: async () => {
|
||||
events.push("status:thinking");
|
||||
},
|
||||
updateStatusMessage: async () => {
|
||||
events.push("status:update");
|
||||
},
|
||||
setTraceVisible: (nextTraceVisible) => {
|
||||
traceVisible = nextTraceVisible;
|
||||
events.push(`trace:${nextTraceVisible ? "on" : "off"}`);
|
||||
},
|
||||
getTraceVisible: () => traceVisible,
|
||||
answerCallbackQuery: async (_id, text) => {
|
||||
events.push(`answer:${text ?? ""}`);
|
||||
},
|
||||
},
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await handleTelegramStatusMenuCallbackAction(
|
||||
"callback-trace",
|
||||
"status:trace",
|
||||
reasoningModel as never,
|
||||
{
|
||||
updateModelMenuMessage: async () => {
|
||||
events.push("unexpected:model");
|
||||
},
|
||||
updateThinkingMenuMessage: async () => {
|
||||
events.push("unexpected:thinking");
|
||||
},
|
||||
updateStatusMessage: async () => {
|
||||
events.push("status:update");
|
||||
},
|
||||
setTraceVisible: (nextTraceVisible) => {
|
||||
traceVisible = nextTraceVisible;
|
||||
events.push(`trace:${nextTraceVisible ? "on" : "off"}`);
|
||||
},
|
||||
getTraceVisible: () => traceVisible,
|
||||
answerCallbackQuery: async (_id, text) => {
|
||||
events.push(`answer:${text ?? ""}`);
|
||||
},
|
||||
@@ -504,6 +544,13 @@ test("Menu helpers handle status and thinking callback actions", async () => {
|
||||
updateThinkingMenuMessage: async () => {
|
||||
events.push("unexpected:thinking");
|
||||
},
|
||||
updateStatusMessage: async () => {
|
||||
events.push("unexpected:status");
|
||||
},
|
||||
setTraceVisible: () => {
|
||||
events.push("unexpected:trace");
|
||||
},
|
||||
getTraceVisible: () => traceVisible,
|
||||
answerCallbackQuery: async (_id, text) => {
|
||||
events.push(`answer:${text ?? ""}`);
|
||||
},
|
||||
@@ -513,10 +560,13 @@ test("Menu helpers handle status and thinking callback actions", async () => {
|
||||
);
|
||||
assert.equal(events[0], "status:model");
|
||||
assert.equal(events[1], "answer:");
|
||||
assert.equal(events[2], "set:high");
|
||||
assert.equal(events[2], "trace:off");
|
||||
assert.equal(events[3], "status:update");
|
||||
assert.equal(events[4], "answer:Thinking: high");
|
||||
assert.equal(events[5], "answer:This model has no reasoning controls.");
|
||||
assert.equal(events[4], "answer:Trace: off");
|
||||
assert.equal(events[5], "set:high");
|
||||
assert.equal(events[6], "status:update");
|
||||
assert.equal(events[7], "answer:Thinking: high");
|
||||
assert.equal(events[8], "answer:This model has no reasoning controls.");
|
||||
});
|
||||
|
||||
test("Menu helpers build pure render payloads before transport", () => {
|
||||
@@ -536,6 +586,7 @@ test("Menu helpers build pure render payloads before transport", () => {
|
||||
"<b>Status</b>",
|
||||
modelA as never,
|
||||
"medium",
|
||||
true,
|
||||
);
|
||||
assert.equal(modelPayload.nextMode, "model");
|
||||
assert.equal(modelPayload.text, "<b>Choose a model:</b>");
|
||||
@@ -586,6 +637,7 @@ test("Menu helpers update and send interactive menu messages", async () => {
|
||||
"<b>Status</b>",
|
||||
modelA as never,
|
||||
"medium",
|
||||
true,
|
||||
deps,
|
||||
);
|
||||
const sentStatusId = await sendTelegramStatusMessage(
|
||||
@@ -593,6 +645,7 @@ test("Menu helpers update and send interactive menu messages", async () => {
|
||||
"<b>Status</b>",
|
||||
modelA as never,
|
||||
"medium",
|
||||
true,
|
||||
deps,
|
||||
);
|
||||
const sentModelId = await sendTelegramModelMenuMessage(state, modelA as never, deps);
|
||||
@@ -638,8 +691,9 @@ test("Menu helpers build model, thinking, and status UI payloads", () => {
|
||||
thinkingMarkup.inline_keyboard.some((row) => row[0]?.text === "✅ medium"),
|
||||
true,
|
||||
);
|
||||
const statusMarkup = buildStatusReplyMarkup(modelA as never, "medium");
|
||||
assert.equal(statusMarkup.inline_keyboard.length, 2);
|
||||
const noReasoningMarkup = buildStatusReplyMarkup(modelB as never, "medium");
|
||||
assert.equal(noReasoningMarkup.inline_keyboard.length, 1);
|
||||
const statusMarkup = buildStatusReplyMarkup(modelA as never, "medium", true);
|
||||
assert.equal(statusMarkup.inline_keyboard.length, 3);
|
||||
assert.equal(statusMarkup.inline_keyboard[1]?.[0]?.callback_data, "status:trace");
|
||||
const noReasoningMarkup = buildStatusReplyMarkup(modelB as never, "medium", false);
|
||||
assert.equal(noReasoningMarkup.inline_keyboard.length, 2);
|
||||
});
|
||||
|
||||
+83
-25
@@ -6,10 +6,68 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { __telegramTestUtils } from "../index.ts";
|
||||
import {
|
||||
MAX_MESSAGE_LENGTH,
|
||||
buildTelegramAssistantPreviewText,
|
||||
buildTelegramAssistantTranscriptMarkdown,
|
||||
renderTelegramMessage,
|
||||
} from "../lib/rendering.ts";
|
||||
|
||||
test("Assistant trace helpers build compact previews and fuller transcripts", () => {
|
||||
const blocks = [
|
||||
{ type: "text", text: "Answer intro" },
|
||||
{ type: "thinking", text: "Need to inspect the config first." },
|
||||
{
|
||||
type: "tool_call",
|
||||
name: "read_config",
|
||||
argsText: '{"path":"config.json"}',
|
||||
},
|
||||
{ type: "text", text: "\n\nFinal answer." },
|
||||
] as const;
|
||||
assert.equal(
|
||||
buildTelegramAssistantPreviewText(blocks as never, true),
|
||||
[
|
||||
"Answer intro\n\nFinal answer.",
|
||||
'[thinking] Need to inspect the config first.\n[tool] read_config {"path":"config.json"}',
|
||||
].join("\n\n"),
|
||||
);
|
||||
assert.equal(
|
||||
buildTelegramAssistantPreviewText(blocks as never, false),
|
||||
"Answer intro\n\nFinal answer.",
|
||||
);
|
||||
assert.equal(
|
||||
buildTelegramAssistantTranscriptMarkdown(blocks as never, true),
|
||||
[
|
||||
"Answer intro",
|
||||
"**Thinking**\n> Need to inspect the config first.",
|
||||
'**Tool call** `read_config` `{"path":"config.json"}`',
|
||||
"Final answer.",
|
||||
].join("\n\n"),
|
||||
);
|
||||
assert.equal(
|
||||
buildTelegramAssistantTranscriptMarkdown(blocks as never, false),
|
||||
"Answer intro\n\nFinal answer.",
|
||||
);
|
||||
});
|
||||
|
||||
test("Assistant trace transcript uses code fences for long tool arguments", () => {
|
||||
const markdown = buildTelegramAssistantTranscriptMarkdown(
|
||||
[
|
||||
{ type: "text", text: "Answer" },
|
||||
{
|
||||
type: "tool_call",
|
||||
name: "write_file",
|
||||
argsText: '{\n "path": "out/report.md",\n "content": "long body"\n}',
|
||||
},
|
||||
],
|
||||
true,
|
||||
);
|
||||
assert.match(markdown, /\*\*Tool call\*\* `write_file`/);
|
||||
assert.match(markdown, /```json/);
|
||||
});
|
||||
|
||||
test("Nested lists stay out of code blocks", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"- Level 1\n - Level 2\n - Level 3 with **bold** text",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -27,7 +85,7 @@ test("Nested lists stay out of code blocks", () => {
|
||||
});
|
||||
|
||||
test("Fenced code blocks preserve literal markdown", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
'~~~ts\nconst value = "**raw**";\n~~~',
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -37,7 +95,7 @@ test("Fenced code blocks preserve literal markdown", () => {
|
||||
});
|
||||
|
||||
test("Underscores inside words do not become italic", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"Path: foo_bar_baz.txt and **bold**",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -47,7 +105,7 @@ test("Underscores inside words do not become italic", () => {
|
||||
});
|
||||
|
||||
test("Quoted nested lists stay in blockquote rendering", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"> Quoted intro\n> - nested item\n> - deeper item",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -59,7 +117,7 @@ test("Quoted nested lists stay in blockquote rendering", () => {
|
||||
});
|
||||
|
||||
test("Numbered lists use monospace numeric markers", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"1. first\n 2. second",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -69,7 +127,7 @@ test("Numbered lists use monospace numeric markers", () => {
|
||||
});
|
||||
|
||||
test("Nested blockquotes flatten into one Telegram blockquote with indentation", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"> outer\n>> inner\n>>> deepest",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -82,7 +140,7 @@ test("Nested blockquotes flatten into one Telegram blockquote with indentation",
|
||||
});
|
||||
|
||||
test("Markdown tables render as literal monospace blocks without outer side borders", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"| Name | Value |\n| --- | --- |\n| **x** | `y` |",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -96,7 +154,7 @@ test("Markdown tables render as literal monospace blocks without outer side bord
|
||||
});
|
||||
|
||||
test("Links, code spans, and underscore-heavy text coexist safely", () => {
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(
|
||||
const chunks = renderTelegramMessage(
|
||||
"See [docs](https://example.com), run `foo_bar()` and keep foo_bar.txt literal",
|
||||
{ mode: "markdown" },
|
||||
);
|
||||
@@ -114,12 +172,12 @@ test("Long quoted blocks stay chunked with balanced blockquote tags", () => {
|
||||
{ length: 500 },
|
||||
(_, index) => `> quoted **${index}** line`,
|
||||
).join("\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<blockquote>/g) ?? []).length,
|
||||
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
|
||||
@@ -132,12 +190,12 @@ test("Long markdown replies stay chunked below Telegram limits", () => {
|
||||
{ length: 600 },
|
||||
(_, index) => `- item **${index}**`,
|
||||
).join("\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<b>/g) ?? []).length,
|
||||
(chunk.text.match(/<\/b>/g) ?? []).length,
|
||||
@@ -151,12 +209,12 @@ test("Long mixed links and code spans stay chunked with balanced inline tags", (
|
||||
(_, index) =>
|
||||
`Paragraph ${index}: see [docs ${index}](https://example.com/${index}), run \`code_${index}()\`, and keep foo_bar_${index}.txt literal`,
|
||||
).join("\n\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<a /g) ?? []).length,
|
||||
(chunk.text.match(/<\/a>/g) ?? []).length,
|
||||
@@ -180,12 +238,12 @@ test("Long multi-block markdown keeps quotes and code fences structurally balanc
|
||||
"```",
|
||||
].join("\n");
|
||||
}).join("\n\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<blockquote>/g) ?? []).length,
|
||||
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
|
||||
@@ -206,12 +264,12 @@ test("Chunked mixed block transitions keep quote and list structure balanced", (
|
||||
`plain paragraph ${index} with [link](https://example.com/${index})`,
|
||||
].join("\n");
|
||||
}).join("\n\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<blockquote>/g) ?? []).length,
|
||||
(chunk.text.match(/<\/blockquote>/g) ?? []).length,
|
||||
@@ -232,12 +290,12 @@ test("Chunked code fence transitions keep code blocks closed before following pr
|
||||
`After code **${index}** and \`inline_${index}()\``,
|
||||
].join("\n");
|
||||
}).join("\n\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<pre><code/g) ?? []).length,
|
||||
(chunk.text.match(/<\/code><\/pre>/g) ?? []).length,
|
||||
@@ -253,12 +311,12 @@ test("Long inline formatting paragraphs stay balanced across chunk boundaries",
|
||||
const markdown = Array.from({ length: 500 }, (_, index) => {
|
||||
return `Segment ${index} keeps **bold_${index}** with \`code_${index}()\`, [link_${index}](https://example.com/${index}), and foo_bar_${index}.txt literal.`;
|
||||
}).join(" ");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<b>/g) ?? []).length,
|
||||
(chunk.text.match(/<\/b>/g) ?? []).length,
|
||||
@@ -286,12 +344,12 @@ test("Chunked list, code, quote, and prose cycles stay balanced across transitio
|
||||
`Plain paragraph ${index} with \`inline_${index}()\``,
|
||||
].join("\n");
|
||||
}).join("\n\n");
|
||||
const chunks = __telegramTestUtils.renderTelegramMessage(markdown, {
|
||||
const chunks = renderTelegramMessage(markdown, {
|
||||
mode: "markdown",
|
||||
});
|
||||
assert.ok(chunks.length > 1);
|
||||
for (const chunk of chunks) {
|
||||
assert.ok(chunk.text.length <= __telegramTestUtils.MAX_MESSAGE_LENGTH);
|
||||
assert.ok(chunk.text.length <= MAX_MESSAGE_LENGTH);
|
||||
assert.equal(
|
||||
(chunk.text.match(/<pre><code/g) ?? []).length,
|
||||
(chunk.text.match(/<\/code><\/pre>/g) ?? []).length,
|
||||
|
||||
Reference in New Issue
Block a user