Skip to content

Commit 146281d

Browse files
authored
Merge pull request #95 from llblab/dev
0.19.2: draft and rendering isolation hotfix
2 parents 86b9bbd + 1158a96 commit 146281d

17 files changed

Lines changed: 164 additions & 54 deletions

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
11
# Changelog
22

3+
## 0.19.2: Draft And Rendering Isolation Hotfix
4+
5+
- `[Config]` Grouped assistant answer output under `assistant: { rendering, draftPreviews }`, while still reading and cleaning up legacy `assistantRendering`, `draftPreviews`, and `richDraftPreviews`. Impact: config vocabulary now matches the feature boundary; draft visibility and final rendering live together without implying that previews are inherently Rich Markdown.
6+
- `[Preview]` Hard-gated preview state creation behind the Draft previews setting. Impact: when Draft previews are off, `message_start` / `message_update` cannot create or flush draft frames; Telegram should show only native active status until the final answer.
7+
- `[Preview]` Aligned enabled draft previews with the selected final renderer: `assistant.rendering: "rich"` uses `sendRichMessageDraft`, while `assistant.rendering: "html"` uses legacy `sendMessageDraft` with HTML. Impact: the visible draft no longer morphs from Native Rich Markdown into legacy HTML at finalization.
8+
- `[Rendering]` Removed the thread-reply special case that forced anchored thread assistant replies through legacy Markdown-to-HTML. Impact: `assistant.rendering: "rich"` now uses native Rich Markdown for final assistant replies in Threaded Mode too, while `assistant.rendering: "html"` remains the only path that selects legacy HTML rendering.
9+
- `[Tests]` Updated reply regressions to assert native Rich Markdown delivery for anchored thread replies.
10+
311
## 0.19.1: Settings Layer Hotfix
412

513
- `[Settings]` Split the overloaded Rich Draft setting into two independent controls: `Draft previews` for live `sendRichMessageDraft` streaming and `Assistant rendering` for final-answer delivery mode. Impact: operators can hide/show in-progress drafts without changing how final Markdown is rendered.
614
- `[Rendering]` Added persisted `assistantRendering: "rich" | "html"`, defaulting to Native Rich Markdown and allowing legacy Markdown-to-HTML final assistant replies when selected. Impact: renderer compatibility is explicit instead of being conflated with preview visibility.
7-
- `[Preview]` Kept `richDraftPreviews` as the stored draft-preview flag for compatibility, but renamed the Settings UI to `Draft previews`. Impact: existing configs keep working while the product vocabulary matches the Bot API layer.
15+
- `[Preview]` Kept `richDraftPreviews` as the stored draft-preview flag for compatibility, but renamed the Settings UI to `Draft previews`. Impact: existing configs keep working while the product vocabulary moves toward the preview feature boundary.
816
- `[Validation]` Updated menu/settings/reply regressions for the two-axis configuration model.
917

1018
## 0.19.0: Telegram Companion Hub

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ Most controls live in Pi commands or the Telegram menu. Environment variables re
202202
| Inbound file limit | `PI_TELEGRAM_INBOUND_FILE_MAX_BYTES`, `TELEGRAM_MAX_FILE_SIZE_BYTES` |
203203
| Outbound attachment limit | `PI_TELEGRAM_OUTBOUND_ATTACHMENT_MAX_BYTES`, `TELEGRAM_MAX_ATTACHMENT_SIZE_BYTES` |
204204

205-
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistantRendering: "rich"` for final replies, `richDraftPreviews: false` for no live draft text by default, and native Telegram active status for long-running turns.
205+
Defaults are chosen for ordinary private-bot use: saved config in `~/.pi/agent`, inbound temp files in `~/.pi/agent/tmp/telegram`, `assistant: { rendering: "rich", draftPreviews: false }` for assistant answer output, and native Telegram active status for long-running turns.
206206

207207
## Extension Platform
208208

docs/architecture.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,11 +223,11 @@ During active Telegram-owned turns, assistant message start/update hooks re-arm
223223

224224
### Rendering And Delivery
225225

226-
Rich Markdown is the default model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage` when `assistantRendering` is `rich`, and through the legacy Markdown-to-HTML renderer when `assistantRendering` is `html`; guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering. Streaming previews may use `sendRichMessageDraft` only when Draft previews are enabled and draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
226+
Rich Markdown is the default model-answer membrane. Complete assistant replies send final Markdown directly as `InputRichMessage.markdown` through `sendRichMessage` when `assistant.rendering` is `rich`, and through the legacy Markdown-to-HTML renderer when `assistant.rendering` is `html`; guest replies use native Rich Markdown through `InputRichMessageContent` in `answerGuestQuery` results. Tool-call rows, reasoning/thinking blocks, menus, status rows, queue controls, settings, diagnostics, and other harness-owned surfaces stay on explicit Telegram HTML/plain rendering. Streaming previews may use `sendRichMessageDraft` only when `assistant.draftPreviews` is enabled and draft delivery succeeds. The bridge still strips top-level assistant action comments before delivery and may split output only for Telegram transport limits.
227227

