@llblab/pi-telegram 0.6.3 → 0.7.0

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/queue.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /**
2
- * Telegram queue and queue-runtime domain helpers
3
- * Owns queue items, queue mutations, dispatch and lifecycle planning, session resets, and queue-adjacent runtime helpers
2
+ * Telegram queue core contracts and pure planning helpers
3
+ * Zones: telegram queue, pi agent lifecycle, scheduling
4
+ * Owns queue item contracts, lane admission, pure queue mutations, and dispatch planning
4
5
  */
5
6
 
6
7
  // --- Queue Items ---
@@ -178,6 +179,14 @@ export function createTelegramQueueStore<TContext = unknown>(
178
179
  };
179
180
  }
180
181
 
182
+ export function createTelegramQueueItemCountGetter<TContext = unknown>(
183
+ store: Pick<TelegramQueueStore<TContext>, "getQueuedItems">,
184
+ ): () => number {
185
+ return function getTelegramQueueItemCount() {
186
+ return store.getQueuedItems().length;
187
+ };
188
+ }
189
+
181
190
  export function createTelegramActiveTurnStore<
182
191
  TTurn extends PendingTelegramTurn = PendingTelegramTurn,
183
192
  >(): TelegramActiveTurnStore<TTurn> {
@@ -344,7 +353,7 @@ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
344
353
  item: TelegramQueueItem<TContext>,
345
354
  ): string {
346
355
  if (item.queueLane === "priority") {
347
- return `⬆ ${item.statusSummary}`;
356
+ return `⚡ ${item.statusSummary}`;
348
357
  }
349
358
  return item.statusSummary;
350
359
  }
@@ -665,18 +674,32 @@ export type TelegramAgentLifecycleHooksRuntimeDeps<
665
674
  TTurn extends PendingTelegramTurn,
666
675
  TContext,
667
676
  TMessage,
677
+ TReplyMarkup = unknown,
668
678
  > = TelegramAgentStartHookRuntimeDeps<TTurn, TContext> &
669
- TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage> &
679
+ TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage, TReplyMarkup> &
670
680
  TelegramToolExecutionHookRuntimeDeps<TContext>;
671
681
 
672
682
  export function createTelegramAgentLifecycleHooks<
673
683
  TTurn extends PendingTelegramTurn,
674
684
  TContext,
675
685
  TMessage,
