@llblab/pi-telegram 0.13.2 → 0.15.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/AGENTS.md +5 -3
- package/CHANGELOG.md +19 -12
- package/README.md +13 -7
- package/api/status.ts +12 -0
- package/docs/architecture.md +6 -5
- package/docs/locks.md +4 -2
- package/docs/outbound.md +6 -3
- package/docs/public-api.md +32 -6
- package/docs/sections.md +4 -4
- package/index.ts +33 -18
- package/lib/bindings.ts +32 -3
- package/lib/command-templates.ts +155 -10
- package/lib/commands.ts +14 -12
- package/lib/lifecycle.ts +34 -0
- package/lib/locks.ts +16 -2
- package/lib/outbound-attachments.ts +253 -31
- package/lib/prompts.ts +4 -4
- package/lib/queue.ts +33 -32
- package/lib/routing.ts +7 -7
- package/lib/runtime.ts +45 -17
- package/lib/sections.ts +95 -31
- package/lib/status.ts +89 -4
- package/package.json +2 -1
package/lib/runtime.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const TELEGRAM_TYPING_ACTION_INTERVAL_MS =
|
|
7
|
+
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 3000;
|
|
8
|
+
const TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS = 300;
|
|
8
9
|
|
|
9
10
|
export interface TelegramRuntimeQueueCounters {
|
|
10
11
|
nextQueuedTelegramItemOrder: number;
|
|
@@ -16,7 +17,7 @@ export interface TelegramRuntimeLifecycleFlags {
|
|
|
16
17
|
activeTelegramToolExecutions: number;
|
|
17
18
|
telegramTurnDispatchPending: boolean;
|
|
18
19
|
compactionInProgress: boolean;
|
|
19
|
-
|
|
20
|
+
foldQueuedPromptsIntoHistory: boolean;
|
|
20
21
|
setupInProgress: boolean;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -24,6 +25,7 @@ export interface TelegramBridgeRuntimeState
|
|
|
24
25
|
extends TelegramRuntimeQueueCounters, TelegramRuntimeLifecycleFlags {
|
|
25
26
|
abortHandler?: () => void;
|
|
26
27
|
typingInterval?: ReturnType<typeof setInterval>;
|
|
28
|
+
typingInFlight?: Promise<void>;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface TelegramRuntimeQueuePort {
|
|
@@ -44,8 +46,8 @@ export interface TelegramRuntimeLifecyclePort {
|
|
|
44
46
|
clearDispatchPending: () => void;
|
|
45
47
|
isCompactionInProgress: () => boolean;
|
|
46
48
|
setCompactionInProgress: (inProgress: boolean) => void;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
+
shouldFoldQueuedPromptsIntoHistory: () => boolean;
|
|
50
|
+
setFoldQueuedPromptsIntoHistory: (fold: boolean) => void;
|
|
49
51
|
}
|
|
50
52
|
|
|
51
53
|
export interface TelegramRuntimeSetupPort {
|
|
@@ -65,6 +67,7 @@ export interface TelegramRuntimeAbortPort {
|
|
|
65
67
|
export interface TelegramRuntimeTypingPort {
|
|
66
68
|
start: (deps: TelegramTypingLoopDeps) => boolean;
|
|
67
69
|
stop: () => boolean;
|
|
70
|
+
waitForIdle: () => Promise<void>;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export interface TelegramBridgeRuntime {
|
|
@@ -84,7 +87,7 @@ export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
|
|
|
84
87
|
activeTelegramToolExecutions: 0,
|
|
85
88
|
telegramTurnDispatchPending: false,
|
|
86
89
|
compactionInProgress: false,
|
|
87
|
-
|
|
90
|
+
foldQueuedPromptsIntoHistory: false,
|
|
88
91
|
setupInProgress: false,
|
|
89
92
|
};
|
|
90
93
|
}
|
|
@@ -117,10 +120,10 @@ export function createTelegramBridgeRuntime(
|
|
|
117
120
|
isCompactionInProgress: () => isTelegramCompactionInProgress(state),
|
|
118
121
|
setCompactionInProgress: (inProgress) =>
|
|
119
122
|
setTelegramCompactionInProgress(state, inProgress),
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
shouldFoldQueuedPromptsIntoHistory: () =>
|
|
124
|
+
shouldFoldQueuedPromptsIntoHistory(state),
|
|
125
|
+
setFoldQueuedPromptsIntoHistory: (fold) =>
|
|
126
|
+
setFoldQueuedPromptsIntoHistory(state, fold),
|
|
124
127
|
},
|
|
125
128
|
setup: {
|
|
126
129
|
isInProgress: () => isTelegramSetupInProgress(state),
|
|
@@ -138,6 +141,7 @@ export function createTelegramBridgeRuntime(
|
|
|
138
141
|
typing: {
|
|
139
142
|
start: (deps) => startTelegramTypingLoop(state, deps),
|
|
140
143
|
stop: () => stopTelegramTypingLoop(state),
|
|
144
|
+
waitForIdle: () => waitForTelegramTypingLoopIdle(state),
|
|
141
145
|
},
|
|
142
146
|
};
|
|
143
147
|
}
|
|
@@ -195,8 +199,8 @@ export function syncTelegramLifecycleRuntimeFlags(
|
|
|
195
199
|
if (flags.compactionInProgress !== undefined) {
|
|
196
200
|
state.compactionInProgress = flags.compactionInProgress;
|
|
197
201
|
}
|
|
198
|
-
if (flags.
|
|
199
|
-
state.
|
|
202
|
+
if (flags.foldQueuedPromptsIntoHistory !== undefined) {
|
|
203
|
+
state.foldQueuedPromptsIntoHistory = flags.foldQueuedPromptsIntoHistory;
|
|
200
204
|
}
|
|
201
205
|
if (flags.setupInProgress !== undefined) {
|
|
202
206
|
state.setupInProgress = flags.setupInProgress;
|
|
@@ -254,17 +258,17 @@ export function setTelegramCompactionInProgress(
|
|
|
254
258
|
state.compactionInProgress = inProgress;
|
|
255
259
|
}
|
|
256
260
|
|
|
257
|
-
export function
|
|
261
|
+
export function shouldFoldQueuedPromptsIntoHistory(
|
|
258
262
|
state: TelegramBridgeRuntimeState,
|
|
259
263
|
): boolean {
|
|
260
|
-
return state.
|
|
264
|
+
return state.foldQueuedPromptsIntoHistory;
|
|
261
265
|
}
|
|
262
266
|
|
|
263
|
-
export function
|
|
267
|
+
export function setFoldQueuedPromptsIntoHistory(
|
|
264
268
|
state: TelegramBridgeRuntimeState,
|
|
265
|
-
|
|
269
|
+
fold: boolean,
|
|
266
270
|
): void {
|
|
267
|
-
state.
|
|
271
|
+
state.foldQueuedPromptsIntoHistory = fold;
|
|
268
272
|
}
|
|
269
273
|
|
|
270
274
|
export function isTelegramSetupInProgress(
|
|
@@ -392,7 +396,13 @@ export function startTelegramTypingLoop(
|
|
|
392
396
|
if (state.typingInterval || deps.chatId === undefined || deps.chatId === 0)
|
|
393
397
|
return false;
|
|
394
398
|
const sendTyping = (): void => {
|
|
395
|
-
|
|
399
|
+
const typing = Promise.resolve(deps.sendTypingAction(deps.chatId as number))
|
|
400
|
+
.then(() => undefined)
|
|
401
|
+
.catch(() => undefined);
|
|
402
|
+
state.typingInFlight = typing;
|
|
403
|
+
void typing.finally(() => {
|
|
404
|
+
if (state.typingInFlight === typing) state.typingInFlight = undefined;
|
|
405
|
+
});
|
|
396
406
|
};
|
|
397
407
|
sendTyping();
|
|
398
408
|
state.typingInterval = setInterval(sendTyping, deps.intervalMs);
|
|
@@ -408,6 +418,24 @@ export function stopTelegramTypingLoop(
|
|
|
408
418
|
return true;
|
|
409
419
|
}
|
|
410
420
|
|
|
421
|
+
export async function waitForTelegramTypingLoopIdle(
|
|
422
|
+
state: TelegramBridgeRuntimeState,
|
|
423
|
+
timeoutMs = TELEGRAM_TYPING_IDLE_DRAIN_MAX_MS,
|
|
424
|
+
): Promise<void> {
|
|
425
|
+
const inFlight = state.typingInFlight;
|
|
426
|
+
if (!inFlight) return;
|
|
427
|
+
if (timeoutMs <= 0) {
|
|
428
|
+
await Promise.race([inFlight, Promise.resolve()]);
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
await Promise.race([
|
|
432
|
+
inFlight,
|
|
433
|
+
new Promise<void>((resolve) => {
|
|
434
|
+
setTimeout(resolve, timeoutMs);
|
|
435
|
+
}),
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
|
|
411
439
|
export function createTelegramContextAbortHandlerSetter<
|
|
412
440
|
TContext extends { abort: () => void },
|
|
413
441
|
>(
|
package/lib/sections.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface TelegramSectionDiagnostic {
|
|
|
94
94
|
id: TelegramSectionId;
|
|
95
95
|
token: TelegramSectionToken;
|
|
96
96
|
label: string;
|
|
97
|
-
status: "active" | "
|
|
97
|
+
status: "active" | "error";
|
|
98
98
|
lastError?: string;
|
|
99
99
|
}
|
|
100
100
|
|
|
@@ -106,6 +106,12 @@ export interface TelegramSectionRegistry {
|
|
|
106
106
|
token: TelegramSectionToken,
|
|
107
107
|
): RegisteredTelegramSection | undefined;
|
|
108
108
|
getDiagnostics(): TelegramSectionDiagnostic[];
|
|
109
|
+
recordError(
|
|
110
|
+
token: TelegramSectionToken,
|
|
111
|
+
message: string,
|
|
112
|
+
source?: string,
|
|
113
|
+
): void;
|
|
114
|
+
clearError(token: TelegramSectionToken, source?: string): void;
|
|
109
115
|
clear(): void;
|
|
110
116
|
}
|
|
111
117
|
|
|
@@ -299,6 +305,10 @@ function getUtf8ByteLength(value: string): number {
|
|
|
299
305
|
return new TextEncoder().encode(value).byteLength;
|
|
300
306
|
}
|
|
301
307
|
|
|
308
|
+
function sectionErrorMessage(error: unknown): string {
|
|
309
|
+
return error instanceof Error ? error.message : String(error);
|
|
310
|
+
}
|
|
311
|
+
|
|
302
312
|
function buildTelegramSectionCallbackData(
|
|
303
313
|
token: TelegramSectionToken,
|
|
304
314
|
action: string,
|
|
@@ -340,7 +350,10 @@ function prependBackRow(
|
|
|
340
350
|
/** @internal */
|
|
341
351
|
export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistry {
|
|
342
352
|
const sections = new Map<TelegramSectionToken, RegisteredTelegramSection>();
|
|
343
|
-
const errors = new Map<
|
|
353
|
+
const errors = new Map<
|
|
354
|
+
TelegramSectionToken,
|
|
355
|
+
{ message: string; source: string }
|
|
356
|
+
>();
|
|
344
357
|
let nextToken = 0;
|
|
345
358
|
|
|
346
359
|
function register(section: TelegramSectionRegistration): () => void {
|
|
@@ -382,24 +395,46 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
|
|
|
382
395
|
token: s.token,
|
|
383
396
|
label: s.label,
|
|
384
397
|
status: errors.has(s.token) ? "error" : "active",
|
|
385
|
-
lastError: errors.get(s.token),
|
|
398
|
+
lastError: errors.get(s.token)?.message,
|
|
386
399
|
}));
|
|
387
400
|
}
|
|
388
401
|
|
|
402
|
+
function recordError(
|
|
403
|
+
token: TelegramSectionToken,
|
|
404
|
+
message: string,
|
|
405
|
+
source = "runtime",
|
|
406
|
+
): void {
|
|
407
|
+
if (sections.has(token)) errors.set(token, { message, source });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function clearError(token: TelegramSectionToken, source?: string): void {
|
|
411
|
+
const current = errors.get(token);
|
|
412
|
+
if (!source || !current || current.source === source) errors.delete(token);
|
|
413
|
+
}
|
|
414
|
+
|
|
389
415
|
function clear(): void {
|
|
390
416
|
sections.clear();
|
|
391
417
|
errors.clear();
|
|
392
418
|
nextToken = 0;
|
|
393
419
|
}
|
|
394
420
|
|
|
395
|
-
return {
|
|
421
|
+
return {
|
|
422
|
+
register,
|
|
423
|
+
getSections,
|
|
424
|
+
getByToken,
|
|
425
|
+
getDiagnostics,
|
|
426
|
+
recordError,
|
|
427
|
+
clearError,
|
|
428
|
+
clear,
|
|
429
|
+
};
|
|
396
430
|
}
|
|
397
431
|
|
|
398
432
|
/** @internal */
|
|
399
433
|
export function getTelegramExtensionSettingsRows(
|
|
400
434
|
registry: TelegramSectionRegistry,
|
|
401
435
|
): TelegramSectionSettingsRow[] {
|
|
402
|
-
|
|
436
|
+
const rows: TelegramSectionSettingsRow[] = [];
|
|
437
|
+
for (const section of registry
|
|
403
438
|
.getSections()
|
|
404
439
|
.filter((s) => s.registration.settings)
|
|
405
440
|
.sort((a, b) => {
|
|
@@ -407,22 +442,47 @@ export function getTelegramExtensionSettingsRows(
|
|
|
407
442
|
const orderB = b.registration.settings!.order ?? 0;
|
|
408
443
|
if (orderA !== orderB) return orderA - orderB;
|
|
409
444
|
return a.id.localeCompare(b.id);
|
|
410
|
-
})
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
445
|
+
})) {
|
|
446
|
+
try {
|
|
447
|
+
rows.push({
|
|
448
|
+
label:
|
|
449
|
+
section.registration.settings!.getLabel?.() ??
|
|
450
|
+
section.registration.settings!.label,
|
|
451
|
+
callback_data: `section:${section.token}:settings:open`,
|
|
452
|
+
});
|
|
453
|
+
registry.clearError(section.token, "settings_label");
|
|
454
|
+
} catch (error) {
|
|
455
|
+
registry.recordError(
|
|
456
|
+
section.token,
|
|
457
|
+
sectionErrorMessage(error),
|
|
458
|
+
"settings_label",
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return rows;
|
|
416
463
|
}
|
|
417
464
|
|
|
418
465
|
/** @internal */
|
|
419
466
|
export function getTelegramSectionMainMenuRows(
|
|
420
467
|
registry: TelegramSectionRegistry,
|
|
421
468
|
): TelegramSectionMainMenuRow[] {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
469
|
+
const rows: TelegramSectionMainMenuRow[] = [];
|
|
470
|
+
for (const section of registry.getSections()) {
|
|
471
|
+
try {
|
|
472
|
+
rows.push({
|
|
473
|
+
text: section.registration.getLabel?.() ?? section.label,
|
|
474
|
+
callback_data: `section:${section.token}:open`,
|
|
475
|
+
});
|
|
476
|
+
registry.clearError(section.token, "main_label");
|
|
477
|
+
} catch (error) {
|
|
478
|
+
registry.recordError(
|
|
479
|
+
section.token,
|
|
480
|
+
sectionErrorMessage(error),
|
|
481
|
+
"main_label",
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
return rows;
|
|
426
486
|
}
|
|
427
487
|
|
|
428
488
|
/** @internal */
|
|
@@ -508,9 +568,11 @@ export async function handleTelegramSectionOpen(
|
|
|
508
568
|
viewWithBack.replyMarkup,
|
|
509
569
|
);
|
|
510
570
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
571
|
+
registry.clearError(token, "render");
|
|
511
572
|
return true;
|
|
512
573
|
} catch (error) {
|
|
513
|
-
const message =
|
|
574
|
+
const message = sectionErrorMessage(error);
|
|
575
|
+
registry.recordError(token, message, "render");
|
|
514
576
|
await deps.answerCallbackQuery(
|
|
515
577
|
callbackQueryId,
|
|
516
578
|
`Section error: ${message}`,
|
|
@@ -537,10 +599,10 @@ export async function handleTelegramSectionCallback(
|
|
|
537
599
|
);
|
|
538
600
|
return true;
|
|
539
601
|
}
|
|
540
|
-
// Try main handleCallback first, then settings handleCallback as fallback
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
602
|
+
// Try main handleCallback first, then settings handleCallback as fallback.
|
|
603
|
+
const mainHandler = section.registration.handleCallback;
|
|
604
|
+
const settingsHandler = section.registration.settings?.handleCallback;
|
|
605
|
+
const handler = mainHandler ?? settingsHandler;
|
|
544
606
|
if (!handler) {
|
|
545
607
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
546
608
|
return true;
|
|
@@ -555,16 +617,12 @@ export async function handleTelegramSectionCallback(
|
|
|
555
617
|
payload,
|
|
556
618
|
callbackQueryId,
|
|
557
619
|
deps,
|
|
558
|
-
`section:${token}:open
|
|
620
|
+
mainHandler ? `section:${token}:open` : "settings:list",
|
|
559
621
|
);
|
|
560
622
|
let result = await handler(ctx);
|
|
561
623
|
// Fallback: if main handler passed and settings handler exists, try settings
|
|
562
|
-
// with the correct navigation context (back → settings list)
|
|
563
|
-
if (
|
|
564
|
-
result === "pass" &&
|
|
565
|
-
handler !== section.registration.settings?.handleCallback &&
|
|
566
|
-
section.registration.settings?.handleCallback
|
|
567
|
-
) {
|
|
624
|
+
// with the correct navigation context (back → settings list).
|
|
625
|
+
if (result === "pass" && mainHandler && settingsHandler) {
|
|
568
626
|
const settingsCtx = buildTelegramSectionCallbackContext(
|
|
569
627
|
section.id,
|
|
570
628
|
token,
|
|
@@ -576,14 +634,16 @@ export async function handleTelegramSectionCallback(
|
|
|
576
634
|
deps,
|
|
577
635
|
"settings:list",
|
|
578
636
|
);
|
|
579
|
-
result = await
|
|
637
|
+
result = await settingsHandler(settingsCtx);
|
|
580
638
|
}
|
|
581
639
|
if (result === "pass") {
|
|
582
640
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
583
641
|
}
|
|
642
|
+
registry.clearError(token, "callback");
|
|
584
643
|
return true;
|
|
585
644
|
} catch (error) {
|
|
586
|
-
const message =
|
|
645
|
+
const message = sectionErrorMessage(error);
|
|
646
|
+
registry.recordError(token, message, "callback");
|
|
587
647
|
await deps.answerCallbackQuery(
|
|
588
648
|
callbackQueryId,
|
|
589
649
|
`Section error: ${message}`,
|
|
@@ -632,9 +692,11 @@ export async function handleTelegramSectionSettingsOpen(
|
|
|
632
692
|
viewWithBack.replyMarkup,
|
|
633
693
|
);
|
|
634
694
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
695
|
+
registry.clearError(token, "settings_open");
|
|
635
696
|
return true;
|
|
636
697
|
} catch (error) {
|
|
637
|
-
const message =
|
|
698
|
+
const message = sectionErrorMessage(error);
|
|
699
|
+
registry.recordError(token, message, "settings_open");
|
|
638
700
|
await deps.answerCallbackQuery(
|
|
639
701
|
callbackQueryId,
|
|
640
702
|
`Section error: ${message}`,
|
|
@@ -677,9 +739,11 @@ export async function handleTelegramSectionSettingsCallback(
|
|
|
677
739
|
if (result === "pass") {
|
|
678
740
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
679
741
|
}
|
|
742
|
+
registry.clearError(token, "settings_callback");
|
|
680
743
|
return true;
|
|
681
744
|
} catch (error) {
|
|
682
|
-
const message =
|
|
745
|
+
const message = sectionErrorMessage(error);
|
|
746
|
+
registry.recordError(token, message, "settings_callback");
|
|
683
747
|
await deps.answerCallbackQuery(
|
|
684
748
|
callbackQueryId,
|
|
685
749
|
`Section error: ${message}`,
|
package/lib/status.ts
CHANGED
|
@@ -36,9 +36,24 @@ interface TelegramContextUsage {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
export interface TelegramStatusActiveModel {
|
|
39
|
+
provider?: string;
|
|
40
|
+
id?: string;
|
|
39
41
|
contextWindow?: number;
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export interface TelegramStatusLineProviderContext {
|
|
45
|
+
activeModel: TelegramStatusActiveModel | undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface TelegramStatusLineProviderResult {
|
|
49
|
+
label: string;
|
|
50
|
+
value: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type TelegramStatusLineProvider = (
|
|
54
|
+
ctx: TelegramStatusLineProviderContext,
|
|
55
|
+
) => TelegramStatusLineProviderResult | undefined;
|
|
56
|
+
|
|
42
57
|
export interface TelegramStatusContext {
|
|
43
58
|
sessionManager: { getEntries(): TelegramStatusSessionEntry[] };
|
|
44
59
|
getContextUsage(): TelegramContextUsage | undefined;
|
|
@@ -52,6 +67,8 @@ export interface TelegramStatusContext {
|
|
|
52
67
|
|
|
53
68
|
export type TelegramRuntimeEventDetailValue = string | number | boolean | null;
|
|
54
69
|
|
|
70
|
+
const TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY =
|
|
71
|
+
"__piTelegramStatusLineProviders__";
|
|
55
72
|
const MAX_RECENT_TELEGRAM_RUNTIME_EVENTS = 10;
|
|
56
73
|
const MAX_TELEGRAM_RUNTIME_EVENT_MESSAGE_LENGTH = 1000;
|
|
57
74
|
const MAX_TELEGRAM_RUNTIME_EVENT_DETAIL_LENGTH = 1000;
|
|
@@ -166,7 +183,10 @@ export interface TelegramStatusRuntime<
|
|
|
166
183
|
getStatusLines: () => string[];
|
|
167
184
|
}
|
|
168
185
|
|
|
169
|
-
function truncateTelegramRuntimeEventText(
|
|
186
|
+
function truncateTelegramRuntimeEventText(
|
|
187
|
+
text: string,
|
|
188
|
+
maxLength: number,
|
|
189
|
+
): string {
|
|
170
190
|
if (text.length <= maxLength) return text;
|
|
171
191
|
return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
|
|
172
192
|
}
|
|
@@ -262,6 +282,61 @@ export function recordTelegramRuntimeEvent(
|
|
|
262
282
|
recordStructuredTelegramRuntimeEvent(events, { category, error }, options);
|
|
263
283
|
}
|
|
264
284
|
|
|
285
|
+
function getOrCreateTelegramStatusLineProviderRegistry(): Map<
|
|
286
|
+
string,
|
|
287
|
+
TelegramStatusLineProvider
|
|
288
|
+
> {
|
|
289
|
+
const existing = (globalThis as Record<string, unknown>)[
|
|
290
|
+
TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
|
|
291
|
+
];
|
|
292
|
+
if (existing instanceof Map)
|
|
293
|
+
return existing as Map<string, TelegramStatusLineProvider>;
|
|
294
|
+
const registry = new Map<string, TelegramStatusLineProvider>();
|
|
295
|
+
(globalThis as Record<string, unknown>)[
|
|
296
|
+
TELEGRAM_STATUS_LINE_PROVIDER_REGISTRY_KEY
|
|
297
|
+
] = registry;
|
|
298
|
+
return registry;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Register a compact companion-extension line for the Telegram status menu.
|
|
303
|
+
*
|
|
304
|
+
* Providers are synchronous and should return undefined when their line is not
|
|
305
|
+
* relevant for the active model. Errors are isolated so optional companion
|
|
306
|
+
* status cannot break the core Telegram menu.
|
|
307
|
+
*/
|
|
308
|
+
export function registerTelegramStatusLineProvider(
|
|
309
|
+
provider: TelegramStatusLineProvider,
|
|
310
|
+
options: { id: string },
|
|
311
|
+
): () => void {
|
|
312
|
+
const registry = getOrCreateTelegramStatusLineProviderRegistry();
|
|
313
|
+
registry.set(options.id, provider);
|
|
314
|
+
return () => {
|
|
315
|
+
if (registry.get(options.id) === provider) registry.delete(options.id);
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function getTelegramStatusLineProviderResults(
|
|
320
|
+
ctx: TelegramStatusLineProviderContext,
|
|
321
|
+
): TelegramStatusLineProviderResult[] {
|
|
322
|
+
const results: TelegramStatusLineProviderResult[] = [];
|
|
323
|
+
const registry = getOrCreateTelegramStatusLineProviderRegistry();
|
|
324
|
+
for (const provider of registry.values()) {
|
|
325
|
+
try {
|
|
326
|
+
const result = provider(ctx);
|
|
327
|
+
if (!result?.label || !result.value) continue;
|
|
328
|
+
results.push(result);
|
|
329
|
+
} catch {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return results;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function clearTelegramStatusLineProviders(): void {
|
|
337
|
+
getOrCreateTelegramStatusLineProviderRegistry().clear();
|
|
338
|
+
}
|
|
339
|
+
|
|
265
340
|
export function createTelegramRuntimeEventRecorder(
|
|
266
341
|
options: TelegramRuntimeEventRecorderOptions,
|
|
267
342
|
): TelegramRuntimeEventRecorder {
|
|
@@ -314,7 +389,9 @@ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
|
|
|
314
389
|
return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
|
|
315
390
|
}
|
|
316
391
|
|
|
317
|
-
function buildTelegramRuntimeEventSummary(
|
|
392
|
+
function buildTelegramRuntimeEventSummary(
|
|
393
|
+
events: TelegramRuntimeEvent[],
|
|
394
|
+
): string {
|
|
318
395
|
const counts = new Map<string, number>();
|
|
319
396
|
for (const event of events) {
|
|
320
397
|
const category = formatTelegramRuntimeEventCategory(event);
|
|
@@ -462,7 +539,7 @@ export function buildTelegramStatusBarText(
|
|
|
462
539
|
? theme.fg("success", state.queuedStatus)
|
|
463
540
|
: "";
|
|
464
541
|
if (state.compactionInProgress) {
|
|
465
|
-
return `${label} ${theme.fg("
|
|
542
|
+
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
466
543
|
}
|
|
467
544
|
if (state.processing) {
|
|
468
545
|
const processingStatus = state.queuedStatus
|
|
@@ -553,8 +630,13 @@ function collectUsageStats(ctx: TelegramStatusContext): TelegramUsageStats {
|
|
|
553
630
|
return stats;
|
|
554
631
|
}
|
|
555
632
|
|
|
633
|
+
function formatStatusRowLabel(label: string): string {
|
|
634
|
+
if (!label) return label;
|
|
635
|
+
return `${label[0]?.toUpperCase() ?? ""}${label.slice(1)}`;
|
|
636
|
+
}
|
|
637
|
+
|
|
556
638
|
function buildStatusRow(label: string, value: string): string {
|
|
557
|
-
return `<b>${escapeHtml(label)}:</b> <code>${escapeHtml(value)}</code>`;
|
|
639
|
+
return `<b>${escapeHtml(formatStatusRowLabel(label))}:</b> <code>${escapeHtml(value)}</code>`;
|
|
558
640
|
}
|
|
559
641
|
|
|
560
642
|
function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
|
|
@@ -613,5 +695,8 @@ export function buildStatusHtml(
|
|
|
613
695
|
lines.push(buildStatusRow("Cost", costSummary));
|
|
614
696
|
}
|
|
615
697
|
lines.push(buildStatusRow("Context", buildContextSummary(ctx, activeModel)));
|
|
698
|
+
for (const row of getTelegramStatusLineProviderResults({ activeModel })) {
|
|
699
|
+
lines.push(buildStatusRow(row.label, row.value));
|
|
700
|
+
}
|
|
616
701
|
return lines.join("\n");
|
|
617
702
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"./outbound": "./api/outbound.ts",
|
|
51
51
|
"./updates": "./api/updates.ts",
|
|
52
52
|
"./sections": "./api/sections.ts",
|
|
53
|
+
"./status": "./api/status.ts",
|
|
53
54
|
"./voice": "./api/voice.ts",
|
|
54
55
|
"./keyboard": "./api/keyboard.ts"
|
|
55
56
|
},
|