@llblab/pi-telegram 0.2.8 → 0.2.9

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/lib/replies.ts CHANGED
@@ -1,187 +1,10 @@
1
1
  /**
2
- * Telegram reply and preview domain helpers
3
- * Owns preview text decisions, preview runtime behavior, rendered-message delivery, and plain or markdown reply sending
2
+ * Telegram reply delivery helpers
3
+ * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
4
4
  */
5
5
 
6
6
  import type { TelegramRenderedChunk, TelegramRenderMode } from "./rendering.ts";
7
7
 
8
- // --- Preview ---
9
-
10
- export interface TelegramPreviewStateLike {
11
- mode: "draft" | "message";
12
- draftId?: number;
13
- messageId?: number;
14
- pendingText: string;
15
- lastSentText: string;
16
- }
17
-
18
- export interface TelegramPreviewRuntimeState extends TelegramPreviewStateLike {
19
- flushTimer?: ReturnType<typeof setTimeout>;
20
- }
21
-
22
- export interface TelegramPreviewRuntimeDeps {
23
- getState: () => TelegramPreviewRuntimeState | undefined;
24
- setState: (state: TelegramPreviewRuntimeState | undefined) => void;
25
- clearScheduledFlush: (state: TelegramPreviewRuntimeState) => void;
26
- maxMessageLength: number;
27
- renderPreviewText: (markdown: string) => string;
28
- getDraftSupport: () => "unknown" | "supported" | "unsupported";
29
- setDraftSupport: (support: "unknown" | "supported" | "unsupported") => void;
30
- allocateDraftId: () => number;
31
- sendDraft: (chatId: number, draftId: number, text: string) => Promise<void>;
32
- sendMessage: (
33
- chatId: number,
34
- text: string,
35
- ) => Promise<TelegramSentMessageLike>;
36
- editMessageText: (
37
- chatId: number,
38
- messageId: number,
39
- text: string,
40
- ) => Promise<void>;
41
- renderTelegramMessage: (
42
- text: string,
43
- options?: { mode?: TelegramRenderMode },
44
- ) => TelegramRenderedChunk[];
45
- sendRenderedChunks: (
46
- chatId: number,
47
- chunks: TelegramRenderedChunk[],
48
- ) => Promise<number | undefined>;
49
- editRenderedMessage: (
50
- chatId: number,
51
- messageId: number,
52
- chunks: TelegramRenderedChunk[],
53
- ) => Promise<number | undefined>;
54
- }
55
-
56
- export function buildTelegramPreviewFlushText(options: {
57
- state: TelegramPreviewStateLike;
58
- maxMessageLength: number;
59
- renderPreviewText: (markdown: string) => string;
60
- }): string | undefined {
61
- const rawText = options.state.pendingText.trim();
62
- const previewText = options.renderPreviewText(rawText).trim();
63
- if (!previewText || previewText === options.state.lastSentText) {
64
- return undefined;
65
- }
66
- return previewText.length > options.maxMessageLength
67
- ? previewText.slice(0, options.maxMessageLength)
68
- : previewText;
69
- }
70
-
71
- export function buildTelegramPreviewFinalText(
72
- state: TelegramPreviewStateLike,
73
- ): string | undefined {
74
- const finalText = (state.pendingText.trim() || state.lastSentText).trim();
75
- return finalText || undefined;
76
- }
77
-
78
- export function shouldUseTelegramDraftPreview(options: {
79
- draftSupport: "unknown" | "supported" | "unsupported";
80
- }): boolean {
81
- return options.draftSupport !== "unsupported";
82
- }
83
-
84
- export async function clearTelegramPreview(
85
- chatId: number,
86
- deps: TelegramPreviewRuntimeDeps,
87
- ): Promise<void> {
88
- const state = deps.getState();
89
- if (!state) return;
90
- deps.clearScheduledFlush(state);
91
- deps.setState(undefined);
92
- if (state.mode !== "draft" || state.draftId === undefined) return;
93
- try {
94
- await deps.sendDraft(chatId, state.draftId, "");
95
- } catch {
96
- // ignore
97
- }
98
- }
99
-
100
- export async function flushTelegramPreview(
101
- chatId: number,
102
- deps: TelegramPreviewRuntimeDeps,
103
- ): Promise<void> {
104
- const state = deps.getState();
105
- if (!state) return;
106
- state.flushTimer = undefined;
107
- const truncated = buildTelegramPreviewFlushText({
108
- state,
109
- maxMessageLength: deps.maxMessageLength,
110
- renderPreviewText: deps.renderPreviewText,
111
- });
112
- if (!truncated) return;
113
- if (shouldUseTelegramDraftPreview({ draftSupport: deps.getDraftSupport() })) {
114
- const draftId = state.draftId ?? deps.allocateDraftId();
115
- state.draftId = draftId;
116
- try {
117
- await deps.sendDraft(chatId, draftId, truncated);
118
- deps.setDraftSupport("supported");
119
- state.mode = "draft";
120
- state.lastSentText = truncated;
121
- return;
122
- } catch {
123
- deps.setDraftSupport("unsupported");
124
- }
125
- }
126
- if (state.messageId === undefined) {
127
- const sent = await deps.sendMessage(chatId, truncated);
128
- state.messageId = sent.message_id;
129
- state.mode = "message";
130
- state.lastSentText = truncated;
131
- return;
132
- }
133
- await deps.editMessageText(chatId, state.messageId, truncated);
134
- state.mode = "message";
135
- state.lastSentText = truncated;
136
- }
137
-
138
- export async function finalizeTelegramPreview(
139
- chatId: number,
140
- deps: TelegramPreviewRuntimeDeps,
141
- ): Promise<boolean> {
142
- const state = deps.getState();
143
- if (!state) return false;
144
- await flushTelegramPreview(chatId, deps);
145
- const finalText = buildTelegramPreviewFinalText(state);
146
- if (!finalText) {
147
- await clearTelegramPreview(chatId, deps);
148
- return false;
149
- }
150
- if (state.mode === "draft") {
151
- await deps.sendMessage(chatId, finalText);
152
- await clearTelegramPreview(chatId, deps);
153
- return true;
154
- }
155
- deps.setState(undefined);
156
- return state.messageId !== undefined;
157
- }
158
-
159
- export async function finalizeTelegramMarkdownPreview(
160
- chatId: number,
161
- markdown: string,
162
- deps: TelegramPreviewRuntimeDeps,
163
- ): Promise<boolean> {
164
- const state = deps.getState();
165
- if (!state) return false;
166
- await flushTelegramPreview(chatId, deps);
167
- const chunks = deps.renderTelegramMessage(markdown, { mode: "markdown" });
168
- if (chunks.length === 0) {
169
- await clearTelegramPreview(chatId, deps);
170
- return false;
171
- }
172
- if (state.mode === "draft") {
173
- await deps.sendRenderedChunks(chatId, chunks);
174
- await clearTelegramPreview(chatId, deps);
175
- return true;
176
- }
177
- if (state.messageId === undefined) return false;
178
- await deps.editRenderedMessage(chatId, state.messageId, chunks);
179
- deps.setState(undefined);
180
- return true;
181
- }
182
-
183
- // --- Delivery ---
184
-
185
8
  export interface TelegramSentMessageLike {
186
9
  message_id: number;
187
10
  }