676
- >(deps: TelegramAgentLifecycleHooksRuntimeDeps<TTurn, TContext, TMessage>) {
686
+ TReplyMarkup = unknown,
687
+ >(
688
+ deps: TelegramAgentLifecycleHooksRuntimeDeps<
689
+ TTurn,
690
+ TContext,
691
+ TMessage,
692
+ TReplyMarkup
693
+ >,
694
+ ) {
677
695
  return {
678
696
  onAgentStart: createTelegramAgentStartHook<TTurn, TContext>(deps),
679
- onAgentEnd: createTelegramAgentEndHook<TTurn, TContext, TMessage>(deps),
697
+ onAgentEnd: createTelegramAgentEndHook<
698
+ TTurn,
699
+ TContext,
700
+ TMessage,
701
+ TReplyMarkup
702
+ >(deps),
680
703
  ...createTelegramToolExecutionHooks<TContext>(deps),
681
704
  };
682
705
  }
@@ -737,6 +760,7 @@ export interface TelegramAgentEndOutboundReplyPlan<TReplyMarkup = unknown> {
737
760
 
738
761
  export interface TelegramAgentEndRuntimeDeps<
739
762
  TTurn extends PendingTelegramTurn,
763
+ TReplyMarkup = unknown,
740
764
  > {
741
765
  turn: TTurn | undefined;
742
766
  assistant: TelegramAgentEndAssistantResult;
@@ -750,13 +774,13 @@ export interface TelegramAgentEndRuntimeDeps<
750
774
  chatId: number,
751
775
  markdown: string,
752
776
  replyToMessageId: number,
753
- options?: { replyMarkup?: unknown },
777
+ options?: { replyMarkup?: TReplyMarkup },
754
778
  ) => Promise<boolean>;
755
779
  sendMarkdownReply: (
756
780
  chatId: number,
757
781
  replyToMessageId: number,
758
782
  markdown: string,
759
- options?: { replyMarkup?: unknown },
783
+ options?: { replyMarkup?: TReplyMarkup },
760
784
  ) => Promise<unknown>;
761
785
  sendTextReply: (
762
786
  chatId: number,
@@ -764,7 +788,9 @@ export interface TelegramAgentEndRuntimeDeps<
764
788
  text: string,
765
789
  ) => Promise<unknown>;
766
790
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
767
- planOutboundReply?: (markdown: string) => TelegramAgentEndOutboundReplyPlan;
791
+ planOutboundReply?: (
792
+ markdown: string,
793
+ ) => TelegramAgentEndOutboundReplyPlan<TReplyMarkup>;
768
794
  sendOutboundReplyArtifacts?: (
769
795
  turn: TTurn,
770
796
  plan: TelegramAgentEndOutboundReplyPlan,
@@ -776,6 +802,7 @@ export interface TelegramAgentEndHookRuntimeDeps<
776
802
  TTurn extends PendingTelegramTurn,
777
803
  TContext,
778
804
  TMessage,
805
+ TReplyMarkup = unknown,
779
806
  > {
780
807
  getActiveTurn: () => TTurn | undefined;
781
808
  extractAssistant: (
@@ -790,11 +817,11 @@ export interface TelegramAgentEndHookRuntimeDeps<
790
817
  ) => void;
791
818
  clearPreview: (chatId: number) => Promise<void>;
792
819
  setPreviewPendingText: (text: string) => void;
793
- finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn>["finalizeMarkdownPreview"];
794
- sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn>["sendMarkdownReply"];
820
+ finalizeMarkdownPreview: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["finalizeMarkdownPreview"];
821
+ sendMarkdownReply: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["sendMarkdownReply"];
795
822
  sendTextReply: TelegramAgentEndRuntimeDeps<TTurn>["sendTextReply"];
796
823
  sendQueuedAttachments: (turn: TTurn) => Promise<void>;
797
- planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn>["planOutboundReply"];
824
+ planOutboundReply?: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>["planOutboundReply"];
798
825
  sendOutboundReplyArtifacts?: TelegramAgentEndRuntimeDeps<TTurn>["sendOutboundReplyArtifacts"];
799
826
  }
800
827
 
@@ -872,7 +899,15 @@ export function createTelegramAgentEndHook<
872
899
  TTurn extends PendingTelegramTurn,
873
900
  TContext,
874
901
  TMessage,
875
- >(deps: TelegramAgentEndHookRuntimeDeps<TTurn, TContext, TMessage>) {
902
+ TReplyMarkup = unknown,
903
+ >(
904
+ deps: TelegramAgentEndHookRuntimeDeps<
905
+ TTurn,
906
+ TContext,
907
+ TMessage,
908
+ TReplyMarkup
909
+ >,
910
+ ) {
876
911
  return async function onAgentEnd(
877
912
  event: TelegramAgentEndHookEvent<TMessage>,
878
913
  ctx: TContext,
@@ -903,7 +938,8 @@ export function createTelegramAgentEndHook<
903
938
 
904
939
  export async function handleTelegramAgentEndRuntime<
905
940
  TTurn extends PendingTelegramTurn,
906
- >(deps: TelegramAgentEndRuntimeDeps<TTurn>): Promise<void> {
941
+ TReplyMarkup = unknown,
942
+ >(deps: TelegramAgentEndRuntimeDeps<TTurn, TReplyMarkup>): Promise<void> {
907
943
  const { turn, assistant } = deps;
908
944
  const rawFinalText = assistant.text;
909
945
  const outboundReply = rawFinalText
@@ -934,7 +970,7 @@ export async function handleTelegramAgentEndRuntime<
934
970
  turn.chatId,
935
971
  turn.replyToMessageId,
936
972
  assistant.errorMessage ||
937
- "Telegram bridge: pi failed while processing the request.",
973
+ "Telegram bridge: π failed while processing the request.",
938
974
  );
939
975
  if (endPlan.shouldDispatchNext) deps.dispatchNextQueuedTelegramTurn();
940
976
  return;
package/lib/rendering.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram preview and markdown rendering helpers
3
+ * Zones: telegram rendering, shared text utils
3
4
  * Converts assistant output into Telegram-safe plain text and HTML chunks with chunk-boundary handling
4
5
  */
5
6
 
package/lib/replies.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram reply delivery helpers
3
+ * Zones: telegram outbound, rendering transport
3
4
  * Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
4
5
  */
5
6
 
@@ -10,10 +11,48 @@ import {
10
11
  type TelegramRenderMode,
11
12
  } from "./rendering.ts";
12
13
 
14
+ // --- Reply Dedup ---
15
+
16
+ /** Non-persistent reply deduplication for a single agent turn.
17
+ * First reply to a prompt gets `reply_parameters.reply_to_message_id`;
18
+ * subsequent replies in the same turn skip it to avoid stacking
19
+ * duplicate reply headers in the chat viewport. */
20
+ export interface ReplyDedupRuntime {
21
+ /** Returns true if this is the first reply for the given prompt
22
+ * message id in the current turn. Side-effect: marks it replied. */
23
+ shouldReply(promptMessageId: number): boolean;
24
+ /** Reset the tracker when a new prompt enters the queue. */
25
+ reset(): void;
26
+ }
27
+
28
+ export function createReplyDedupRuntime(): ReplyDedupRuntime {
29
+ const replied = new Map<number, boolean>();
30
+ return {
31
+ shouldReply(promptMessageId: number): boolean {
32
+ if (replied.has(promptMessageId)) return false;
33
+ replied.set(promptMessageId, true);
34
+ return true;
35
+ },
36
+ reset(): void {
37
+ replied.clear();
38
+ },
39
+ };
40
+ }
41
+
42
+ // --- Transport-level dedup ---
43
+
44
+ let lastRepliedToMessageId: number | undefined;
45
+
46
+ export function resetTransportReplyDedup(): void {
47
+ lastRepliedToMessageId = undefined;
48
+ }
49
+
13
50
  export function buildTelegramReplyParameters(
14
51
  messageId: number | undefined,
15
52
  ): TelegramReplyParameters | undefined {
16
53
  if (messageId === undefined) return undefined;
54
+ if (messageId === lastRepliedToMessageId) return undefined;
55
+ lastRepliedToMessageId = messageId;
17
56
  return { message_id: messageId, allow_sending_without_reply: true };
18
57
  }
19
58
 
@@ -219,13 +258,13 @@ export interface TelegramRenderedMessageRuntimeDeps<TReplyMarkup> {
219
258
  export interface TelegramRenderedMessageRuntime<TReplyMarkup> {
220
259
  sendTextReply: (
221
260
  chatId: number,
222
- replyToMessageId: number,
261
+ replyToMessageId: number | undefined,
223
262
  text: string,
224
263
  options?: { parseMode?: "HTML" },
225
264
  ) => Promise<number | undefined>;
226
265
  sendMarkdownReply: (
227
266
  chatId: number,
228
- replyToMessageId: number,
267
+ replyToMessageId: number | undefined,
229
268
  markdown: string,
230
269
  options?: { replyMarkup?: unknown },
231
270
  ) => Promise<number | undefined>;
@@ -332,3 +371,48 @@ export function createTelegramRenderedMessageRuntime<TReplyMarkup>(
332
371
  },
333
372
  };
334
373
  }
374
+
375
+ // --- Dedup-wrapped Reply Wrappers ---
376
+
377
+ /** Wrap a sendTextReply with reply dedup so only the first message
378
+ * in a turn carries `reply_to_message_id`. */
379
+ export function dedupSendTextReply(
380
+ dedup: ReplyDedupRuntime,
381
+ inner: (
382
+ chatId: number,
383
+ replyToMessageId: number | undefined,
384
+ text: string,
385
+ options?: { parseMode?: "HTML" },
386
+ ) => Promise<number | undefined>,
387
+ ): (
388
+ chatId: number,
389
+ replyToMessageId: number,
390
+ text: string,
391
+ options?: { parseMode?: "HTML" },
392
+ ) => Promise<number | undefined> {
393
+ return async (chatId, replyToMessageId, text, options) => {
394
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
395
+ return inner(chatId, effectiveReplyTo, text, options);
396
+ };
397
+ }
398
+
399
+ /** Wrap a sendMarkdownReply with reply dedup. */
400
+ export function dedupSendMarkdownReply<TReplyMarkup = unknown>(
401
+ dedup: ReplyDedupRuntime,
402
+ inner: (
403
+ chatId: number,
404
+ replyToMessageId: number | undefined,
405
+ markdown: string,
406
+ options?: { replyMarkup?: TReplyMarkup },
407
+ ) => Promise<number | undefined>,
408
+ ): (
409
+ chatId: number,
410
+ replyToMessageId: number,
411
+ markdown: string,
412
+ options?: { replyMarkup?: TReplyMarkup },
413
+ ) => Promise<number | undefined> {
414
+ return async (chatId, replyToMessageId, markdown, options) => {
415
+ const effectiveReplyTo = dedup.shouldReply(replyToMessageId) ? replyToMessageId : undefined;
416
+ return inner(chatId, effectiveReplyTo, markdown, options);
417
+ };
418
+ }
package/lib/routing.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram inbound routing composition
3
+ * Zones: telegram inbound, orchestration, queue/menu/command composition
3
4
  * Wires authorized updates into menus, commands, media grouping, and prompt queueing
4
5
  */
5
6
 
@@ -11,6 +12,7 @@ import * as Media from "./media.ts";
11
12
  import * as Menu from "./menu.ts";
12
13
  import * as Model from "./model.ts";
13
14
  import * as Queue from "./queue.ts";
15
+ import * as PromptTemplates from "./prompt-templates.ts";
14
16
  import type { TelegramBridgeRuntime } from "./runtime.ts";
15
17
  import * as Turns from "./turns.ts";
16
18
  import * as Updates from "./updates.ts";
@@ -25,11 +27,6 @@ export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
25
27
  Menu.MenuCallbackQuery;
26
28
 
27
29
  export interface TelegramInboundRouteRuntimeDeps<
28
- TUpdate extends Updates.TelegramUpdateFlow & {
29
- message?: TMessage;
30
- edited_message?: TMessage;
31
- callback_query?: TCallbackQuery;
32
- },
33
30
  TMessage extends TelegramRoutedMessage,
34
31
  TCallbackQuery extends TelegramRoutedCallbackQuery,
35
32
  TContext,
@@ -51,6 +48,15 @@ export interface TelegramInboundRouteRuntimeDeps<
51
48
  Model.ScopedTelegramModel<TModel>
52
49
  >;
53
50
  menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
51
+ openQueueMenu: (
52
+ chatId: number,
53
+ replyToMessageId: number,
54
+ ctx: TContext,
55
+ ) => Promise<void>;
56
+ queueMenuCallbackHandler: (
57
+ query: TCallbackQuery,
58
+ ctx: TContext,
59
+ ) => Promise<boolean>;
54
60
  buttonActionStore?: OutboundHandlers.TelegramButtonActionStore;
55
61
  attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
56
62
  updateStatus: (ctx: TContext, error?: string) => void;
@@ -65,6 +71,7 @@ export interface TelegramInboundRouteRuntimeDeps<
65
71
  text: string,
66
72
  ) => Promise<number | undefined>;
67
73
  setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
74
+ getCommands: () => Parameters<typeof PromptTemplates.getTelegramPromptTemplateCommands>[0];
68
75
  downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
69
76
  getThinkingLevel: () => Model.ThinkingLevel;
70
77
  setThinkingLevel: (level: Model.ThinkingLevel) => void;
@@ -94,7 +101,6 @@ export function createTelegramInboundRouteRuntime<
94
101
  TModel extends Model.MenuModel,
95
102
  >(
96
103
  deps: TelegramInboundRouteRuntimeDeps<
97
- TUpdate,
98
104
  TMessage,
99
105
  TCallbackQuery,
100
106
  TContext,
@@ -159,8 +165,53 @@ export function createTelegramInboundRouteRuntime<
159
165
  );
160
166
  if (handled) return;
161
167
  }
168
+ const handledByQueue = await deps.queueMenuCallbackHandler(query, ctx);
169
+ if (handledByQueue) return;
162
170
  await menuCallbackHandler(query, ctx);
163
171
  };
172
+ const promptTurnBuilder = Turns.createTelegramPromptTurnRuntimeBuilder<
173
+ TMessage,
174
+ TContext
175
+ >({
176
+ allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
177
+ downloadFile: deps.downloadFile,
178
+ processAttachments: deps.attachmentHandlerRuntime.process,
179
+ });
180
+ const enqueueContinueTurn = async (
181
+ message: TMessage,
182
+ ctx: TContext,
183
+ ): Promise<void> => {
184
+ const enqueuePlan = Queue.planTelegramPromptEnqueue(
185
+ deps.telegramQueueStore.getQueuedItems(),
186
+ deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory(),
187
+ );
188
+ deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory(false);
189
+ const continueMessage = {
190
+ ...message,
191
+ text: "continue",
192
+ caption: undefined,
193
+ } as TMessage;
194
+ const turn = await promptTurnBuilder(
195
+ [continueMessage],
196
+ enqueuePlan.historyTurns,
197
+ ctx,
198
+ );
199
+ const continueTurn = {
200
+ ...turn,
201
+ queueLane: "priority" as const,
202
+ laneOrder: Number.MIN_SAFE_INTEGER + turn.queueOrder,
203
+ statusSummary: "continue",
204
+ };
205
+ deps.telegramQueueStore.setQueuedItems(enqueuePlan.remainingItems);
206
+ deps.queueMutationRuntime.append(continueTurn, ctx);
207
+ deps.dispatchNextQueuedTelegramTurn(ctx);
208
+ };
209
+ const reservedCommandNames = new Set(Commands.TELEGRAM_RESERVED_COMMAND_NAMES);
210
+ const getPromptTemplateCommands = () =>
211
+ PromptTemplates.getTelegramPromptTemplateCommands(
212
+ deps.getCommands(),
213
+ reservedCommandNames,
214
+ );
164
215
  const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
165
216
  TMessage,
166
217
  TContext
@@ -181,15 +232,25 @@ export function createTelegramInboundRouteRuntime<
181
232
  deps.bridgeRuntime.lifecycle.setCompactionInProgress,
182
233
  updateStatus: deps.updateStatus,
183
234
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
235
+ enqueueContinueTurn,
184
236
  compact: deps.compact,
185
237
  allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
186
238
  allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
187
239
  appendControlItem: deps.queueMutationRuntime.append,
188
240
  showStatus: deps.menuActions.sendStatusMessage,
189
241
  openModelMenu: deps.menuActions.openModelMenu,
242
+ openThinkingMenu: (message, ctx) => {
243
+ const chatId = (message as { chat: { id: number } }).chat.id;
244
+ return deps.menuActions.openThinkingMenu(chatId, message.message_id, ctx);
245
+ },
246
+ openQueueMenu: (message, ctx) => {
247
+ const chatId = (message as { chat: { id: number } }).chat.id;
248
+ return deps.openQueueMenu(chatId, message.message_id, ctx);
249
+ },
190
250
  getAllowedUserId: deps.configStore.getAllowedUserId,
191
251
  setAllowedUserId: deps.configStore.setAllowedUserId,
192
252
  setMyCommands: deps.setMyCommands,
253
+ getPromptTemplateCommands,
193
254
  persistConfig: deps.configStore.persist,
194
255
  sendTextReply: deps.sendTextReply,
195
256
  recordRuntimeEvent: deps.recordRuntimeEvent,
@@ -203,14 +264,7 @@ export function createTelegramInboundRouteRuntime<
203
264
  deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
204
265
  setPreserveQueuedTurnsAsHistory:
205
266
  deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
206
- createTurn: Turns.createTelegramPromptTurnRuntimeBuilder<
207
- TMessage,
208
- TContext
209
- >({
210
- allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
211
- downloadFile: deps.downloadFile,
212
- processAttachments: deps.attachmentHandlerRuntime.process,
213
- }),
267
+ createTurn: promptTurnBuilder,
214
268
  updateStatus: deps.updateStatus,
215
269
  dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
216
270
  }).enqueue;
@@ -220,6 +274,14 @@ export function createTelegramInboundRouteRuntime<
220
274
  >({
221
275
  extractRawText: Media.extractFirstTelegramMessageText,
222
276
  handleCommand: commandHandler,
277
+ expandPromptTemplateCommand: (commandName, args) =>
278
+ PromptTemplates.expandTelegramPromptTemplateCommand(
279
+ commandName,
280
+ args,
281
+ getPromptTemplateCommands(),
282
+ ),
283
+ replaceMessageText: (message, text) =>
284
+ ({ ...message, text, caption: undefined }) as TMessage,
223
285
  enqueueTurn: promptEnqueue,
224
286
  });
225
287
  const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
package/lib/runtime.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram bridge runtime-state helpers
3
+ * Zones: pi agent runtime state, telegram session, shared coordination
3
4
  * Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
4
5
  */
5
6
 
@@ -350,6 +351,7 @@ export function createTelegramTypingLoopStarter<TContext>(
350
351
  } catch (error) {
351
352
  const message =
352
353
  error instanceof Error ? error.message : String(error);
354
+ deps.updateStatus(ctx, message);
353
355
  deps.recordRuntimeEvent?.("typing", error, {
354
356
  chatId: targetChatId,
355
357
  });
package/lib/setup.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram setup prompt helpers
3
+ * Zones: pi agent command ui, telegram config
3
4
  * Computes token-prefill defaults and prompt mode selection for /telegram-setup
4
5
  */
5
6
 
package/lib/status.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram status rendering helpers
3
+ * Zones: telegram ui, pi agent diagnostics, tui
3
4
  * Builds usage, cost, and context summaries for the interactive Telegram status view
4
5
  */
5
6
 
@@ -41,6 +42,8 @@ export interface TelegramStatusActiveModel {
41
42
  export interface TelegramStatusContext {
42
43
  sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
43
44
  getContextUsage(): TelegramContextUsage | undefined;
45
+ isIdle?: () => boolean;
46
+ hasPendingMessages?: () => boolean;
44
47
  modelRegistry: {
45
48
  isUsingOAuth(model: TelegramStatusActiveModel): boolean;
46
49
  };
@@ -406,12 +409,14 @@ export function buildTelegramStatusBarText(
406
409
  return `${label} ${theme.fg("muted", "disconnected")}`;
407
410
  if (!state.paired)
408
411
  return `${label} ${theme.fg("warning", "awaiting pairing")}`;
409
- const queued = theme.fg("muted", state.queuedStatus);
412
+ const queued = state.queuedStatus ? theme.fg("muted", state.queuedStatus) : "";
410
413
  if (state.compactionInProgress) {
411
414
  return `${label} ${theme.fg("accent", "compacting")}${queued}`;
412
415
  }
413
416
  if (state.processing) {
414
- return `${label} ${theme.fg("accent", state.processingStatus ?? "processing")}${queued}`;
417
+ const processingStatus = state.processingStatus ?? "processing";
418
+ const processingToken = processingStatus === "active" ? "warning" : "accent";
419
+ return `${label} ${theme.fg(processingToken, processingStatus)}${queued}`;
415
420
  }
416
421
  return `${label} ${theme.fg("success", "connected")}`;
417
422
  }
@@ -528,6 +533,13 @@ function buildContextSummary(
528
533
  return `${percent}/${formatTokens(contextWindow)}`;
529
534
  }
530
535
 
536
+ function buildStatusSummary(ctx: TelegramStatusContext): string {
537
+ if (ctx.hasPendingMessages?.()) return "pending";
538
+ if (ctx.isIdle?.() === false) return "active";
539
+ if (ctx.isIdle?.() === true) return "idle";
540
+ return "unknown";
541
+ }
542
+
531
543
  export function buildStatusHtml(
532
544
  ctx: TelegramStatusContext,
533
545
  activeModel: TelegramStatusActiveModel | undefined,
@@ -536,7 +548,7 @@ export function buildStatusHtml(
536
548
  const usesSubscription = activeModel
537
549
  ? ctx.modelRegistry.isUsingOAuth(activeModel)
538
550
  : false;
539
- const lines: string[] = [];
551
+ const lines: string[] = [buildStatusRow("Status", buildStatusSummary(ctx))];
540
552
  const usageSummary = buildUsageSummary(stats);
541
553
  const costSummary = buildCostSummary(stats, usesSubscription);
542
554
  if (usageSummary) {
@@ -546,8 +558,5 @@ export function buildStatusHtml(
546
558
  lines.push(buildStatusRow("Cost", costSummary));
547
559
  }
548
560
  lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
549
- if (lines.length === 0) {
550
- lines.push(buildStatusRow("Status", "No usage data yet."));
551
- }
552
561
  return lines.join("\n");
553
562
  }
package/lib/turns.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram turn-building helpers
3
+ * Zones: telegram inbound, pi agent prompt content, queue
3
4
  * Owns prompt-turn summary and content construction so queued Telegram turns are assembled consistently
4
5
  */
5
6
 
package/lib/updates.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  /**
2
2
  * Telegram updates domain helpers
3
+ * Zones: telegram inbound, authorization, routing plans
3
4
  * Owns update extraction, authorization, classification, execution planning, and runtime execution for Telegram updates
4
5
  */
5
6
 
@@ -25,6 +26,9 @@ export type TelegramReactionType =
25
26
  | TelegramReactionTypeEmoji
26
27
  | TelegramReactionTypeNonEmoji;
27
28
 
29
+ export const TELEGRAM_PRIORITY_REACTION_EMOJIS = ["👍", "⚡", "❤", "🕊"] as const;
30
+ export const TELEGRAM_REMOVAL_REACTION_EMOJIS = ["👎", "👻", "💔", "💩"] as const;
31
+
28
32
  export interface TelegramUpdateDeletion {
29
33
  deleted_business_messages?: { message_ids?: unknown };
30
34
  }
@@ -50,6 +54,21 @@ export function collectTelegramReactionEmojis(
50
54
  );
51
55
  }
52
56
 
57
+ function hasAnyTelegramReactionEmoji(
58
+ emojis: Set<string>,
59
+ candidates: readonly string[],
60
+ ): boolean {
61
+ return candidates.some((emoji) => emojis.has(emoji));
62
+ }
63
+
64
+ function hasAddedTelegramReactionEmoji(
65
+ oldEmojis: Set<string>,
66
+ newEmojis: Set<string>,
67
+ candidates: readonly string[],
68
+ ): boolean {
69
+ return candidates.some((emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji));
70
+ }
71
+
53
72
  export function extractDeletedTelegramMessageIds(
54
73
  update: TelegramUpdateDeletion,
55
74
  ): number[] {
@@ -579,8 +598,13 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
579
598
  }
580
599
  const oldEmojis = collectTelegramReactionEmojis(reactionUpdate.old_reaction);
581
600
  const newEmojis = collectTelegramReactionEmojis(reactionUpdate.new_reaction);
582
- const dislikeAdded = !oldEmojis.has("👎") && newEmojis.has("👎");
583
- if (dislikeAdded) {
601
+ if (
602
+ hasAddedTelegramReactionEmoji(
603
+ oldEmojis,
604
+ newEmojis,
605
+ TELEGRAM_REMOVAL_REACTION_EMOJIS,
606
+ )
607
+ ) {
584
608
  deps.removePendingMediaGroupMessages([reactionUpdate.message_id]);
585
609
  deps.removeQueuedTelegramTurnsByMessageIds(
586
610
  [reactionUpdate.message_id],
@@ -588,15 +612,21 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
588
612
  );
589
613
  return;
590
614
  }
591
- const likeRemoved = oldEmojis.has("👍") && !newEmojis.has("👍");
592
- if (likeRemoved) {
615
+ const hadPriorityReaction = hasAnyTelegramReactionEmoji(
616
+ oldEmojis,
617
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
618
+ );
619
+ const hasPriorityReaction = hasAnyTelegramReactionEmoji(
620
+ newEmojis,
621
+ TELEGRAM_PRIORITY_REACTION_EMOJIS,
622
+ );
623
+ if (hadPriorityReaction && !hasPriorityReaction) {
593
624
  deps.clearQueuedTelegramTurnPriorityByMessageId(
594
625
  reactionUpdate.message_id,
595
626
  deps.ctx,
596
627
  );
597
628
  }
598
- const likeAdded = !oldEmojis.has("👍") && newEmojis.has("👍");
599
- if (!likeAdded) return;
629
+ if (!hasAddedTelegramReactionEmoji(oldEmojis, newEmojis, TELEGRAM_PRIORITY_REACTION_EMOJIS)) return;
600
630
  deps.prioritizeQueuedTelegramTurnByMessageId(
601
631
  reactionUpdate.message_id,
602
632
  deps.ctx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.6.3",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "description": "Better Telegram DM bridge extension for pi",
6
6
  "type": "module",
@@ -31,6 +31,9 @@
31
31
  "index.ts",
32
32
  "lib/",
33
33
  "README.md",
34
+ "AGENTS.md",
35
+ "BACKLOG.md",
36
+ "CHANGELOG.md",
34
37
  "docs/",
35
38
  "screenshot.png"
36
39
  ],