@llblab/pi-telegram 0.6.3 → 0.7.1

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/menu.ts CHANGED
@@ -1,131 +1,114 @@
1
1
  /**
2
2
  * Telegram menu and inline-keyboard rendering helpers
3
- * Owns menu state, inline UI text, and reply-markup generation for status, model, and thinking controls
3
+ * Zones: telegram ui, controls, status menu
4
+ * Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
4
5
  */
5
6
 
6
7
  import {
7
- getCanonicalModelId,
8
- isThinkingLevel,
8
+ createTelegramModelMenuStateBuilder,
9
+ handleTelegramModelMenuCallbackAction,
10
+ openTelegramModelMenu,
11
+ sendTelegramModelMenuMessage,
12
+ updateTelegramModelMenuMessage,
13
+ type TelegramMenuMessageRuntimeDeps,
14
+ type TelegramModelMenuState,
15
+ type TelegramModelMenuStateBuilderContext,
16
+ type TelegramModelMenuStateBuilderDeps,
17
+ } from "./menu-model.ts";
18
+ import {
19
+ handleTelegramStatusMenuCallbackAction,
20
+ openTelegramStatusMenu,
21
+ sendTelegramStatusMessage,
22
+ updateTelegramStatusMessage,
23
+ } from "./menu-status.ts";
24
+ import {
25
+ handleTelegramThinkingMenuCallbackAction,
26
+ openTelegramThinkingMenu,
27
+ updateTelegramThinkingMenuMessage,
28
+ } from "./menu-thinking.ts";
29
+ import {
9
30
  type MenuModel,
10
- modelsMatch,
11
- parseTelegramCliScopedModelPatterns,
12
- resolveScopedModelPatterns,
13
31
  type ScopedTelegramModel,
14
- sortScopedModels,
15
- THINKING_LEVELS,
16
32
  type ThinkingLevel,
17
33
  } from "./model.ts";
18
- const TELEGRAM_MODEL_MENU_CACHE_TTL_MS = 5000;
19
- const TELEGRAM_MODEL_MENU_STATE_TTL_MS = 10 * 60 * 1000;
20
- const MAX_STORED_TELEGRAM_MODEL_MENUS = 50;
21
-
22
- export type TelegramModelScope = "all" | "scoped";
23
-
24
- export interface TelegramModelMenuState<TModel extends MenuModel = MenuModel> {
25
- chatId: number;
26
- messageId: number;
27
- page: number;
28
- scope: TelegramModelScope;
29
- scopedModels: ScopedTelegramModel<TModel>[];
30
- allModels: ScopedTelegramModel<TModel>[];
31
- note?: string;
32
- mode: "status" | "model" | "thinking";
33
- }
34
-
35
- export interface StoredTelegramModelMenuState<
36
- TModel extends MenuModel = MenuModel,
37
- > {
38
- state: TelegramModelMenuState<TModel>;
39
- updatedAt: number;
40
- }
41
-
42
- export interface TelegramModelMenuStoreOptions {
43
- maxAgeMs: number;
44
- maxStoredMenus: number;
45
- now?: number;
46
- }
47
-
48
- export interface CachedTelegramModelMenuInputs<
49
- TModel extends MenuModel = MenuModel,
50
- > {
51
- expiresAt: number;
52
- availableModels: TModel[];
53
- configuredScopedModelPatterns: string[];
54
- cliScopedModelPatterns?: string[];
55
- }
56
-
57
- export interface TelegramModelMenuInputCacheDeps<
58
- TModel extends MenuModel = MenuModel,
59
- > {
60
- cacheTtlMs: number;
61
- now?: number;
62
- reloadSettings: () => Promise<void>;
63
- refreshAvailableModels: () => TModel[];
64
- getConfiguredScopedModelPatterns: () => string[] | undefined;
65
- getCliScopedModelPatterns: () => string[] | undefined;
66
- }
67
-
68
- export interface TelegramModelMenuRuntimeContext<
69
- TModel extends MenuModel = MenuModel,
70
- > {
71
- modelRegistry: {
72
- refresh: () => void;
73
- getAvailable: () => TModel[];
74
- };
75
- }
76
-
77
- export interface TelegramModelMenuRuntimeOptions<
78
- TContext extends TelegramModelMenuRuntimeContext<TModel>,
79
- TModel extends MenuModel = MenuModel,
80
- > {
81
- chatId: number;
82
- activeModel: TModel | undefined;
83
- cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined;
84
- cacheTtlMs: number;
85
- ctx: TContext;
86
- reloadSettings: () => Promise<void>;
87
- getConfiguredScopedModelPatterns: () => string[] | undefined;
88
- getCliScopedModelPatterns?: () => string[] | undefined;
89
- }
90
-
91
- export interface MenuSettingsManager {
92
- reload: () => Promise<void>;
93
- getEnabledModels: () => string[] | undefined;
94
- }
95
-
96
- export type TelegramModelMenuStateBuilderContext<
97
- TModel extends MenuModel = MenuModel,
98
- > = TelegramModelMenuRuntimeContext<TModel> & { cwd: string };
99
-
100
- export interface TelegramModelMenuStateBuilderDeps<
101
- TModel extends MenuModel = MenuModel,
102
- TContext extends TelegramModelMenuStateBuilderContext<TModel> =
103
- TelegramModelMenuStateBuilderContext<TModel>,
104
- > {
105
- runtime: TelegramModelMenuRuntime<TModel>;
106
- createSettingsManager: (cwd: string) => MenuSettingsManager;
107
- getActiveModel: (ctx: TContext) => TModel | undefined;
108
- }
109
-
110
- export type TelegramReplyMarkup = {
111
- inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
112
- };
113
34
 
