@llblab/pi-telegram 0.2.9 → 0.3.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/status.ts CHANGED
@@ -3,8 +3,7 @@
3
3
  * Builds usage, cost, and context summaries for the interactive Telegram status view
4
4
  */
5
5
 
6
- import type { Model } from "@mariozechner/pi-ai";
7
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ export type TelegramStatusQueueLane = "control" | "priority" | "default";
8
7
 
9
8
  export interface TelegramUsageStats {
10
9
  totalInput: number;
@@ -14,6 +13,417 @@ export interface TelegramUsageStats {
14
13
  totalCost: number;
15
14
  }
16
15
 
16
+ interface TelegramUsageMessage {
17
+ role: string;
18
+ usage?: {
19
+ input: number;
20
+ output: number;
21
+ cacheRead: number;
22
+ cacheWrite: number;
23
+ cost: { total: number };
24
+ };
25
+ }
26
+
27
+ interface TelegramStatusSessionEntry {
28
+ type: string;
29
+ message?: TelegramUsageMessage;
30
+ }
31
+
32
+ interface TelegramContextUsage {
33
+ contextWindow?: number;
34
+ percent: number | null;
35
+ }
36
+
37
+ export interface TelegramStatusActiveModel {
38
+ contextWindow?: number;
39
+ }
40
+
41
+ export interface TelegramStatusContext {
42
+ sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
43
+ getContextUsage(): TelegramContextUsage | undefined;
44
+ modelRegistry: {
45
+ isUsingOAuth(model: TelegramStatusActiveModel): boolean;
46
+ };
47
+ }
48
+
49
+ export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
50
+
51
+ const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
52
+
53
+ export interface TelegramRuntimeEvent {
54
+ at: number;
55
+ category: string;
56
+ message: string;
57
+ details?: Record<string, TelegramRuntimeEventDetailValue>;
58
+ }
59
+
60
+ export interface TelegramRuntimeEventInput {
61
+ category: string;
62
+ error?: unknown;
63
+ message?: string;
64
+ details?: Record<string, unknown>;
65
+ }
66
+
67
+ export interface TelegramRuntimeEventRecorder {
68
+ record: (
69
+ category: string,
70
+ error: unknown,
71
+ details?: Record<string, unknown>,
72
+ ) => void;
73
+ getEvents: () => TelegramRuntimeEvent[];
74
+ clear: () => void;
75
+ }
76
+
77
+ export interface TelegramRuntimeEventRecorderOptions {
78
+ getBotToken: () => string | undefined;
79
+ maxEvents?: number;
80
+ now?: () => number;
81
+ }
82
+
83
+ export interface TelegramBridgeStatusLineState {
84
+ botUsername?: string;
85
+ allowedUserId?: number;
86
+ pollingActive: boolean;
87
+ lastUpdateId?: number;
88
+ activeSourceMessageIds?: number[];
89
+ pendingDispatch: boolean;
90
+ compactionInProgress: boolean;
91
+ activeToolExecutions: number;
92
+ pendingModelSwitch: boolean;
93
+ queuedItems: Array<{ queueLane: TelegramStatusQueueLane }>;
94
+ recentRuntimeEvents: TelegramRuntimeEvent[];
95
+ }
96
+
97
+ export interface TelegramStatusBarTheme {
98
+ fg: (
99
+ token: "accent" | "error" | "muted" | "warning" | "success",
100
+ text: string,
101
+ ) => string;
102
+ }
103
+
104
+ export interface TelegramStatusBarState {
105
+ hasBotToken: boolean;
106
+ pollingActive: boolean;
107
+ paired: boolean;
108
+ compactionInProgress: boolean;
109
+ processing: boolean;
110
+ queuedStatus: string;
111
+ error?: string;
112
+ }
113
+
114
+ export interface TelegramStatusRuntimeContext {
115
+ ui: {
116
+ theme: TelegramStatusBarTheme;
117
+ setStatus: (key: string, text: string) => void;
118
+ };
119
+ }
120
+
121
+ export interface TelegramStatusRuntimeDeps<
122
+ TContext extends TelegramStatusRuntimeContext,
123
+ > {
124
+ statusKey?: string;
125
+ getStatusBarState: (ctx: TContext, error?: string) => TelegramStatusBarState;
126
+ getBridgeStatusLineState: () => TelegramBridgeStatusLineState;
127
+ }
128
+
129
+ export interface TelegramBridgeStatusConfig {
130
+ botToken?: string;
131
+ botUsername?: string;
132
+ allowedUserId?: number;
133
+ lastUpdateId?: number;
134
+ }
135
+
136
+ export interface TelegramBridgeStatusRuntimeDeps<
137
+ TQueueItem extends { queueLane: TelegramStatusQueueLane },
138
+ > {
139
+ statusKey?: string;
140
+ getConfig: () => TelegramBridgeStatusConfig;
141
+ isPollingActive: () => boolean;
142
+ getActiveSourceMessageIds: () => number[] | undefined;
143
+ hasActiveTurn: () => boolean;
144
+ hasDispatchPending: () => boolean;
145
+ isCompactionInProgress: () => boolean;
146
+ getActiveToolExecutions: () => number;
147
+ hasPendingModelSwitch: () => boolean;
148
+ getQueuedItems: () => TQueueItem[];
149
+ formatQueuedStatus: (items: TQueueItem[]) => string;
150
+ getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
151
+ }
152
+
153
+ export interface TelegramStatusRuntime<
154
+ TContext extends TelegramStatusRuntimeContext,
155
+ > {
156
+ updateStatus: (ctx: TContext, error?: string) => void;
157
+ getStatusLines: () => string[];
158
+ }
159
+
160
+ export function redactTelegramRuntimeMessage(
161
+ message: string,
162
+ botToken: string | undefined,
163
+ ): string {
164
+ if (!botToken) return message;
165
+ return message.split(botToken).join("<redacted-token>");
166
+ }
167
+
168
+ function normalizeTelegramRuntimeEventDetails(
169
+ details: Record<string, unknown> | undefined,
170
+ botToken: string | undefined,
171
+ ): Record<string, TelegramRuntimeEventDetailValue> | undefined {
172
+ if (!details) return undefined;
173
+ const normalized: Record<string, TelegramRuntimeEventDetailValue> = {};
174
+ for (const [key, value] of Object.entries(details)) {
175
+ if (value === undefined) continue;
176
+ if (typeof value === "string") {
177
+ normalized[key] = redactTelegramRuntimeMessage(value, botToken);
178
+ continue;
179
+ }
180
+ if (typeof value === "number" || typeof value === "boolean") {
181
+ normalized[key] = value;
182
+ continue;
183
+ }
184
+ if (value === null) {
185
+ normalized[key] = null;
186
+ continue;
187
+ }
188
+ normalized[key] = redactTelegramRuntimeMessage(String(value), botToken);
189
+ }
190
+ return Object.keys(normalized).length > 0 ? normalized : undefined;
191
+ }
192
+
193
+ function getTelegramRuntimeEventMessage(
194
+ input: TelegramRuntimeEventInput,
195
+ ): string {
196
+ if (input.message !== undefined) return input.message;
197
+ if (input.error instanceof Error) return input.error.message;
198
+ return String(input.error);
199
+ }
200
+
201
+ export function recordStructuredTelegramRuntimeEvent(
202
+ events: TelegramRuntimeEvent[],
203
+ input: TelegramRuntimeEventInput,
204
+ options: { botToken?: string; maxEvents: number; now?: number },
205
+ ): void {
206
+ const details = normalizeTelegramRuntimeEventDetails(
207
+ input.details,
208
+ options.botToken,
209
+ );
210
+ events.push({
211
+ at: options.now ?? Date.now(),
212
+ category: input.category,
213
+ message: redactTelegramRuntimeMessage(
214
+ getTelegramRuntimeEventMessage(input),
215
+ options.botToken,
216
+ ),
217
+ ...(details ? { details } : {}),
218
+ });
219
+ while (events.length > options.maxEvents) {
220
+ events.shift();
221
+ }
222
+ }
223
+
224
+ export function recordTelegramRuntimeEvent(
225
+ events: TelegramRuntimeEvent[],
226
+ category: string,
227
+ error: unknown,
228
+ options: { botToken?: string; maxEvents: number; now?: number },
229
+ ): void {
230
+ recordStructuredTelegramRuntimeEvent(events, { category, error }, options);
231
+ }
232
+
233
+ export function createTelegramRuntimeEventRecorder(
234
+ options: TelegramRuntimeEventRecorderOptions,
235
+ ): TelegramRuntimeEventRecorder {
236
+ const events: TelegramRuntimeEvent[] = [];
237
+ return {
238
+ record: (category, error, details) => {
239
+ recordStructuredTelegramRuntimeEvent(
240
+ events,
241
+ { category, error, details },
242
+ {
243
+ botToken: options.getBotToken(),
244
+ maxEvents: options.maxEvents ?? MAX_RECENT_TELEGRAM_RUNTIME_EVENTS,
245
+ now: options.now?.(),
246
+ },
247
+ );
248
+ },
249
+ getEvents: () => events,
250
+ clear: () => {
251
+ events.length = 0;
252
+ },
253
+ };
254
+ }
255
+
256
+ function formatTelegramRuntimeEventCategory(
257
+ event: TelegramRuntimeEvent,
258
+ ): string {
259
+ const method = event.details?.method;
260
+ return typeof method === "string"
261
+ ? `${event.category}:${method}`
262
+ : event.category;
263
+ }
264
+
265
+ function formatTelegramRuntimeEventDetails(
266
+ event: TelegramRuntimeEvent,
267
+ ): string {
268
+ if (!event.details) return "";
269
+ const details = Object.entries(event.details)
270
+ .filter(([key]) => key !== "method")
271
+ .map(([key, value]) => `${key}=${JSON.stringify(value)}`);
272
+ return details.length > 0 ? ` (${details.join(", ")})` : "";
273
+ }
274
+
275
+ function formatTelegramRuntimeEventSummary(
276
+ event: TelegramRuntimeEvent,
277
+ ): string {
278
+ return `${formatTelegramRuntimeEventCategory(event)}: ${event.message}${formatTelegramRuntimeEventDetails(event)}`;
279
+ }
280
+
281
+ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
282
+ return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
283
+ }
284
+
285
+ export function buildTelegramRuntimeEventLines(
286
+ events: TelegramRuntimeEvent[],
287
+ ): string[] {
288
+ if (events.length === 0) return ["recent runtime events: none"];
289
+ return [
290
+ "recent runtime events:",
291
+ ...events
292
+ .slice()
293
+ .reverse()
294
+ .map((event) => `- ${formatTelegramRuntimeEvent(event)}`),
295
+ ];
296
+ }
297
+
298
+ export function createTelegramStatusHtmlBuilder<TContext>(deps: {
299
+ getActiveModel: (ctx: TContext) => TelegramStatusActiveModel | undefined;
300
+ }): (ctx: TContext & TelegramStatusContext) => string {
301
+ return (ctx) => buildStatusHtml(ctx, deps.getActiveModel(ctx));
302
+ }
303
+
304
+ export function createTelegramStatusRuntime<
305
+ TContext extends TelegramStatusRuntimeContext,
306
+ >(deps: TelegramStatusRuntimeDeps<TContext>): TelegramStatusRuntime<TContext> {
307
+ const statusKey = deps.statusKey ?? "telegram";
308
+ return {
309
+ updateStatus: (ctx, error) => {
310
+ ctx.ui.setStatus(
311
+ statusKey,
312
+ buildTelegramStatusBarText(
313
+ ctx.ui.theme,
314
+ deps.getStatusBarState(ctx, error),
315
+ ),
316
+ );
317
+ },
318
+ getStatusLines: () =>
319
+ buildTelegramBridgeStatusLines(deps.getBridgeStatusLineState()),
320
+ };
321
+ }
322
+
323
+ export function createTelegramBridgeStatusRuntime<
324
+ TContext extends TelegramStatusRuntimeContext,
325
+ TQueueItem extends { queueLane: TelegramStatusQueueLane },
326
+ >(
327
+ deps: TelegramBridgeStatusRuntimeDeps<TQueueItem>,
328
+ ): TelegramStatusRuntime<TContext> {
329
+ return createTelegramStatusRuntime({
330
+ statusKey: deps.statusKey,
331
+ getStatusBarState: (_ctx, error) => {
332
+ const config = deps.getConfig();
333
+ const queuedItems = deps.getQueuedItems();
334
+ const compactionInProgress = deps.isCompactionInProgress();
335
+ return {
336
+ hasBotToken: !!config.botToken,
337
+ pollingActive: deps.isPollingActive(),
338
+ paired: !!config.allowedUserId,
339
+ compactionInProgress,
340
+ processing:
341
+ deps.hasActiveTurn() ||
342
+ deps.hasDispatchPending() ||
343
+ queuedItems.length > 0,
344
+ queuedStatus: deps.formatQueuedStatus(queuedItems),
345
+ error,
346
+ };
347
+ },
348
+ getBridgeStatusLineState: () => {
349
+ const config = deps.getConfig();
350
+ return {
351
+ botUsername: config.botUsername,
352
+ allowedUserId: config.allowedUserId,
353
+ pollingActive: deps.isPollingActive(),
354
+ lastUpdateId: config.lastUpdateId,
355
+ activeSourceMessageIds: deps.getActiveSourceMessageIds(),
356
+ pendingDispatch: deps.hasDispatchPending(),
357
+ compactionInProgress: deps.isCompactionInProgress(),
358
+ activeToolExecutions: deps.getActiveToolExecutions(),
359
+ pendingModelSwitch: deps.hasPendingModelSwitch(),
360
+ queuedItems: deps.getQueuedItems(),
361
+ recentRuntimeEvents: deps.getRecentRuntimeEvents(),
362
+ };
363
+ },
364
+ });
365
+ }
366
+
367
+ export function buildTelegramStatusBarText(
368
+ theme: TelegramStatusBarTheme,
369
+ state: TelegramStatusBarState,
370
+ ): string {
371
+ const label = theme.fg("accent", "telegram");
372
+ if (state.error) {
373
+ return `${label} ${theme.fg("error", "error")} ${theme.fg("muted", state.error)}`;
374
+ }
375
+ if (!state.hasBotToken)
376
+ return `${label} ${theme.fg("muted", "not configured")}`;
377
+ if (!state.pollingActive)
378
+ return `${label} ${theme.fg("muted", "disconnected")}`;
379
+ if (!state.paired)
380
+ return `${label} ${theme.fg("warning", "awaiting pairing")}`;
381
+ const queued = theme.fg("muted", state.queuedStatus);
382
+ if (state.compactionInProgress) {
383
+ return `${label} ${theme.fg("accent", "compacting")}${queued}`;
384
+ }
385
+ if (state.processing) {
386
+ return `${label} ${theme.fg("accent", "processing")}${queued}`;
387
+ }
388
+ return `${label} ${theme.fg("success", "connected")}`;
389
+ }
390
+
391
+ export function buildTelegramBridgeStatusLines(
392
+ state: TelegramBridgeStatusLineState,
393
+ ): string[] {
394
+ const controlQueueCount = state.queuedItems.filter(
395
+ (item) => item.queueLane === "control",
396
+ ).length;
397
+ const priorityQueueCount = state.queuedItems.filter(
398
+ (item) => item.queueLane === "priority",
399
+ ).length;
400
+ const defaultQueueCount = state.queuedItems.filter(
401
+ (item) => item.queueLane === "default",
402
+ ).length;
403
+ return [
404
+ "connection:",
405
+ `- bot: ${state.botUsername ? `@${state.botUsername}` : "not configured"}`,
406
+ `- allowed user: ${state.allowedUserId ?? "not paired"}`,
407
+ "",
408
+ "polling:",
409
+ `- state: ${state.pollingActive ? "running" : "stopped"}`,
410
+ `- last update id: ${state.lastUpdateId ?? "none"}`,
411
+ "",
412
+ "execution:",
413
+ `- active turn: ${state.activeSourceMessageIds?.join(",") || "no"}`,
414
+ `- pending dispatch: ${state.pendingDispatch ? "yes" : "no"}`,
415
+ `- compaction: ${state.compactionInProgress ? "running" : "idle"}`,
416
+ `- active tools: ${state.activeToolExecutions}`,
417
+ `- pending model switch: ${state.pendingModelSwitch ? "yes" : "no"}`,
418
+ "",
419
+ "queue:",
420
+ `- queued turns: ${state.queuedItems.length}`,
421
+ `- lanes: control=${controlQueueCount}, priority=${priorityQueueCount}, default=${defaultQueueCount}`,
422
+ "",
423
+ ...buildTelegramRuntimeEventLines(state.recentRuntimeEvents),
424
+ ];
425
+ }
426
+
17
427
  function escapeHtml(text: string): string {
18
428
  return text
19
429
  .replace(/&/g, "&amp;")
@@ -29,7 +439,7 @@ function formatTokens(count: number): string {
29
439
  return `${Math.round(count / 1000000)}M`;
30
440
  }
31
441
 
32
- export function collectUsageStats(ctx: ExtensionContext): TelegramUsageStats {
442
+ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
33
443
  const stats: TelegramUsageStats = {
34
444
  totalInput: 0,
35
445
  totalOutput: 0,
@@ -38,14 +448,19 @@ export function collectUsageStats(ctx: ExtensionContext): TelegramUsageStats {
38
448
  totalCost: 0,
39
449
  };
40
450
  for (const entry of ctx.sessionManager.getEntries()) {
41
- if (entry.type !== "message" || entry.message.role !== "assistant") {
451
+ const usage = entry.message?.usage;
452
+ if (
453
+ entry.type !== "message" ||
454
+ entry.message?.role !== "assistant" ||
455
+ !usage
456
+ ) {
42
457
  continue;
43
458
  }
44
- stats.totalInput += entry.message.usage.input;
45
- stats.totalOutput += entry.message.usage.output;
46
- stats.totalCacheRead += entry.message.usage.cacheRead;
47
- stats.totalCacheWrite += entry.message.usage.cacheWrite;
48
- stats.totalCost += entry.message.usage.cost.total;
459
+ stats.totalInput += usage.input;
460
+ stats.totalOutput += usage.output;
461
+ stats.totalCacheRead += usage.cacheRead;
462
+ stats.totalCacheWrite += usage.cacheWrite;
463
+ stats.totalCost += usage.cost.total;
49
464
  }
50
465
  return stats;
51
466
  }
@@ -74,8 +489,8 @@ function buildCostSummary(
74
489
  }
75
490
 
76
491
  function buildContextSummary(
77
- ctx: ExtensionContext,
78
- activeModel: Model<any> | undefined,
492
+ ctx: TelegramStatusContext,
493
+ activeModel: TelegramStatusActiveModel | undefined,
79
494
  ): string {
80
495
  const usage = ctx.getContextUsage();
81
496
  if (!usage) return "unknown";
@@ -85,8 +500,8 @@ function buildContextSummary(
85
500
  }
86
501
 
87
502
  export function buildStatusHtml(
88
- ctx: ExtensionContext,
89
- activeModel: Model<any> | undefined,
503
+ ctx: TelegramStatusContext,
504
+ activeModel: TelegramStatusActiveModel | undefined,
90
505
  ): string {
91
506
  const stats = collectUsageStats(ctx);
92
507
  const usesSubscription = activeModel