228228
Assistant delivery guarantees:
229229

230-
- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistantRendering: "html"` for compatibility.
230+
- Model-authored Markdown is the source of truth; the bridge does not pre-render assistant Markdown to HTML unless the operator selects `assistant.rendering: "html"` for compatibility.
231231
- Before native Rich Markdown delivery, the bridge normalizes known Bot-API-fragile source forms without changing visible meaning, including space-after-marker blockquotes and dollar-prefixed ticker atoms that Telegram may otherwise treat as unterminated math.
232232
- Quoted rich replies use Telegram `rich_message` blocks as the prompt-context source when available, so `[reply]` context receives rendered plain text instead of raw `InputRichMessage.markdown` fallback text.
233233
- Long native Markdown replies are split only at Telegram Rich Message transport limits; oversized fenced code, display-math, and fully wrapped inline-formatting blocks are rewrapped per chunk so persisted Rich Markdown chunks remain structurally valid.

index.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,9 @@ export default function (pi: Pi.ExtensionAPI) {
478478
recordRuntimeEvent,
479479
});
480480
const nativeMarkdownDraftSender =
481-
TelegramApi.createTelegramNativeMarkdownDraftSender({
481+
TelegramApi.createTelegramAssistantDraftSender({
482+
getAssistantRenderingMode: configControls.getAssistantRenderingMode,
483+
renderMarkdownToHtmlDraft: Replies.renderTelegramMarkdownToHtmlDraft,
482484
sendMessageDraft,
483485
sendRichMessageDraft,
484486
});
@@ -488,7 +490,7 @@ export default function (pi: Pi.ExtensionAPI) {
488490
getMessageText: Replies.getAgentMessageText,
489491
getDefaultReplyToMessageId: activeTurnRuntime.getReplyToMessageId,
490492
sendDraft: nativeMarkdownDraftSender,
491-
canSend: configControls.areRichDraftPreviewsEnabled,
493+
canSend: configControls.areDraftPreviewsEnabled,
492494
sendMarkdownReply,
493495
recordRuntimeEvent,
494496
...replyTransport,

lib/config.ts

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,15 @@ export interface TelegramConfig {
5959
attachmentHandlers?: TelegramInboundHandlerConfig[];
6060
outboundHandlers?: TelegramOutboundHandlerConfig[];
6161
proactivePush?: boolean;
62+
assistant?: {
63+
draftPreviews?: boolean;
64+
rendering?: TelegramAssistantRenderingMode;
65+
};
66+
/** @deprecated use assistant.draftPreviews */
67+
draftPreviews?: boolean;
68+
/** @deprecated use assistant.draftPreviews */
6269
richDraftPreviews?: boolean;
70+
/** @deprecated use assistant.rendering */
6371
assistantRendering?: TelegramAssistantRenderingMode;
6472
voice?: {
6573
replyMode?: "manual" | "mirror" | "always";
@@ -263,18 +271,34 @@ export function createTelegramProactivePushSetter(
263271
};
264272
}
265273

266-
export function createTelegramRichDraftPreviewsChecker(
274+
export function createTelegramDraftPreviewsChecker(
267275
configStore: Pick<TelegramConfigStore, "get">,
268276
): () => boolean {
269-
return () => configStore.get().richDraftPreviews ?? false;
277+
return () => {
278+
const config = configStore.get();
279+
return (
280+
config.assistant?.draftPreviews ??
281+
config.draftPreviews ??
282+
config.richDraftPreviews ??
283+
false
284+
);
285+
};
270286
}
271287

272-
export function createTelegramRichDraftPreviewsSetter(
288+
export function createTelegramDraftPreviewsSetter(
273289
configStore: TelegramMutableConfigStore,
274290
): (enabled: boolean) => Promise<void> {
275291
return async (enabled) => {
276292
await loadLatestTelegramConfig(configStore);
277-
const config = { ...configStore.get(), richDraftPreviews: enabled };
293+
const {
294+
draftPreviews: _legacyDraftPreviews,
295+
richDraftPreviews: _legacyRichDraftPreviews,
296+
...current
297+
} = configStore.get();
298+
const config = {
299+
...current,
300+
assistant: { ...current.assistant, draftPreviews: enabled },
301+
};
278302
configStore.set(config);
279303
await configStore.persist(config);
280304
};
@@ -284,7 +308,8 @@ export function createTelegramAssistantRenderingModeGetter(
284308
configStore: Pick<TelegramConfigStore, "get">,
285309
): () => TelegramAssistantRenderingMode {
286310
return () => {
287-
const mode = configStore.get().assistantRendering;
311+
const config = configStore.get();
312+
const mode = config.assistant?.rendering ?? config.assistantRendering;
288313
return mode === "html" ? "html" : "rich";
289314
};
290315
}
@@ -294,7 +319,12 @@ export function createTelegramAssistantRenderingModeSetter(
294319
): (mode: TelegramAssistantRenderingMode) => Promise<void> {
295320
return async (mode) => {
296321
await loadLatestTelegramConfig(configStore);
297-
const config = { ...configStore.get(), assistantRendering: mode };
322+
const { assistantRendering: _legacyAssistantRendering, ...current } =
323+
configStore.get();
324+
const config = {
325+
...current,
326+
assistant: { ...current.assistant, rendering: mode },
327+
};
298328
configStore.set(config);
299329
await configStore.persist(config);
300330
};
@@ -435,10 +465,8 @@ export function createTelegramConfigControls(
435465
return {
436466
isProactivePushEnabled: createTelegramProactivePushChecker(configStore),
437467
setProactivePushEnabled: createTelegramProactivePushSetter(configStore),
438-
areRichDraftPreviewsEnabled:
439-
createTelegramRichDraftPreviewsChecker(configStore),
440-
setRichDraftPreviewsEnabled:
441-
createTelegramRichDraftPreviewsSetter(configStore),
468+
areDraftPreviewsEnabled: createTelegramDraftPreviewsChecker(configStore),
469+
setDraftPreviewsEnabled: createTelegramDraftPreviewsSetter(configStore),
442470
getAssistantRenderingMode:
443471
createTelegramAssistantRenderingModeGetter(configStore),
444472
setAssistantRenderingMode:

lib/menu-settings.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
2121

2222
export interface TelegramSettingsStateDeps {
2323
isProactivePushEnabled: () => boolean;
24-
areRichDraftPreviewsEnabled: () => boolean;
24+
areDraftPreviewsEnabled: () => boolean;
2525
getAssistantRenderingMode: () => TelegramAssistantRenderingMode;
2626
getTimeInjectionMode: () => TelegramTimeMode;
2727
getVoiceReplyMode: () => TelegramVoiceReplyMode;
@@ -30,7 +30,7 @@ export interface TelegramSettingsStateDeps {
3030

3131
export interface TelegramSettingsMutationDeps extends TelegramSettingsStateDeps {
3232
setProactivePushEnabled: (enabled: boolean) => Promise<void>;
33-
setRichDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
33+
setDraftPreviewsEnabled: (enabled: boolean) => Promise<void>;
3434
setAssistantRenderingMode: (
3535
mode: TelegramAssistantRenderingMode,
3636
) => Promise<void>;
@@ -311,7 +311,7 @@ export async function openTelegramSettingsMenu<
311311
buildTelegramSettingsMenuText(),
312312
buildTelegramSettingsMenuReplyMarkup(
313313
deps.isProactivePushEnabled(),
314-
deps.areRichDraftPreviewsEnabled(),
314+
deps.areDraftPreviewsEnabled(),
315315
deps.getAssistantRenderingMode(),
316316
deps.getVoiceReplyMode(),
317317
deps.getTimeInjectionMode(),
@@ -431,7 +431,7 @@ export async function updateTelegramSettingsMenuMessage(
431431
buildTelegramSettingsMenuText(),
432432
buildTelegramSettingsMenuReplyMarkup(
433433
deps.isProactivePushEnabled(),
434-
deps.areRichDraftPreviewsEnabled(),
434+
deps.areDraftPreviewsEnabled(),
435435
deps.getAssistantRenderingMode(),
436436
deps.getVoiceReplyMode(),
437437
deps.getTimeInjectionMode(),
@@ -454,7 +454,7 @@ export async function updateProactivePushSettingsMessage(
454454
export async function updateDraftPreviewsSettingsMessage(
455455
deps: TelegramSettingsMenuCallbackDeps,
456456
): Promise<void> {
457-
const enabled = deps.areRichDraftPreviewsEnabled();
457+
const enabled = deps.areDraftPreviewsEnabled();
458458
await deps.updateSettingsMessage(
459459
buildDraftPreviewsSettingsText(enabled),
460460
buildDraftPreviewsSettingsReplyMarkup(enabled),
@@ -577,7 +577,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
577577
data === "settings:set:rich-drafts:off"
578578
) {
579579
const enabled = data.endsWith(":on");
580-
await deps.setRichDraftPreviewsEnabled(enabled);
580+
await deps.setDraftPreviewsEnabled(enabled);
581581
await updateDraftPreviewsSettingsMessage(deps);
582582
await deps.answerCallbackQuery(
583583
callbackQueryId,
@@ -624,7 +624,7 @@ export function createTelegramSettingsMenuRuntime<
624624
{
625625
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
626626
isProactivePushEnabled: deps.isProactivePushEnabled,
627-
areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
627+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
628628
getAssistantRenderingMode: deps.getAssistantRenderingMode,
629629
getVoiceReplyMode: deps.getVoiceReplyMode,
630630
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
@@ -644,7 +644,7 @@ export function createTelegramSettingsMenuRuntime<
644644
updateTelegramSettingsMenuMessage(
645645
{
646646
isProactivePushEnabled: deps.isProactivePushEnabled,
647-
areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
647+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
648648
getAssistantRenderingMode: deps.getAssistantRenderingMode,
649649
getVoiceReplyMode: deps.getVoiceReplyMode,
650650
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
@@ -691,7 +691,7 @@ export function createTelegramSettingsMenuRuntime<
691691
query.data === "settings:set:rich-drafts:off"
692692
) {
693693
const enabled = query.data.endsWith(":on");
694-
await deps.setRichDraftPreviewsEnabled(enabled);
694+
await deps.setDraftPreviewsEnabled(enabled);
695695
await deps.answerCallbackQuery(
696696
query.id,
697697
`Draft previews ${enabled ? "enabled" : "disabled"}`,
@@ -736,13 +736,13 @@ export function createTelegramSettingsMenuRuntime<
736736
}
737737
return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
738738
isProactivePushEnabled: deps.isProactivePushEnabled,
739-
areRichDraftPreviewsEnabled: deps.areRichDraftPreviewsEnabled,
739+
areDraftPreviewsEnabled: deps.areDraftPreviewsEnabled,
740740
getAssistantRenderingMode: deps.getAssistantRenderingMode,
741741
getVoiceReplyMode: deps.getVoiceReplyMode,
742742
isVoiceReplyModeConfigured: deps.isVoiceReplyModeConfigured,
743743
getTimeInjectionMode: deps.getTimeInjectionMode,
744744
setProactivePushEnabled: deps.setProactivePushEnabled,
745-
setRichDraftPreviewsEnabled: deps.setRichDraftPreviewsEnabled,
745+
setDraftPreviewsEnabled: deps.setDraftPreviewsEnabled,
746746
setAssistantRenderingMode: deps.setAssistantRenderingMode,
747747
setVoiceReplyMode: deps.setVoiceReplyMode,
748748
setTimeInjectionMode: deps.setTimeInjectionMode,

lib/preview.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export interface TelegramAssistantMessagePreviewStartDeps<
7373
getState: () => TelegramPreviewRuntimeState | undefined;
7474
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
7575
createPreviewState: () => TelegramPreviewRuntimeState;
76+
canSend?: () => boolean;
7677
finalizePreview: (chatId: number) => Promise<boolean>;
7778
finalizeMarkdownPreview: (
7879
chatId: number,
@@ -88,6 +89,7 @@ export interface TelegramAssistantMessagePreviewUpdateDeps<TMessage> {
8889
getState: () => TelegramPreviewRuntimeState | undefined;
8990
setState: (state: TelegramPreviewRuntimeState | undefined) => void;
9091
createPreviewState: () => TelegramPreviewRuntimeState;
92+
canSend?: () => boolean;
9193
getMessageText: (message: TMessage) => string;
9294
schedulePreviewFlush: (
9395
chatId: number,
@@ -260,6 +262,7 @@ export function createTelegramAssistantPreviewRuntime<
260262
getState: controller.getState,
261263
setState: controller.setState,
262264
createPreviewState: controller.createState,
265+
canSend: deps.canSend,
263266
finalizePreview: controller.finalize,
264267
finalizeMarkdownPreview,
265268
getMessageText: deps.getMessageText,
@@ -349,6 +352,10 @@ export async function handleTelegramAssistantMessagePreviewStart<
349352
): Promise<void> {
350353
const turn = deps.getActiveTurn();
351354
if (!turn || !deps.isAssistantMessage(message)) return;
355+
if (deps.canSend && !deps.canSend()) {
356+
deps.setState(undefined);
357+
return;
358+
}
352359
if (shouldSuppressPreviewForVoice(turn)) {
353360
deps.setState(undefined);
354361
return;
@@ -382,6 +389,10 @@ export async function handleTelegramAssistantMessagePreviewUpdate<TMessage>(
382389
): Promise<void> {
383390
const turn = deps.getActiveTurn();
384391
if (!turn || !deps.isAssistantMessage(message)) return;
392+
if (deps.canSend && !deps.canSend()) {
393+
deps.setState(undefined);
394+
return;
395+
}
385396
if (shouldSuppressPreviewForVoice(turn)) return;
386397
let state = deps.getState();
387398
if (!state) {

lib/replies.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ export {
2727
type TelegramRenderMode,
2828
};
2929

30+
export function renderTelegramMarkdownToHtmlDraft(markdown: string): string {
31+
return renderTelegramMessage(markdown, { mode: "markdown" })
32+
.map((chunk) => chunk.text)
33+
.join("\n");
34+
}
35+
3036
export const TELEGRAM_RICH_MESSAGE_MAX_CHARS = 32768;
3137
export const TELEGRAM_RICH_MESSAGE_MAX_BLOCKS = 500;
3238

@@ -783,12 +789,7 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
783789
},
784790
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
785791
const renderingMode = deps.getAssistantRenderingMode?.() ?? "rich";
786-
if (
787-
renderingMode === "html" ||
788-
(typeof options?.target?.threadId === "number" &&
789-
replyToMessageId !== undefined &&
790-
replyToMessageId > 0)
791-
) {
792+
if (renderingMode === "html") {
792793
return deps.replyTransport.sendRenderedChunks(
793794
chatId,
794795
deps.renderTelegramMessage(markdown, { mode: "markdown" }),

lib/telegram-api.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,24 @@ export function createTelegramNativeMarkdownDraftSender(deps: {
11121112
};
11131113
}
11141114

1115+
export function createTelegramAssistantDraftSender(deps: {
1116+
getAssistantRenderingMode: () => "rich" | "html";
1117+
renderMarkdownToHtmlDraft: (markdown: string) => string;
1118+
sendMessageDraft: TelegramBridgeApiRuntime["sendMessageDraft"];
1119+
sendRichMessageDraft: TelegramBridgeApiRuntime["sendRichMessageDraft"];
1120+
}): TelegramBridgeApiRuntime["sendMessageDraft"] {
1121+
const sendNativeDraft = createTelegramNativeMarkdownDraftSender(deps);
1122+
return (chatId, draftId, text, options) => {
1123+
if (text === undefined || deps.getAssistantRenderingMode() === "rich") {
1124+
return sendNativeDraft(chatId, draftId, text, options);
1125+
}
1126+
return deps.sendMessageDraft(chatId, draftId, deps.renderMarkdownToHtmlDraft(text), {
1127+
...options,
1128+
parse_mode: "HTML",
1129+
});
1130+
};
1131+
}
1132+
11151133
export function createDefaultTelegramBridgeApiRuntime(deps: {
11161134
getBotToken: () => string | undefined;
11171135
recordRuntimeEvent: TelegramBridgeApiRuntimeDeps["recordRuntimeEvent"];

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)