114
- export interface TelegramMenuMessageRuntimeDeps {
115
- editInteractiveMessage: (
116
- chatId: number,
117
- messageId: number,
118
- text: string,
119
- mode: "html" | "plain",
120
- replyMarkup: TelegramReplyMarkup,
121
- ) => Promise<void>;
122
- sendInteractiveMessage: (
123
- chatId: number,
124
- text: string,
125
- mode: "html" | "plain",
126
- replyMarkup: TelegramReplyMarkup,
127
- ) => Promise<number | undefined>;
128
- }
35
+ export {
36
+ applyTelegramModelPageSelection,
37
+ applyTelegramModelScopeSelection,
38
+ buildModelMenuReplyMarkup,
39
+ buildModelPageMenuReplyMarkup,
40
+ buildTelegramModelCallbackPlan,
41
+ buildTelegramModelMenuRenderPayload,
42
+ buildTelegramModelPageMenuRenderPayload,
43
+ buildTelegramModelMenuState,
44
+ buildTelegramModelMenuStateRuntime,
45
+ createTelegramModelMenuRuntime,
46
+ createTelegramModelMenuStateBuilder,
47
+ formatScopedModelButtonText,
48
+ getModelMenuItems,
49
+ getStoredTelegramModelMenuState,
50
+ getTelegramModelMenuPage,
51
+ getTelegramModelSelection,
52
+ handleTelegramModelMenuCallbackAction,
53
+ MODEL_MENU_TITLE,
54
+ MODEL_PAGE_MENU_TITLE,
55
+ openTelegramModelMenu,
56
+ pruneStoredTelegramModelMenus,
57
+ resolveCachedTelegramModelMenuInputs,
58
+ sendTelegramModelMenuMessage,
59
+ storeTelegramModelMenuState,
60
+ TELEGRAM_MODEL_PAGE_SIZE,
61
+ updateTelegramModelMenuMessage,
62
+ } from "./menu-model.ts";
63
+ export type {
64
+ BuildTelegramModelCallbackPlanParams,
65
+ BuildTelegramModelMenuStateParams,
66
+ CachedTelegramModelMenuInputs,
67
+ MenuSettingsManager,
68
+ StoredTelegramModelMenuState,
69
+ TelegramMenuMessageRuntimeDeps,
70
+ TelegramMenuMutationResult,
71
+ TelegramMenuRenderPayload,
72
+ TelegramMenuSelectionResult,
73
+ TelegramModelCallbackPlan,
74
+ TelegramModelMenuCallbackDeps,
75
+ TelegramModelMenuInputCacheDeps,
76
+ TelegramModelMenuOpenDeps,
77
+ TelegramModelMenuPage,
78
+ TelegramModelMenuRuntime,
79
+ TelegramModelMenuRuntimeContext,
80
+ TelegramModelMenuRuntimeOptions,
81
+ TelegramModelMenuState,
82
+ TelegramModelMenuStateBuilderContext,
83
+ TelegramModelMenuStateBuilderDeps,
84
+ TelegramModelMenuStoreOptions,
85
+ TelegramModelScope,
86
+ TelegramReplyMarkup,
87
+ } from "./menu-model.ts";
88
+ export {
89
+ buildStatusReplyMarkup,
90
+ buildTelegramStatusMenuRenderPayload,
91
+ handleTelegramStatusMenuCallbackAction,
92
+ openTelegramStatusMenu,
93
+ sendTelegramStatusMessage,
94
+ updateTelegramStatusMessage,
95
+ } from "./menu-status.ts";
96
+ export type {
97
+ TelegramStatusMenuCallbackDeps,
98
+ TelegramStatusMenuOpenDeps,
99
+ } from "./menu-status.ts";
100
+ export {
101
+ buildTelegramThinkingMenuRenderPayload,
102
+ buildThinkingMenuReplyMarkup,
103
+ buildThinkingMenuText,
104
+ handleTelegramThinkingMenuCallbackAction,
105
+ openTelegramThinkingMenu,
106
+ updateTelegramThinkingMenuMessage,
107
+ } from "./menu-thinking.ts";
108
+ export type {
109
+ TelegramThinkingMenuCallbackDeps,
110
+ TelegramThinkingMenuOpenDeps,
111
+ } from "./menu-thinking.ts";
129
112
 
130
113
  export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
131
114
  answerCallbackQuery: (
@@ -145,37 +128,6 @@ export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
145
128
  ) => Promise<boolean> | boolean;
146
129
  }
147
130
 
148
- export type TelegramStatusMenuCallbackDeps<
149
- TModel extends MenuModel = MenuModel,
150
- > = Pick<
151
- TelegramMenuEffectPort<TModel>,
152
- "updateModelMenuMessage" | "updateThinkingMenuMessage" | "answerCallbackQuery"
153
- >;
154
-
155
- export type TelegramThinkingMenuCallbackDeps<
156
- TModel extends MenuModel = MenuModel,
157
- > = Pick<
158
- TelegramMenuEffectPort<TModel>,
159
- | "setThinkingLevel"
160
- | "getCurrentThinkingLevel"
161
- | "updateStatusMessage"
162
- | "answerCallbackQuery"
163
- >;
164
-
165
- export type TelegramModelMenuCallbackDeps<
166
- TModel extends MenuModel = MenuModel,
167
- > = Pick<
168
- TelegramMenuEffectPort<TModel>,
169
- | "updateModelMenuMessage"
170
- | "updateStatusMessage"
171
- | "answerCallbackQuery"
172
- | "setModel"
173
- | "setCurrentModel"
174
- | "setThinkingLevel"
175
- | "stagePendingModelSwitch"
176
- | "restartInterruptedTelegramTurn"
177
- >;
178
-
179
131
  export interface TelegramMenuCallbackEntryDeps {
180
132
  handleStatusAction: () => Promise<boolean>;
181
133
  handleThinkingAction: () => Promise<boolean>;
@@ -256,40 +208,6 @@ export interface TelegramMenuCallbackRuntimeDeps<
256
208
  ) => Promise<boolean> | boolean;
257
209
  }
258
210
 
259
- export interface TelegramStatusMenuOpenDeps<
260
- TModel extends MenuModel = MenuModel,
261
- > {
262
- isIdle: () => boolean;
263
- sendBusyMessage: () => Promise<void>;
264
- getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
265
- buildStatusHtml: () => string;
266
- getActiveModel: () => TModel | undefined;
267
- getThinkingLevel: () => ThinkingLevel;
268
- sendStatusMenu: (
269
- state: TelegramModelMenuState<TModel>,
270
- statusHtml: string,
271
- activeModel: TModel | undefined,
272
- thinkingLevel: ThinkingLevel,
273
- ) => Promise<number | undefined>;
274
- storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
275
- }
276
-
277
- export interface TelegramModelMenuOpenDeps<
278
- TModel extends MenuModel = MenuModel,
279
- > {
280
- isIdle: () => boolean;
281
- canOfferInFlightModelSwitch: () => boolean;
282
- sendBusyMessage: () => Promise<void>;
283
- sendNoModelsMessage: () => Promise<void>;
284
- getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
285
- getActiveModel: () => TModel | undefined;
286
- sendModelMenu: (
287
- state: TelegramModelMenuState<TModel>,
288
- activeModel: TModel | undefined,
289
- ) => Promise<number | undefined>;
290
- storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
291
- }
292
-
293
211
  export interface TelegramMenuActionRuntimeDeps<
294
212
  TContext,
295
213
  TModel extends MenuModel = MenuModel,
@@ -300,6 +218,7 @@ export interface TelegramMenuActionRuntimeDeps<
300
218
  ) => Promise<TelegramModelMenuState<TModel>>;