@@ -278,8 +101,6 @@ export async function editTelegramRenderedMessage<TReplyMarkup>(
278
101
  return messageId;
279
102
  }
280
103
 
281
- // --- Reply Runtime ---
282
-
283
104
  export interface TelegramReplyRuntimeDeps {
284
105
  renderTelegramMessage: (
285
106
  text: string,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",
@@ -144,7 +144,9 @@ test("Menu helpers apply menu mutations and resolve model selections", () => {
144
144
  assert.equal(state.page, 2);
145
145
  assert.equal(applyTelegramModelPageSelection(state, "2"), "unchanged");
146
146
  assert.equal(applyTelegramModelPageSelection(state, "bad"), "invalid");
147
- assert.deepEqual(getTelegramModelSelection(state, "bad"), { kind: "invalid" });
147
+ assert.deepEqual(getTelegramModelSelection(state, "bad"), {
148
+ kind: "invalid",
149
+ });
148
150
  assert.deepEqual(getTelegramModelSelection(state, "9"), { kind: "missing" });
149
151
  assert.equal(getTelegramModelSelection(state, "0").kind, "selected");
150
152
  });
@@ -174,7 +176,11 @@ test("Menu helpers derive normalized menu pages without mutating state", () => {
174
176
 
175
177
  test("Menu helpers build model callback plans for paging, selection, and restart modes", () => {
176
178
  const modelA = { provider: "openai", id: "gpt-5", reasoning: true } as const;
177
- const modelB = { provider: "anthropic", id: "claude-3", reasoning: false } as const;
179
+ const modelB = {
180
+ provider: "anthropic",
181
+ id: "claude-3",
182
+ reasoning: false,
183
+ } as const;
178
184
  const state = {
179
185
  chatId: 1,
180
186
  messageId: 2,
@@ -227,7 +233,8 @@ test("Menu helpers build model callback plans for paging, selection, and restart
227
233
  kind: "switch-model",
228
234
  selection: state.allModels[1],
229
235
  mode: "restart-after-tool",
230
- callbackText: "Switched to claude-3. Restarting after the current tool finishes…",
236
+ callbackText:
237
+ "Switched to claude-3. Restarting after the current tool finishes…",
231
238
  },
232
239
  );
233
240
  assert.deepEqual(
@@ -254,14 +261,19 @@ test("Menu helpers route callback entry states before action handlers", async ()
254
261
  events.push(`answer:${text ?? ""}`);
255
262
  },
256
263
  });
257
- await handleTelegramMenuCallbackEntry("callback-2", "status:model", undefined, {
258
- handleStatusAction: async () => false,
259
- handleThinkingAction: async () => false,
260
- handleModelAction: async () => false,
261
- answerCallbackQuery: async (_id, text) => {
262
- events.push(`answer:${text ?? ""}`);
264
+ await handleTelegramMenuCallbackEntry(
265
+ "callback-2",
266
+ "status:model",
267
+ undefined,
268
+ {
269
+ handleStatusAction: async () => false,
270
+ handleThinkingAction: async () => false,
271
+ handleModelAction: async () => false,
272
+ answerCallbackQuery: async (_id, text) => {
273
+ events.push(`answer:${text ?? ""}`);
274
+ },
263
275
  },
264
- });
276
+ );
265
277
  await handleTelegramMenuCallbackEntry(
266
278
  "callback-3",
267
279
  "status:model",
@@ -296,7 +308,11 @@ test("Menu helpers route callback entry states before action handlers", async ()
296
308
  test("Menu helpers execute model callback actions across update, switch, and restart paths", async () => {
297
309
  const events: string[] = [];
298
310
  const modelA = { provider: "openai", id: "gpt-5", reasoning: true } as const;
299
- const modelB = { provider: "anthropic", id: "claude-3", reasoning: false } as const;
311
+ const modelB = {
312
+ provider: "anthropic",
313
+ id: "claude-3",
314
+ reasoning: false,
315
+ } as const;
300
316
  const state = {
301
317
  chatId: 1,
302
318
  messageId: 2,
@@ -530,8 +546,14 @@ test("Menu helpers build pure render payloads before transport", () => {
530
546
  allModels: [{ model: modelA }],
531
547
  mode: "status" as const,
532
548
  } as unknown as TelegramModelMenuState;
533
- const modelPayload = buildTelegramModelMenuRenderPayload(state, modelA as never);
534
- const thinkingPayload = buildTelegramThinkingMenuRenderPayload(modelA as never, "medium");
549
+ const modelPayload = buildTelegramModelMenuRenderPayload(
550
+ state,
551
+ modelA as never,
552
+ );
553
+ const thinkingPayload = buildTelegramThinkingMenuRenderPayload(
554
+ modelA as never,
555
+ "medium",
556
+ );
535
557
  const statusPayload = buildTelegramStatusMenuRenderPayload(
536
558
  "<b>Status</b>",
537
559
  modelA as never,
@@ -580,7 +602,12 @@ test("Menu helpers update and send interactive menu messages", async () => {
580
602
  },
581
603
  };
582
604
  await updateTelegramModelMenuMessage(state, modelA as never, deps);
583
- await updateTelegramThinkingMenuMessage(state, modelA as never, "medium", deps);
605
+ await updateTelegramThinkingMenuMessage(
606
+ state,
607
+ modelA as never,
608
+ "medium",
609
+ deps,
610
+ );
584
611
  await updateTelegramStatusMessage(
585
612
  state,
586
613
  "<b>Status</b>",
@@ -595,7 +622,11 @@ test("Menu helpers update and send interactive menu messages", async () => {
595
622
  "medium",
596
623
  deps,
597
624
  );
598
- const sentModelId = await sendTelegramModelMenuMessage(state, modelA as never, deps);
625
+ const sentModelId = await sendTelegramModelMenuMessage(
626
+ state,
627
+ modelA as never,
628
+ deps,
629
+ );
599
630
  assert.equal(sentStatusId, 99);
600
631
  assert.equal(sentModelId, 99);
601
632
  assert.equal(events[0], "edit:1:2:html:<b>Choose a model:</b>");