301
219
  getActiveModel: (ctx: TContext) => TModel | undefined;
302
220
  getThinkingLevel: () => ThinkingLevel;
221
+ getQueueItemCount?: () => number;
303
222
  buildStatusHtml: (ctx: TContext) => string;
304
223
  storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
305
224
  isIdle: (ctx: TContext) => boolean;
@@ -337,332 +256,23 @@ export interface TelegramMenuActionRuntime<
337
256
  replyToMessageId: number,
338
257
  ctx: TContext,
339
258
  ) => Promise<void>;
340
- }
341
-
342
- export const TELEGRAM_MODEL_PAGE_SIZE = 6;
343
-
344
- export function pruneStoredTelegramModelMenus<
345
- TModel extends MenuModel = MenuModel,
346
- >(
347
- menus: Map<number, StoredTelegramModelMenuState<TModel>>,
348
- options: TelegramModelMenuStoreOptions,
349
- ): void {
350
- const now = options.now ?? Date.now();
351
- for (const [messageId, entry] of menus.entries()) {
352
- if (now - entry.updatedAt <= options.maxAgeMs) continue;
353
- menus.delete(messageId);
354
- }
355
- while (menus.size > options.maxStoredMenus) {
356
- const oldestMessageId = menus.keys().next().value as number | undefined;
357
- if (oldestMessageId === undefined) return;
358
- menus.delete(oldestMessageId);
359
- }
360
- }
361
-
362
- export function storeTelegramModelMenuState<
363
- TModel extends MenuModel = MenuModel,
364
- >(
365
- menus: Map<number, StoredTelegramModelMenuState<TModel>>,
366
- state: TelegramModelMenuState<TModel>,
367
- options: TelegramModelMenuStoreOptions,
368
- ): void {
369
- const now = options.now ?? Date.now();
370
- pruneStoredTelegramModelMenus(menus, { ...options, now });
371
- menus.set(state.messageId, { state, updatedAt: now });
372
- pruneStoredTelegramModelMenus(menus, { ...options, now });
373
- }
374
-
375
- export function getStoredTelegramModelMenuState<
376
- TModel extends MenuModel = MenuModel,
377
- >(
378
- menus: Map<number, StoredTelegramModelMenuState<TModel>>,
379
- messageId: number | undefined,
380
- options: TelegramModelMenuStoreOptions,
381
- ): TelegramModelMenuState<TModel> | undefined {
382
- if (messageId === undefined) return undefined;
383
- const now = options.now ?? Date.now();
384
- pruneStoredTelegramModelMenus(menus, { ...options, now });
385
- const entry = menus.get(messageId);
386
- if (!entry) return undefined;
387
- menus.delete(messageId);
388
- entry.updatedAt = now;
389
- menus.set(messageId, entry);
390
- return entry.state;
391
- }
392
-
393
- export interface TelegramModelMenuRuntime<
394
- TModel extends MenuModel = MenuModel,
395
- > {
396
- storeState: (state: TelegramModelMenuState<TModel>) => void;
397
- getState: (
398
- messageId: number | undefined,
399
- ) => TelegramModelMenuState<TModel> | undefined;
400
- clear: () => void;
401
- buildState: <TContext extends TelegramModelMenuRuntimeContext<TModel>>(
402
- options: Omit<
403
- TelegramModelMenuRuntimeOptions<TContext, TModel>,
404
- "cachedInputs" | "cacheTtlMs"
405
- >,
406
- ) => Promise<TelegramModelMenuState<TModel>>;
407
- }
408
-
409
- export function createTelegramModelMenuRuntime<
410
- TModel extends MenuModel = MenuModel,
411
- >(
412
- options: Partial<TelegramModelMenuStoreOptions> = {},
413
- ): TelegramModelMenuRuntime<TModel> {
414
- const menus = new Map<number, StoredTelegramModelMenuState<TModel>>();
415
- let cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined;
416
- const getStoreOptions = (): TelegramModelMenuStoreOptions => ({
417
- maxAgeMs: options.maxAgeMs ?? TELEGRAM_MODEL_MENU_STATE_TTL_MS,
418
- maxStoredMenus: options.maxStoredMenus ?? MAX_STORED_TELEGRAM_MODEL_MENUS,
419
- now: options.now,
420
- });
421
- return {
422
- storeState: (state) => {
423
- storeTelegramModelMenuState(menus, state, getStoreOptions());
424
- },
425
- getState: (messageId) =>
426
- getStoredTelegramModelMenuState(menus, messageId, getStoreOptions()),
427
- clear: () => {
428
- menus.clear();
429
- cachedInputs = undefined;
430
- },
431
- buildState: async (stateOptions) => {
432
- const result = await buildTelegramModelMenuStateRuntime({
433
- ...stateOptions,
434
- cachedInputs,
435
- cacheTtlMs: TELEGRAM_MODEL_MENU_CACHE_TTL_MS,
436
- });
437
- cachedInputs = result.cachedInputs;
438
- return result.state;
439
- },
440
- };
441
- }
442
-
443
- export function createTelegramModelMenuStateBuilder<
444
- TModel extends MenuModel = MenuModel,
445
- TContext extends TelegramModelMenuStateBuilderContext<TModel> =
446
- TelegramModelMenuStateBuilderContext<TModel>,
447
- >(
448
- deps: TelegramModelMenuStateBuilderDeps<TModel, TContext>,
449
- ): (chatId: number, ctx: TContext) => Promise<TelegramModelMenuState<TModel>> {
450
- return async (chatId, ctx) => {
451
- const settingsManager = deps.createSettingsManager(ctx.cwd);
452
- return deps.runtime.buildState({
453
- chatId,
454
- activeModel: deps.getActiveModel(ctx),
455
- ctx,
456
- reloadSettings: () => settingsManager.reload(),
457
- getConfiguredScopedModelPatterns: () =>
458
- settingsManager.getEnabledModels(),
459
- });
460
- };
461
- }
462
-
463
- export async function resolveCachedTelegramModelMenuInputs<
464
- TModel extends MenuModel = MenuModel,
465
- >(
466
- cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined,
467
- deps: TelegramModelMenuInputCacheDeps<TModel>,
468
- ): Promise<CachedTelegramModelMenuInputs<TModel>> {
469
- const now = deps.now ?? Date.now();
470
- if (cachedInputs && cachedInputs.expiresAt > now) return cachedInputs;
471
- await deps.reloadSettings();
472
- const availableModels = deps.refreshAvailableModels();
473
- const cliScopedModelPatterns = deps.getCliScopedModelPatterns();
474
- const configuredScopedModelPatterns =
475
- cliScopedModelPatterns ?? deps.getConfiguredScopedModelPatterns() ?? [];
476
- return {
477
- expiresAt: now + deps.cacheTtlMs,
478
- availableModels,
479
- configuredScopedModelPatterns,
480
- cliScopedModelPatterns,
481
- };
482
- }
483
-
484
- function getTelegramCliScopedModelPatterns(): string[] | undefined {
485
- return parseTelegramCliScopedModelPatterns(process.argv.slice(2));
486
- }
487
-
488
- export const MODEL_MENU_TITLE = "<b>Choose a model:</b>";
489
-
490
- export interface BuildTelegramModelMenuStateParams<
491
- TModel extends MenuModel = MenuModel,
492
- > {
493
- chatId: number;
494
- activeModel: TModel | undefined;
495
- availableModels: TModel[];
496
- configuredScopedModelPatterns: string[];
497
- cliScopedModelPatterns?: string[];
259
+ openThinkingMenu: (
260
+ chatId: number,
261
+ replyToMessageId: number,
262
+ ctx: TContext,
263
+ ) => Promise<void>;
498
264
  }
499
265
 
500
266
  export type TelegramMenuCallbackAction =
501
267
  | { kind: "ignore" }
502
- | { kind: "status"; action: "model" | "thinking" }
268
+ | { kind: "status"; action: "model" | "thinking" | "queue" }
503
269
  | { kind: "thinking:set"; level: string }
504
270
  | {
505
271
  kind: "model";
506
- action: "noop" | "scope" | "page" | "pick";
272
+ action: "noop" | "scope" | "page" | "pages" | "pick";
507
273
  value?: string;
508
274
  };
509
275
 
510
- export type TelegramMenuMutationResult = "invalid" | "unchanged" | "changed";
511
- export type TelegramMenuSelectionResult<TModel extends MenuModel = MenuModel> =
512
- | { kind: "invalid" }
513
- | { kind: "missing" }
514
- | { kind: "selected"; selection: ScopedTelegramModel<TModel> };
515
-
516
- export interface TelegramModelMenuPage<TModel extends MenuModel = MenuModel> {
517
- page: number;
518
- pageCount: number;
519
- start: number;
520
- items: ScopedTelegramModel<TModel>[];
521
- }
522
-
523
- export interface TelegramMenuRenderPayload {
524
- nextMode: TelegramModelMenuState["mode"];
525
- text: string;
526
- mode: "html" | "plain";
527
- replyMarkup: TelegramReplyMarkup;
528
- }
529
-
530
- export type TelegramModelCallbackPlan<TModel extends MenuModel = MenuModel> =
531
- | { kind: "ignore" }
532
- | { kind: "answer"; text?: string }
533
- | { kind: "update-menu"; text?: string }
534
- | {
535
- kind: "refresh-status";
536
- selection: ScopedTelegramModel<TModel>;
537
- callbackText: string;
538
- shouldApplyThinkingLevel: boolean;
539
- }
540
- | {
541
- kind: "switch-model";
542
- selection: ScopedTelegramModel<TModel>;
543
- mode: "idle" | "restart-now" | "restart-after-tool";
544
- callbackText: string;
545
- };
546
-
547
- export interface BuildTelegramModelCallbackPlanParams<
548
- TModel extends MenuModel = MenuModel,
549
- > {
550
- data: string | undefined;
551
- state: TelegramModelMenuState<TModel>;
552
- activeModel: TModel | undefined;
553
- currentThinkingLevel: ThinkingLevel;
554
- isIdle: boolean;
555
- canRestartBusyRun: boolean;
556
- hasActiveToolExecutions: boolean;
557
- }
558
-
559
- function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
560
- return label.length <= maxLength
561
- ? label
562
- : `${label.slice(0, maxLength - 1)}…`;
563
- }
564
-
565
- export function formatScopedModelButtonText<
566
- TModel extends MenuModel = MenuModel,
567
- >(
568
- entry: ScopedTelegramModel<TModel>,
569
- currentModel: TModel | undefined,
570
- ): string {
571
- let label = `${modelsMatch(entry.model, currentModel) ? "✅ " : ""}${entry.model.id} [${entry.model.provider}]`;
572
- if (entry.thinkingLevel) {
573
- label += ` · ${entry.thinkingLevel}`;
574
- }
575
- return truncateTelegramButtonLabel(label);
576
- }
577
-
578
- export function formatStatusButtonLabel(label: string, value: string): string {
579
- return truncateTelegramButtonLabel(`${label}: ${value}`, 64);
580
- }
581
-
582
- export function getModelMenuItems<TModel extends MenuModel = MenuModel>(
583
- state: TelegramModelMenuState<TModel>,
584
- ): ScopedTelegramModel<TModel>[] {
585
- return state.scope === "scoped" && state.scopedModels.length > 0
586
- ? state.scopedModels
587
- : state.allModels;
588
- }
589
-
590
- export function buildTelegramModelMenuState<
591
- TModel extends MenuModel = MenuModel,
592
- >(
593
- params: BuildTelegramModelMenuStateParams<TModel>,
594
- ): TelegramModelMenuState<TModel> {
595
- const allModels = sortScopedModels(
596
- params.availableModels.map((model) => ({ model })),
597
- params.activeModel,
598
- );
599
- const scopedModels =
600
- params.configuredScopedModelPatterns.length > 0
601
- ? sortScopedModels(
602
- resolveScopedModelPatterns(
603
- params.configuredScopedModelPatterns,
604
- params.availableModels,
605
- ),
606
- params.activeModel,
607
- )
608
- : [];
609
- let note: string | undefined;
610
- if (
611
- params.configuredScopedModelPatterns.length > 0 &&
612
- scopedModels.length === 0
613
- ) {
614
- note = params.cliScopedModelPatterns
615
- ? "No CLI scoped models matched the current auth configuration. Showing all available models."
616
- : "No scoped models matched the current auth configuration. Showing all available models.";
617
- }
618
- return {
619
- chatId: params.chatId,
620
- messageId: 0,
621
- page: 0,
622
- scope: scopedModels.length > 0 ? "scoped" : "all",
623
- scopedModels,
624
- allModels,
625
- note,
626
- mode: "status",
627
- };
628
- }
629
-
630
- export async function buildTelegramModelMenuStateRuntime<
631
- TContext extends TelegramModelMenuRuntimeContext<TModel>,
632
- TModel extends MenuModel = MenuModel,
633
- >(
634
- options: TelegramModelMenuRuntimeOptions<TContext, TModel>,
635
- ): Promise<{
636
- state: TelegramModelMenuState<TModel>;
637
- cachedInputs: CachedTelegramModelMenuInputs<TModel>;
638
- }> {
639
- const cachedInputs = await resolveCachedTelegramModelMenuInputs(
640
- options.cachedInputs,
641
- {
642
- cacheTtlMs: options.cacheTtlMs,
643
- reloadSettings: options.reloadSettings,
644
- refreshAvailableModels: () => {
645
- options.ctx.modelRegistry.refresh();
646
- return options.ctx.modelRegistry.getAvailable();
647
- },
648
- getConfiguredScopedModelPatterns:
649
- options.getConfiguredScopedModelPatterns,
650
- getCliScopedModelPatterns:
651
- options.getCliScopedModelPatterns ?? getTelegramCliScopedModelPatterns,
652
- },
653
- );
654
- return {
655
- cachedInputs,
656
- state: buildTelegramModelMenuState({
657
- chatId: options.chatId,
658
- activeModel: options.activeModel,
659
- availableModels: cachedInputs.availableModels,
660
- configuredScopedModelPatterns: cachedInputs.configuredScopedModelPatterns,
661
- cliScopedModelPatterns: cachedInputs.cliScopedModelPatterns,
662
- }),
663
- };
664
- }
665
-
666
276
  export function parseTelegramMenuCallbackAction(
667
277
  data: string | undefined,
668
278
  ): TelegramMenuCallbackAction {
@@ -670,6 +280,7 @@ export function parseTelegramMenuCallbackAction(
670
280
  if (data === "status:thinking") {
671
281
  return { kind: "status", action: "thinking" };
672
282
  }
283
+ if (data === "status:queue") return { kind: "status", action: "queue" };
673
284
  if (data?.startsWith("thinking:set:")) {
674
285
  return {
675
286
  kind: "thinking:set",
@@ -682,6 +293,7 @@ export function parseTelegramMenuCallbackAction(
682
293
  action === "noop" ||
683
294
  action === "scope" ||
684
295
  action === "page" ||
296
+ action === "pages" ||
685
297
  action === "pick"
686
298
  ) {
687
299
  return { kind: "model", action, value };
@@ -690,155 +302,6 @@ export function parseTelegramMenuCallbackAction(
690
302
  return { kind: "ignore" };
691
303
  }
692
304
 
693
- export function applyTelegramModelScopeSelection(
694
- state: TelegramModelMenuState,
695
- value: string | undefined,
696
- ): TelegramMenuMutationResult {
697
- if (value !== "all" && value !== "scoped") return "invalid";
698
- if (value === state.scope) return "unchanged";
699
- state.scope = value;
700
- state.page = 0;
701
- return "changed";
702
- }
703
-
704
- export function applyTelegramModelPageSelection(
705
- state: TelegramModelMenuState,
706
- value: string | undefined,
707
- ): TelegramMenuMutationResult {
708
- const page = Number(value);
709
- if (!Number.isFinite(page)) return "invalid";
710
- if (page === state.page) return "unchanged";
711
- state.page = page;
712
- return "changed";
713
- }
714
-
715
- export function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(
716
- state: TelegramModelMenuState<TModel>,
717
- value: string | undefined,
718
- ): TelegramMenuSelectionResult<TModel> {
719
- const index = Number(value);
720
- if (!Number.isFinite(index)) return { kind: "invalid" };
721
- const selection = getModelMenuItems(state)[index];
722
- if (!selection) return { kind: "missing" };
723
- return { kind: "selected", selection };
724
- }
725
-
726
- export function buildTelegramModelCallbackPlan<
727
- TModel extends MenuModel = MenuModel,
728
- >(
729
- params: BuildTelegramModelCallbackPlanParams<TModel>,
730
- ): TelegramModelCallbackPlan<TModel> {
731
- const action = parseTelegramMenuCallbackAction(params.data);
732
- if (action.kind !== "model") return { kind: "ignore" };
733
- if (action.action === "noop") return { kind: "answer" };
734
- if (action.action === "scope") {
735
- const scopeResult = applyTelegramModelScopeSelection(
736
- params.state,
737
- action.value,
738
- );
739
- if (scopeResult === "invalid") {
740
- return { kind: "answer", text: "Unknown model scope." };
741
- }
742
- if (scopeResult === "unchanged") {
743
- return { kind: "answer" };
744
- }
745
- return {
746
- kind: "update-menu",
747
- text: params.state.scope === "scoped" ? "Scoped models" : "All models",
748
- };
749
- }
750
- if (action.action === "page") {
751
- const pageResult = applyTelegramModelPageSelection(
752
- params.state,
753
- action.value,
754
- );
755
- if (pageResult === "invalid") {
756
- return { kind: "answer", text: "Invalid page." };
757
- }
758
- if (pageResult === "unchanged") {
759
- return { kind: "answer" };
760
- }
761
- return { kind: "update-menu" };
762
- }
763
- if (action.action !== "pick") {
764
- return { kind: "answer" };
765
- }
766
- const selectionResult = getTelegramModelSelection(params.state, action.value);
767
- if (selectionResult.kind === "invalid") {
768
- return { kind: "answer", text: "Invalid model selection." };
769
- }
770
- if (selectionResult.kind === "missing") {
771
- return { kind: "answer", text: "Selected model is no longer available." };
772
- }
773
- const selection = selectionResult.selection;
774
- if (modelsMatch(selection.model, params.activeModel)) {
775
- return {
776
- kind: "refresh-status",
777
- selection,
778
- callbackText: `Model: ${selection.model.id}`,
779
- shouldApplyThinkingLevel:
780
- !!selection.thinkingLevel &&
781
- selection.thinkingLevel !== params.currentThinkingLevel,
782
- };
783
- }
784
- if (!params.isIdle) {
785
- if (!params.canRestartBusyRun) {
786
- return { kind: "answer", text: "Pi is busy. Send /stop first." };
787
- }
788
- return {
789
- kind: "switch-model",
790
- selection,
791
- mode: params.hasActiveToolExecutions
792
- ? "restart-after-tool"
793
- : "restart-now",
794
- callbackText: params.hasActiveToolExecutions
795
- ? `Switched to ${selection.model.id}. Restarting after the current tool finishes…`
796
- : `Switching to ${selection.model.id} and continuing…`,
797
- };
798
- }
799
- return {
800
- kind: "switch-model",
801
- selection,
802
- mode: "idle",
803
- callbackText: `Switched to ${selection.model.id}`,
804
- };
805
- }
806
-
807
- export async function openTelegramStatusMenu<
808
- TModel extends MenuModel = MenuModel,
809
- >(deps: TelegramStatusMenuOpenDeps<TModel>): Promise<void> {
810
- const state = await deps.getModelMenuState();
811
- const messageId = await deps.sendStatusMenu(
812
- state,
813
- deps.buildStatusHtml(),
814
- deps.getActiveModel(),
815
- deps.getThinkingLevel(),
816
- );
817
- if (messageId === undefined) return;
818
- state.messageId = messageId;
819
- state.mode = "status";
820
- deps.storeModelMenuState(state);
821
- }
822
-
823
- export async function openTelegramModelMenu<
824
- TModel extends MenuModel = MenuModel,
825
- >(deps: TelegramModelMenuOpenDeps<TModel>): Promise<void> {
826
- if (!deps.isIdle() && !deps.canOfferInFlightModelSwitch()) {
827
- await deps.sendBusyMessage();
828
- return;
829
- }
830
- const state = await deps.getModelMenuState();
831
- if (state.allModels.length === 0) {
832
- await deps.sendNoModelsMessage();
833
- return;
834
- }
835
- const messageId = await deps.sendModelMenu(state, deps.getActiveModel());
836
- if (messageId === undefined) return;
837
- state.messageId = messageId;
838
- state.mode = "model";
839
- deps.storeModelMenuState(state);
840
- }
841
-
842
305
  export async function handleTelegramMenuCallbackEntry(
843
306
  callbackQueryId: string,
844
307
  data: string | undefined,
@@ -979,6 +442,16 @@ export async function handleTelegramMenuCallbackRuntime<
979
442
  ctx: TContext,
980
443
  deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
981
444
  ): Promise<void> {
445
+ if (query.data === "menu:back") {
446
+ const state = deps.getStoredModelMenuState(query.message?.message_id);
447
+ if (!state) {
448
+ await deps.answerCallbackQuery(query.id, "Interactive message expired.");
449
+ return;
450
+ }
451
+ await deps.updateStatusMessage(state, ctx);
452
+ await deps.answerCallbackQuery(query.id);
453
+ return;
454
+ }
982
455
  await handleStoredTelegramMenuCallback(query, {
983
456
  getStoredModelMenuState: deps.getStoredModelMenuState,
984
457
  handleStatusAction: async (state) =>
@@ -1050,264 +523,6 @@ export async function handleTelegramMenuCallbackRuntime<
1050
523
  });
1051
524
  }
1052
525
 
1053
- export async function handleTelegramModelMenuCallbackAction<
1054
- TModel extends MenuModel = MenuModel,
1055
- >(
1056
- callbackQueryId: string,
1057
- params: BuildTelegramModelCallbackPlanParams<TModel>,
1058
- deps: TelegramModelMenuCallbackDeps<TModel>,
1059
- ): Promise<boolean> {
1060
- const plan = buildTelegramModelCallbackPlan(params);
1061
- if (plan.kind === "ignore") return false;
1062
- if (plan.kind === "answer") {
1063
- await deps.answerCallbackQuery(callbackQueryId, plan.text);
1064
- return true;
1065
- }
1066
- if (plan.kind === "update-menu") {
1067
- await deps.updateModelMenuMessage();
1068
- await deps.answerCallbackQuery(callbackQueryId, plan.text);
1069
- return true;
1070
- }
1071
- if (plan.kind === "refresh-status") {
1072
- if (plan.shouldApplyThinkingLevel && plan.selection.thinkingLevel) {
1073
- deps.setThinkingLevel(plan.selection.thinkingLevel);
1074
- }
1075
- await deps.updateStatusMessage();
1076
- await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
1077
- return true;
1078
- }
1079
- const changed = await deps.setModel(plan.selection.model);
1080
- if (changed === false) {
1081
- await deps.answerCallbackQuery(callbackQueryId, "Model is not available.");
1082
- return true;
1083
- }
1084
- deps.setCurrentModel(plan.selection.model);
1085
- if (plan.selection.thinkingLevel) {
1086
- deps.setThinkingLevel(plan.selection.thinkingLevel);
1087
- }
1088
- await deps.updateStatusMessage();
1089
- if (plan.mode === "restart-after-tool") {
1090
- deps.stagePendingModelSwitch(plan.selection);
1091
- await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
1092
- return true;
1093
- }
1094
- if (plan.mode === "restart-now") {
1095
- const restarted = await deps.restartInterruptedTelegramTurn(plan.selection);
1096
- if (!restarted) {
1097
- await deps.answerCallbackQuery(
1098
- callbackQueryId,
1099
- "Pi is busy. Send /stop first.",
1100
- );
1101
- return true;
1102
- }
1103
- }
1104
- await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
1105
- return true;
1106
- }
1107
-
1108
- export async function handleTelegramStatusMenuCallbackAction(
1109
- callbackQueryId: string,
1110
- data: string | undefined,
1111
- activeModel: MenuModel | undefined,
1112
- deps: TelegramStatusMenuCallbackDeps,
1113
- ): Promise<boolean> {
1114
- const action = parseTelegramMenuCallbackAction(data);
1115
- if (action.kind === "status" && action.action === "model") {
1116
- await deps.updateModelMenuMessage();
1117
- await deps.answerCallbackQuery(callbackQueryId);
1118
- return true;
1119
- }
1120
- if (!(action.kind === "status" && action.action === "thinking")) {
1121
- return false;
1122
- }
1123
- if (!activeModel?.reasoning) {
1124
- await deps.answerCallbackQuery(
1125
- callbackQueryId,
1126
- "This model has no reasoning controls.",
1127
- );
1128
- return true;
1129
- }
1130
- await deps.updateThinkingMenuMessage();
1131
- await deps.answerCallbackQuery(callbackQueryId);
1132
- return true;
1133
- }
1134
-
1135
- export async function handleTelegramThinkingMenuCallbackAction(
1136
- callbackQueryId: string,
1137
- data: string | undefined,
1138
- activeModel: MenuModel | undefined,
1139
- deps: TelegramThinkingMenuCallbackDeps,
1140
- ): Promise<boolean> {
1141
- const action = parseTelegramMenuCallbackAction(data);
1142
- if (action.kind !== "thinking:set") return false;
1143
- if (!isThinkingLevel(action.level)) {
1144
- await deps.answerCallbackQuery(callbackQueryId, "Invalid thinking level.");
1145
- return true;
1146
- }
1147
- if (!activeModel?.reasoning) {
1148
- await deps.answerCallbackQuery(
1149
- callbackQueryId,
1150
- "This model has no reasoning controls.",
1151
- );
1152
- return true;
1153
- }
1154
- deps.setThinkingLevel(action.level);
1155
- await deps.updateStatusMessage();
1156
- await deps.answerCallbackQuery(
1157
- callbackQueryId,
1158
- `Thinking: ${deps.getCurrentThinkingLevel()}`,
1159
- );
1160
- return true;
1161
- }
1162
-
1163
- export function buildThinkingMenuText(
1164
- activeModel: MenuModel | undefined,
1165
- currentThinkingLevel: ThinkingLevel,
1166
- ): string {
1167
- const lines = ["Choose a thinking level"];
1168
- if (activeModel) {
1169
- lines.push(`Model: ${getCanonicalModelId(activeModel)}`);
1170
- }
1171
- lines.push(`Current: ${currentThinkingLevel}`);
1172
- return lines.join("\n");
1173
- }
1174
-
1175
- export function getTelegramModelMenuPage(
1176
- state: TelegramModelMenuState,
1177
- pageSize: number,
1178
- ): TelegramModelMenuPage {
1179
- const items = getModelMenuItems(state);
1180
- const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
1181
- const page = Math.max(0, Math.min(state.page, pageCount - 1));
1182
- const start = page * pageSize;
1183
- return {
1184
- page,
1185
- pageCount,
1186
- start,
1187
- items: items.slice(start, start + pageSize),
1188
- };
1189
- }
1190
-
1191
- export function buildModelMenuReplyMarkup(
1192
- state: TelegramModelMenuState,
1193
- currentModel: MenuModel | undefined,
1194
- pageSize: number,
1195
- ): TelegramReplyMarkup {
1196
- const menuPage = getTelegramModelMenuPage(state, pageSize);
1197
- const rows = menuPage.items.map((entry, index) => [
1198
- {
1199
- text: formatScopedModelButtonText(entry, currentModel),
1200
- callback_data: `model:pick:${menuPage.start + index}`,
1201
- },
1202
- ]);
1203
- if (menuPage.pageCount > 1) {
1204
- const previousPage =
1205
- menuPage.page === 0 ? menuPage.pageCount - 1 : menuPage.page - 1;
1206
- const nextPage =
1207
- menuPage.page === menuPage.pageCount - 1 ? 0 : menuPage.page + 1;
1208
- rows.push([
1209
- { text: "⬅️", callback_data: `model:page:${previousPage}` },
1210
- {
1211
- text: `${menuPage.page + 1}/${menuPage.pageCount}`,
1212
- callback_data: "model:noop",
1213
- },
1214
- { text: "➡️", callback_data: `model:page:${nextPage}` },
1215
- ]);
1216
- }
1217
- if (state.scopedModels.length > 0) {
1218
- rows.push([
1219
- {
1220
- text: state.scope === "scoped" ? "✅ Scoped" : "Scoped",
1221
- callback_data: "model:scope:scoped",
1222
- },
1223
- {
1224
- text: state.scope === "all" ? "✅ All" : "All",
1225
- callback_data: "model:scope:all",
1226
- },
1227
- ]);
1228
- }
1229
- return { inline_keyboard: rows };
1230
- }
1231
-
1232
- export function buildThinkingMenuReplyMarkup(
1233
- currentThinkingLevel: ThinkingLevel,
1234
- ): TelegramReplyMarkup {
1235
- return {
1236
- inline_keyboard: THINKING_LEVELS.map((level) => [
1237
- {
1238
- text: level === currentThinkingLevel ? `✅ ${level}` : level,
1239
- callback_data: `thinking:set:${level}`,
1240
- },
1241
- ]),
1242
- };
1243
- }
1244
-
1245
- export function buildStatusReplyMarkup(
1246
- activeModel: MenuModel | undefined,
1247
- currentThinkingLevel: ThinkingLevel,
1248
- ): TelegramReplyMarkup {
1249
- const rows: Array<Array<{ text: string; callback_data: string }>> = [];
1250
- rows.push([
1251
- {
1252
- text: formatStatusButtonLabel(
1253
- "Model",
1254
- activeModel ? getCanonicalModelId(activeModel) : "unknown",
1255
- ),
1256
- callback_data: "status:model",
1257
- },
1258
- ]);
1259
- if (activeModel?.reasoning) {
1260
- rows.push([
1261
- {
1262
- text: formatStatusButtonLabel("Thinking", currentThinkingLevel),
1263
- callback_data: "status:thinking",
1264
- },
1265
- ]);
1266
- }
1267
- return { inline_keyboard: rows };
1268
- }
1269
-
1270
- export function buildTelegramModelMenuRenderPayload(
1271
- state: TelegramModelMenuState,
1272
- activeModel: MenuModel | undefined,
1273
- ): TelegramMenuRenderPayload {
1274
- return {
1275
- nextMode: "model",
1276
- text: MODEL_MENU_TITLE,
1277
- mode: "html",
1278
- replyMarkup: buildModelMenuReplyMarkup(
1279
- state,
1280
- activeModel,
1281
- TELEGRAM_MODEL_PAGE_SIZE,
1282
- ),
1283
- };
1284
- }
1285
-
1286
- export function buildTelegramThinkingMenuRenderPayload(
1287
- activeModel: MenuModel | undefined,
1288
- currentThinkingLevel: ThinkingLevel,
1289
- ): TelegramMenuRenderPayload {
1290
- return {
1291
- nextMode: "thinking",
1292
- text: buildThinkingMenuText(activeModel, currentThinkingLevel),
1293
- mode: "plain",
1294
- replyMarkup: buildThinkingMenuReplyMarkup(currentThinkingLevel),
1295
- };
1296
- }
1297
-
1298
- export function buildTelegramStatusMenuRenderPayload(
1299
- statusText: string,
1300
- activeModel: MenuModel | undefined,
1301
- currentThinkingLevel: ThinkingLevel,
1302
- ): TelegramMenuRenderPayload {
1303
- return {
1304
- nextMode: "status",
1305
- text: statusText,
1306
- mode: "html",
1307
- replyMarkup: buildStatusReplyMarkup(activeModel, currentThinkingLevel),
1308
- };
1309
- }
1310
-
1311
526
  export interface TelegramMenuActionRuntimeWithStateBuilderDeps<
1312
527
  TModel extends MenuModel = MenuModel,
1313
528
  TContext extends TelegramModelMenuStateBuilderContext<TModel> =
@@ -1332,6 +547,7 @@ export function createTelegramMenuActionRuntimeWithStateBuilder<
1332
547
  }),
1333
548
  getActiveModel: deps.getActiveModel,
1334
549
  getThinkingLevel: deps.getThinkingLevel,
550
+ getQueueItemCount: deps.getQueueItemCount,
1335
551
  buildStatusHtml: deps.buildStatusHtml,
1336
552
  storeModelMenuState: deps.storeModelMenuState,
1337
553
  isIdle: deps.isIdle,
@@ -1365,6 +581,7 @@ export function createTelegramMenuActionRuntime<
1365
581
  deps.getActiveModel(ctx),
1366
582
  deps.getThinkingLevel(),
1367
583
  deps,
584
+ deps.getQueueItemCount?.() ?? 0,
1368
585
  ),
1369
586
  sendStatusMessage: (chatId, replyToMessageId, ctx) =>
1370
587
  openTelegramStatusMenu({
@@ -1373,20 +590,28 @@ export function createTelegramMenuActionRuntime<
1373
590
  await deps.sendTextReply(
1374
591
  chatId,
1375
592
  replyToMessageId,
1376
- "Cannot open status while pi is busy. Send /stop first.",
593
+ "Cannot open status while π is busy. Send /abort, /next, or /stop.",
1377
594
  );
1378
595
  },
1379
596
  getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
1380
597
  buildStatusHtml: () => deps.buildStatusHtml(ctx),
1381
598
  getActiveModel: () => deps.getActiveModel(ctx),
1382
599
  getThinkingLevel: deps.getThinkingLevel,
1383
- sendStatusMenu: (state, statusHtml, activeModel, thinkingLevel) =>
600
+ getQueueItemCount: deps.getQueueItemCount,
601
+ sendStatusMenu: (
602
+ state,
603
+ statusHtml,
604
+ activeModel,
605
+ thinkingLevel,
606
+ queueItemCount,
607
+ ) =>
1384
608
  sendTelegramStatusMessage(
1385
609
  state,
1386
610
  statusHtml,
1387
611
  activeModel,
1388
612
  thinkingLevel,
1389
613
  deps,
614
+ queueItemCount,
1390
615
  ),
1391
616
  storeModelMenuState: deps.storeModelMenuState,
1392
617
  }),
@@ -1399,7 +624,7 @@ export function createTelegramMenuActionRuntime<
1399
624
  await deps.sendTextReply(
1400
625
  chatId,
1401
626
  replyToMessageId,
1402
- "Cannot switch model while pi is busy. Send /stop first.",
627
+ "Cannot switch model while π is busy. Send /abort, /next, or /stop.",
1403
628
  );
1404
629
  },
1405
630
  sendNoModelsMessage: async () => {
@@ -1415,115 +640,14 @@ export function createTelegramMenuActionRuntime<
1415
640
  sendTelegramModelMenuMessage(state, activeModel, deps),
1416
641
  storeModelMenuState: deps.storeModelMenuState,
1417
642
  }),
643
+ openThinkingMenu: (chatId, _replyToMessageId, ctx) =>
644
+ openTelegramThinkingMenu({
645
+ getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
646
+ getActiveModel: () => deps.getActiveModel(ctx),
647
+ getThinkingLevel: deps.getThinkingLevel,
648
+ storeModelMenuState: deps.storeModelMenuState,
649
+ editInteractiveMessage: deps.editInteractiveMessage,
650
+ sendInteractiveMessage: deps.sendInteractiveMessage,
651
+ }),
1418
652
  };
1419
653
  }
1420
-
1421
- function applyTelegramMenuRenderPayload(
1422
- state: TelegramModelMenuState,
1423
- payload: TelegramMenuRenderPayload,
1424
- ): TelegramMenuRenderPayload {
1425
- state.mode = payload.nextMode;
1426
- return payload;
1427
- }
1428
-
1429
- async function editTelegramMenuMessage(
1430
- state: TelegramModelMenuState,
1431
- payload: TelegramMenuRenderPayload,
1432
- deps: TelegramMenuMessageRuntimeDeps,
1433
- ): Promise<void> {
1434
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
1435
- await deps.editInteractiveMessage(
1436
- state.chatId,
1437
- state.messageId,
1438
- appliedPayload.text,
1439
- appliedPayload.mode,
1440
- appliedPayload.replyMarkup,
1441
- );
1442
- }
1443
-
1444
- function sendTelegramMenuMessage(
1445
- state: TelegramModelMenuState,
1446
- payload: TelegramMenuRenderPayload,
1447
- deps: TelegramMenuMessageRuntimeDeps,
1448
- ): Promise<number | undefined> {
1449
- const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
1450
- return deps.sendInteractiveMessage(
1451
- state.chatId,
1452
- appliedPayload.text,
1453
- appliedPayload.mode,
1454
- appliedPayload.replyMarkup,
1455
- );
1456
- }
1457
-
1458
- export async function updateTelegramModelMenuMessage(
1459
- state: TelegramModelMenuState,
1460
- activeModel: MenuModel | undefined,
1461
- deps: TelegramMenuMessageRuntimeDeps,
1462
- ): Promise<void> {
1463
- await editTelegramMenuMessage(
1464
- state,
1465
- buildTelegramModelMenuRenderPayload(state, activeModel),
1466
- deps,
1467
- );
1468
- }
1469
-
1470
- export async function updateTelegramThinkingMenuMessage(
1471
- state: TelegramModelMenuState,
1472
- activeModel: MenuModel | undefined,
1473
- currentThinkingLevel: ThinkingLevel,
1474
- deps: TelegramMenuMessageRuntimeDeps,
1475
- ): Promise<void> {
1476
- await editTelegramMenuMessage(
1477
- state,
1478
- buildTelegramThinkingMenuRenderPayload(activeModel, currentThinkingLevel),
1479
- deps,
1480
- );
1481
- }
1482
-
1483
- export async function updateTelegramStatusMessage(
1484
- state: TelegramModelMenuState,
1485
- statusText: string,
1486
- activeModel: MenuModel | undefined,
1487
- currentThinkingLevel: ThinkingLevel,
1488
- deps: TelegramMenuMessageRuntimeDeps,
1489
- ): Promise<void> {
1490
- await editTelegramMenuMessage(
1491
- state,
1492
- buildTelegramStatusMenuRenderPayload(
1493
- statusText,
1494
- activeModel,
1495
- currentThinkingLevel,
1496
- ),
1497
- deps,
1498
- );
1499
- }
1500
-
1501
- export function sendTelegramStatusMessage(
1502
- state: TelegramModelMenuState,
1503
- statusText: string,
1504
- activeModel: MenuModel | undefined,
1505
- currentThinkingLevel: ThinkingLevel,
1506
- deps: TelegramMenuMessageRuntimeDeps,
1507
- ): Promise<number | undefined> {
1508
- return sendTelegramMenuMessage(
1509
- state,
1510
- buildTelegramStatusMenuRenderPayload(
1511
- statusText,
1512
- activeModel,
1513
- currentThinkingLevel,
1514
- ),
1515
- deps,
1516
- );
1517
- }
1518
-
1519
- export function sendTelegramModelMenuMessage(
1520
- state: TelegramModelMenuState,
1521
- activeModel: MenuModel | undefined,
1522
- deps: TelegramMenuMessageRuntimeDeps,
1523
- ): Promise<number | undefined> {
1524
- return sendTelegramMenuMessage(
1525
- state,
1526
- buildTelegramModelMenuRenderPayload(state, activeModel),
1527
- deps,
1528
- );
1529
- }