@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/AGENTS.md +153 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +148 -0
- package/README.md +40 -39
- package/docs/README.md +1 -0
- package/docs/architecture.md +36 -27
- package/docs/attachment-handlers.md +4 -6
- package/docs/callback-namespaces.md +36 -0
- package/docs/command-templates.md +53 -9
- package/docs/locks.md +4 -0
- package/docs/outbound-handlers.md +6 -5
- package/index.ts +60 -7
- package/lib/api.ts +1 -0
- package/lib/attachment-handlers.ts +21 -10
- package/lib/attachments.ts +1 -0
- package/lib/command-templates.ts +37 -3
- package/lib/commands.ts +363 -88
- package/lib/config.ts +6 -2
- package/lib/keyboard.ts +14 -0
- package/lib/lifecycle.ts +26 -0
- package/lib/locks.ts +3 -2
- package/lib/media.ts +1 -0
- package/lib/menu-model.ts +881 -0
- package/lib/menu-queue.ts +610 -0
- package/lib/menu-status.ts +226 -0
- package/lib/menu-thinking.ts +171 -0
- package/lib/menu.ts +143 -1019
- package/lib/model.ts +1 -0
- package/lib/outbound-handlers.ts +28 -19
- package/lib/pi.ts +8 -0
- package/lib/polling.ts +1 -0
- package/lib/preview.ts +97 -50
- package/lib/prompt-templates.ts +150 -0
- package/lib/prompts.ts +2 -0
- package/lib/queue.ts +60 -15
- package/lib/rendering.ts +1 -0
- package/lib/replies.ts +90 -2
- package/lib/routing.ts +106 -14
- package/lib/runtime.ts +2 -0
- package/lib/setup.ts +1 -0
- package/lib/status.ts +18 -6
- package/lib/turns.ts +1 -0
- package/lib/updates.ts +55 -6
- package/package.json +5 -2
|
@@ -0,0 +1,881 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram model menu UI helpers
|
|
3
|
+
* Zones: telegram ui, model controls, menu composition
|
|
4
|
+
* Owns model-menu state, scoped model pages, model callback planning, and model-menu message rendering
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
8
|
+
import {
|
|
9
|
+
type MenuModel,
|
|
10
|
+
modelsMatch,
|
|
11
|
+
parseTelegramCliScopedModelPatterns,
|
|
12
|
+
resolveScopedModelPatterns,
|
|
13
|
+
type ScopedTelegramModel,
|
|
14
|
+
sortScopedModels,
|
|
15
|
+
type ThinkingLevel,
|
|
16
|
+
} from "./model.ts";
|
|
17
|
+
|
|
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" | "model-pages" | "thinking" | "queue";
|
|
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 = TelegramInlineKeyboardMarkup;
|
|
111
|
+
|
|
112
|
+
export interface TelegramMenuMessageRuntimeDeps {
|
|
113
|
+
editInteractiveMessage: (
|
|
114
|
+
chatId: number,
|
|
115
|
+
messageId: number,
|
|
116
|
+
text: string,
|
|
117
|
+
mode: "html" | "plain",
|
|
118
|
+
replyMarkup: TelegramReplyMarkup,
|
|
119
|
+
) => Promise<void>;
|
|
120
|
+
sendInteractiveMessage: (
|
|
121
|
+
chatId: number,
|
|
122
|
+
text: string,
|
|
123
|
+
mode: "html" | "plain",
|
|
124
|
+
replyMarkup: TelegramReplyMarkup,
|
|
125
|
+
) => Promise<number | undefined>;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export type TelegramModelMenuCallbackDeps<
|
|
129
|
+
TModel extends MenuModel = MenuModel,
|
|
130
|
+
> = {
|
|
131
|
+
answerCallbackQuery: (
|
|
132
|
+
callbackQueryId: string,
|
|
133
|
+
text?: string,
|
|
134
|
+
) => Promise<void>;
|
|
135
|
+
updateModelMenuMessage: () => Promise<void>;
|
|
136
|
+
updateStatusMessage: () => Promise<void>;
|
|
137
|
+
setModel: (model: TModel) => Promise<boolean>;
|
|
138
|
+
setCurrentModel: (model: TModel) => void;
|
|
139
|
+
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
140
|
+
stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
|
|
141
|
+
restartInterruptedTelegramTurn: (
|
|
142
|
+
selection: ScopedTelegramModel<TModel>,
|
|
143
|
+
) => Promise<boolean> | boolean;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export interface TelegramModelMenuOpenDeps<
|
|
147
|
+
TModel extends MenuModel = MenuModel,
|
|
148
|
+
> {
|
|
149
|
+
isIdle: () => boolean;
|
|
150
|
+
canOfferInFlightModelSwitch: () => boolean;
|
|
151
|
+
sendBusyMessage: () => Promise<void>;
|
|
152
|
+
sendNoModelsMessage: () => Promise<void>;
|
|
153
|
+
getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
|
|
154
|
+
getActiveModel: () => TModel | undefined;
|
|
155
|
+
sendModelMenu: (
|
|
156
|
+
state: TelegramModelMenuState<TModel>,
|
|
157
|
+
activeModel: TModel | undefined,
|
|
158
|
+
) => Promise<number | undefined>;
|
|
159
|
+
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface BuildTelegramModelMenuStateParams<
|
|
163
|
+
TModel extends MenuModel = MenuModel,
|
|
164
|
+
> {
|
|
165
|
+
chatId: number;
|
|
166
|
+
activeModel: TModel | undefined;
|
|
167
|
+
availableModels: TModel[];
|
|
168
|
+
configuredScopedModelPatterns: string[];
|
|
169
|
+
cliScopedModelPatterns?: string[];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export type TelegramMenuMutationResult = "invalid" | "unchanged" | "changed";
|
|
173
|
+
export type TelegramMenuSelectionResult<TModel extends MenuModel = MenuModel> =
|
|
174
|
+
| { kind: "invalid" }
|
|
175
|
+
| { kind: "missing" }
|
|
176
|
+
| { kind: "selected"; selection: ScopedTelegramModel<TModel> };
|
|
177
|
+
|
|
178
|
+
export interface TelegramModelMenuPage<TModel extends MenuModel = MenuModel> {
|
|
179
|
+
page: number;
|
|
180
|
+
pageCount: number;
|
|
181
|
+
start: number;
|
|
182
|
+
items: ScopedTelegramModel<TModel>[];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export interface TelegramMenuRenderPayload {
|
|
186
|
+
nextMode: TelegramModelMenuState["mode"];
|
|
187
|
+
text: string;
|
|
188
|
+
mode: "html" | "plain";
|
|
189
|
+
replyMarkup: TelegramReplyMarkup;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export type TelegramModelCallbackPlan<TModel extends MenuModel = MenuModel> =
|
|
193
|
+
| { kind: "ignore" }
|
|
194
|
+
| { kind: "answer"; text?: string }
|
|
195
|
+
| { kind: "update-menu"; text?: string }
|
|
196
|
+
| {
|
|
197
|
+
kind: "refresh-status";
|
|
198
|
+
selection: ScopedTelegramModel<TModel>;
|
|
199
|
+
callbackText: string;
|
|
200
|
+
shouldApplyThinkingLevel: boolean;
|
|
201
|
+
}
|
|
202
|
+
| {
|
|
203
|
+
kind: "switch-model";
|
|
204
|
+
selection: ScopedTelegramModel<TModel>;
|
|
205
|
+
mode: "idle" | "restart-now" | "restart-after-tool";
|
|
206
|
+
callbackText: string;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
export interface BuildTelegramModelCallbackPlanParams<
|
|
210
|
+
TModel extends MenuModel = MenuModel,
|
|
211
|
+
> {
|
|
212
|
+
data: string | undefined;
|
|
213
|
+
state: TelegramModelMenuState<TModel>;
|
|
214
|
+
activeModel: TModel | undefined;
|
|
215
|
+
currentThinkingLevel: ThinkingLevel;
|
|
216
|
+
isIdle: boolean;
|
|
217
|
+
canRestartBusyRun: boolean;
|
|
218
|
+
hasActiveToolExecutions: boolean;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export interface TelegramModelMenuRuntime<
|
|
222
|
+
TModel extends MenuModel = MenuModel,
|
|
223
|
+
> {
|
|
224
|
+
storeState: (state: TelegramModelMenuState<TModel>) => void;
|
|
225
|
+
getState: (
|
|
226
|
+
messageId: number | undefined,
|
|
227
|
+
) => TelegramModelMenuState<TModel> | undefined;
|
|
228
|
+
clear: () => void;
|
|
229
|
+
buildState: <TContext extends TelegramModelMenuRuntimeContext<TModel>>(
|
|
230
|
+
options: Omit<
|
|
231
|
+
TelegramModelMenuRuntimeOptions<TContext, TModel>,
|
|
232
|
+
"cachedInputs" | "cacheTtlMs"
|
|
233
|
+
>,
|
|
234
|
+
) => Promise<TelegramModelMenuState<TModel>>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export const TELEGRAM_MODEL_PAGE_SIZE = 6;
|
|
238
|
+
const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
|
|
239
|
+
export const MODEL_MENU_TITLE = "<b>Choose a model:</b>";
|
|
240
|
+
export const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
|
|
241
|
+
|
|
242
|
+
function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
|
|
243
|
+
return label.length <= maxLength
|
|
244
|
+
? label
|
|
245
|
+
: `${label.slice(0, maxLength - 1)}…`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function getTelegramCliScopedModelPatterns(): string[] | undefined {
|
|
249
|
+
return parseTelegramCliScopedModelPatterns(process.argv.slice(2));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function parseTelegramModelMenuCallbackAction(
|
|
253
|
+
data: string | undefined,
|
|
254
|
+
):
|
|
255
|
+
| { action: "noop" | "scope" | "page" | "pages" | "pick"; value?: string }
|
|
256
|
+
| undefined {
|
|
257
|
+
if (!data?.startsWith("model:")) return undefined;
|
|
258
|
+
const [, action, value] = data.split(":");
|
|
259
|
+
if (
|
|
260
|
+
action === "noop" ||
|
|
261
|
+
action === "scope" ||
|
|
262
|
+
action === "page" ||
|
|
263
|
+
action === "pages" ||
|
|
264
|
+
action === "pick"
|
|
265
|
+
) {
|
|
266
|
+
return { action, value };
|
|
267
|
+
}
|
|
268
|
+
return undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function applyTelegramMenuRenderPayload(
|
|
272
|
+
state: TelegramModelMenuState,
|
|
273
|
+
payload: TelegramMenuRenderPayload,
|
|
274
|
+
): TelegramMenuRenderPayload {
|
|
275
|
+
state.mode = payload.nextMode;
|
|
276
|
+
return payload;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function editTelegramMenuMessage(
|
|
280
|
+
state: TelegramModelMenuState,
|
|
281
|
+
payload: TelegramMenuRenderPayload,
|
|
282
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
283
|
+
): Promise<void> {
|
|
284
|
+
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
|
|
285
|
+
await deps.editInteractiveMessage(
|
|
286
|
+
state.chatId,
|
|
287
|
+
state.messageId,
|
|
288
|
+
appliedPayload.text,
|
|
289
|
+
appliedPayload.mode,
|
|
290
|
+
appliedPayload.replyMarkup,
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function sendTelegramMenuMessage(
|
|
295
|
+
state: TelegramModelMenuState,
|
|
296
|
+
payload: TelegramMenuRenderPayload,
|
|
297
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
298
|
+
): Promise<number | undefined> {
|
|
299
|
+
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
|
|
300
|
+
return deps.sendInteractiveMessage(
|
|
301
|
+
state.chatId,
|
|
302
|
+
appliedPayload.text,
|
|
303
|
+
appliedPayload.mode,
|
|
304
|
+
appliedPayload.replyMarkup,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function formatScopedModelButtonText<
|
|
309
|
+
TModel extends MenuModel = MenuModel,
|
|
310
|
+
>(
|
|
311
|
+
entry: ScopedTelegramModel<TModel>,
|
|
312
|
+
currentModel: TModel | undefined,
|
|
313
|
+
): string {
|
|
314
|
+
let label = `${modelsMatch(entry.model, currentModel) ? "✅ " : ""}${entry.model.id} [${entry.model.provider}]`;
|
|
315
|
+
if (entry.thinkingLevel) {
|
|
316
|
+
label += ` · ${entry.thinkingLevel}`;
|
|
317
|
+
}
|
|
318
|
+
return truncateTelegramButtonLabel(label);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function formatStatusButtonLabel(label: string, value: string): string {
|
|
322
|
+
return truncateTelegramButtonLabel(`${label}: ${value}`, 64);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function getModelMenuItems<TModel extends MenuModel = MenuModel>(
|
|
326
|
+
state: TelegramModelMenuState<TModel>,
|
|
327
|
+
): ScopedTelegramModel<TModel>[] {
|
|
328
|
+
return state.scope === "scoped" && state.scopedModels.length > 0
|
|
329
|
+
? state.scopedModels
|
|
330
|
+
: state.allModels;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function pruneStoredTelegramModelMenus<
|
|
334
|
+
TModel extends MenuModel = MenuModel,
|
|
335
|
+
>(
|
|
336
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
337
|
+
options: TelegramModelMenuStoreOptions,
|
|
338
|
+
): void {
|
|
339
|
+
const now = options.now ?? Date.now();
|
|
340
|
+
for (const [messageId, entry] of menus.entries()) {
|
|
341
|
+
if (now - entry.updatedAt <= options.maxAgeMs) continue;
|
|
342
|
+
menus.delete(messageId);
|
|
343
|
+
}
|
|
344
|
+
while (menus.size > options.maxStoredMenus) {
|
|
345
|
+
const oldestMessageId = menus.keys().next().value as number | undefined;
|
|
346
|
+
if (oldestMessageId === undefined) return;
|
|
347
|
+
menus.delete(oldestMessageId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function storeTelegramModelMenuState<
|
|
352
|
+
TModel extends MenuModel = MenuModel,
|
|
353
|
+
>(
|
|
354
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
355
|
+
state: TelegramModelMenuState<TModel>,
|
|
356
|
+
options: TelegramModelMenuStoreOptions,
|
|
357
|
+
): void {
|
|
358
|
+
const now = options.now ?? Date.now();
|
|
359
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
360
|
+
menus.set(state.messageId, { state, updatedAt: now });
|
|
361
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function getStoredTelegramModelMenuState<
|
|
365
|
+
TModel extends MenuModel = MenuModel,
|
|
366
|
+
>(
|
|
367
|
+
menus: Map<number, StoredTelegramModelMenuState<TModel>>,
|
|
368
|
+
messageId: number | undefined,
|
|
369
|
+
options: TelegramModelMenuStoreOptions,
|
|
370
|
+
): TelegramModelMenuState<TModel> | undefined {
|
|
371
|
+
if (messageId === undefined) return undefined;
|
|
372
|
+
const now = options.now ?? Date.now();
|
|
373
|
+
pruneStoredTelegramModelMenus(menus, { ...options, now });
|
|
374
|
+
const entry = menus.get(messageId);
|
|
375
|
+
if (!entry) return undefined;
|
|
376
|
+
menus.delete(messageId);
|
|
377
|
+
entry.updatedAt = now;
|
|
378
|
+
menus.set(messageId, entry);
|
|
379
|
+
return entry.state;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export function createTelegramModelMenuRuntime<
|
|
383
|
+
TModel extends MenuModel = MenuModel,
|
|
384
|
+
>(
|
|
385
|
+
options: Partial<TelegramModelMenuStoreOptions> = {},
|
|
386
|
+
): TelegramModelMenuRuntime<TModel> {
|
|
387
|
+
const menus = new Map<number, StoredTelegramModelMenuState<TModel>>();
|
|
388
|
+
let cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined;
|
|
389
|
+
const getStoreOptions = (): TelegramModelMenuStoreOptions => ({
|
|
390
|
+
maxAgeMs: options.maxAgeMs ?? TELEGRAM_MODEL_MENU_STATE_TTL_MS,
|
|
391
|
+
maxStoredMenus: options.maxStoredMenus ?? MAX_STORED_TELEGRAM_MODEL_MENUS,
|
|
392
|
+
now: options.now,
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
storeState: (state) => {
|
|
396
|
+
storeTelegramModelMenuState(menus, state, getStoreOptions());
|
|
397
|
+
},
|
|
398
|
+
getState: (messageId) =>
|
|
399
|
+
getStoredTelegramModelMenuState(menus, messageId, getStoreOptions()),
|
|
400
|
+
clear: () => {
|
|
401
|
+
menus.clear();
|
|
402
|
+
cachedInputs = undefined;
|
|
403
|
+
},
|
|
404
|
+
buildState: async (stateOptions) => {
|
|
405
|
+
const result = await buildTelegramModelMenuStateRuntime({
|
|
406
|
+
...stateOptions,
|
|
407
|
+
cachedInputs,
|
|
408
|
+
cacheTtlMs: TELEGRAM_MODEL_MENU_CACHE_TTL_MS,
|
|
409
|
+
});
|
|
410
|
+
cachedInputs = result.cachedInputs;
|
|
411
|
+
return result.state;
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function createTelegramModelMenuStateBuilder<
|
|
417
|
+
TModel extends MenuModel = MenuModel,
|
|
418
|
+
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
|
|
419
|
+
TelegramModelMenuStateBuilderContext<TModel>,
|
|
420
|
+
>(
|
|
421
|
+
deps: TelegramModelMenuStateBuilderDeps<TModel, TContext>,
|
|
422
|
+
): (chatId: number, ctx: TContext) => Promise<TelegramModelMenuState<TModel>> {
|
|
423
|
+
return async (chatId, ctx) => {
|
|
424
|
+
const settingsManager = deps.createSettingsManager(ctx.cwd);
|
|
425
|
+
return deps.runtime.buildState({
|
|
426
|
+
chatId,
|
|
427
|
+
activeModel: deps.getActiveModel(ctx),
|
|
428
|
+
ctx,
|
|
429
|
+
reloadSettings: () => settingsManager.reload(),
|
|
430
|
+
getConfiguredScopedModelPatterns: () =>
|
|
431
|
+
settingsManager.getEnabledModels(),
|
|
432
|
+
});
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
export async function resolveCachedTelegramModelMenuInputs<
|
|
437
|
+
TModel extends MenuModel = MenuModel,
|
|
438
|
+
>(
|
|
439
|
+
cachedInputs: CachedTelegramModelMenuInputs<TModel> | undefined,
|
|
440
|
+
deps: TelegramModelMenuInputCacheDeps<TModel>,
|
|
441
|
+
): Promise<CachedTelegramModelMenuInputs<TModel>> {
|
|
442
|
+
const now = deps.now ?? Date.now();
|
|
443
|
+
if (cachedInputs && cachedInputs.expiresAt > now) return cachedInputs;
|
|
444
|
+
await deps.reloadSettings();
|
|
445
|
+
const availableModels = deps.refreshAvailableModels();
|
|
446
|
+
const cliScopedModelPatterns = deps.getCliScopedModelPatterns();
|
|
447
|
+
const configuredScopedModelPatterns =
|
|
448
|
+
cliScopedModelPatterns ?? deps.getConfiguredScopedModelPatterns() ?? [];
|
|
449
|
+
return {
|
|
450
|
+
expiresAt: now + deps.cacheTtlMs,
|
|
451
|
+
availableModels,
|
|
452
|
+
configuredScopedModelPatterns,
|
|
453
|
+
cliScopedModelPatterns,
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function buildTelegramModelMenuState<
|
|
458
|
+
TModel extends MenuModel = MenuModel,
|
|
459
|
+
>(
|
|
460
|
+
params: BuildTelegramModelMenuStateParams<TModel>,
|
|
461
|
+
): TelegramModelMenuState<TModel> {
|
|
462
|
+
const allModels = sortScopedModels(
|
|
463
|
+
params.availableModels.map((model) => ({ model })),
|
|
464
|
+
params.activeModel,
|
|
465
|
+
);
|
|
466
|
+
const scopedModels =
|
|
467
|
+
params.configuredScopedModelPatterns.length > 0
|
|
468
|
+
? sortScopedModels(
|
|
469
|
+
resolveScopedModelPatterns(
|
|
470
|
+
params.configuredScopedModelPatterns,
|
|
471
|
+
params.availableModels,
|
|
472
|
+
),
|
|
473
|
+
params.activeModel,
|
|
474
|
+
)
|
|
475
|
+
: [];
|
|
476
|
+
let note: string | undefined;
|
|
477
|
+
if (
|
|
478
|
+
params.configuredScopedModelPatterns.length > 0 &&
|
|
479
|
+
scopedModels.length === 0
|
|
480
|
+
) {
|
|
481
|
+
note = params.cliScopedModelPatterns
|
|
482
|
+
? "No CLI scoped models matched the current auth configuration. Showing all available models."
|
|
483
|
+
: "No scoped models matched the current auth configuration. Showing all available models.";
|
|
484
|
+
}
|
|
485
|
+
return {
|
|
486
|
+
chatId: params.chatId,
|
|
487
|
+
messageId: 0,
|
|
488
|
+
page: 0,
|
|
489
|
+
scope: scopedModels.length > 0 ? "scoped" : "all",
|
|
490
|
+
scopedModels,
|
|
491
|
+
allModels,
|
|
492
|
+
note,
|
|
493
|
+
mode: "status",
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
export async function buildTelegramModelMenuStateRuntime<
|
|
498
|
+
TContext extends TelegramModelMenuRuntimeContext<TModel>,
|
|
499
|
+
TModel extends MenuModel = MenuModel,
|
|
500
|
+
>(
|
|
501
|
+
options: TelegramModelMenuRuntimeOptions<TContext, TModel>,
|
|
502
|
+
): Promise<{
|
|
503
|
+
state: TelegramModelMenuState<TModel>;
|
|
504
|
+
cachedInputs: CachedTelegramModelMenuInputs<TModel>;
|
|
505
|
+
}> {
|
|
506
|
+
const cachedInputs = await resolveCachedTelegramModelMenuInputs(
|
|
507
|
+
options.cachedInputs,
|
|
508
|
+
{
|
|
509
|
+
cacheTtlMs: options.cacheTtlMs,
|
|
510
|
+
reloadSettings: options.reloadSettings,
|
|
511
|
+
refreshAvailableModels: () => {
|
|
512
|
+
options.ctx.modelRegistry.refresh();
|
|
513
|
+
return options.ctx.modelRegistry.getAvailable();
|
|
514
|
+
},
|
|
515
|
+
getConfiguredScopedModelPatterns:
|
|
516
|
+
options.getConfiguredScopedModelPatterns,
|
|
517
|
+
getCliScopedModelPatterns:
|
|
518
|
+
options.getCliScopedModelPatterns ?? getTelegramCliScopedModelPatterns,
|
|
519
|
+
},
|
|
520
|
+
);
|
|
521
|
+
return {
|
|
522
|
+
cachedInputs,
|
|
523
|
+
state: buildTelegramModelMenuState({
|
|
524
|
+
chatId: options.chatId,
|
|
525
|
+
activeModel: options.activeModel,
|
|
526
|
+
availableModels: cachedInputs.availableModels,
|
|
527
|
+
configuredScopedModelPatterns: cachedInputs.configuredScopedModelPatterns,
|
|
528
|
+
cliScopedModelPatterns: cachedInputs.cliScopedModelPatterns,
|
|
529
|
+
}),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
export function applyTelegramModelScopeSelection(
|
|
534
|
+
state: TelegramModelMenuState,
|
|
535
|
+
value: string | undefined,
|
|
536
|
+
): TelegramMenuMutationResult {
|
|
537
|
+
if (value !== "all" && value !== "scoped") return "invalid";
|
|
538
|
+
if (value === state.scope) return "unchanged";
|
|
539
|
+
state.scope = value;
|
|
540
|
+
state.page = 0;
|
|
541
|
+
return "changed";
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function applyTelegramModelPageSelection(
|
|
545
|
+
state: TelegramModelMenuState,
|
|
546
|
+
value: string | undefined,
|
|
547
|
+
): TelegramMenuMutationResult {
|
|
548
|
+
const page = Number(value);
|
|
549
|
+
if (!Number.isFinite(page)) return "invalid";
|
|
550
|
+
if (page === state.page) return "unchanged";
|
|
551
|
+
state.page = page;
|
|
552
|
+
return "changed";
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
export function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(
|
|
556
|
+
state: TelegramModelMenuState<TModel>,
|
|
557
|
+
value: string | undefined,
|
|
558
|
+
): TelegramMenuSelectionResult<TModel> {
|
|
559
|
+
const index = Number(value);
|
|
560
|
+
if (!Number.isFinite(index)) return { kind: "invalid" };
|
|
561
|
+
const selection = getModelMenuItems(state)[index];
|
|
562
|
+
if (!selection) return { kind: "missing" };
|
|
563
|
+
return { kind: "selected", selection };
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
export function buildTelegramModelCallbackPlan<
|
|
567
|
+
TModel extends MenuModel = MenuModel,
|
|
568
|
+
>(
|
|
569
|
+
params: BuildTelegramModelCallbackPlanParams<TModel>,
|
|
570
|
+
): TelegramModelCallbackPlan<TModel> {
|
|
571
|
+
const action = parseTelegramModelMenuCallbackAction(params.data);
|
|
572
|
+
if (!action) return { kind: "ignore" };
|
|
573
|
+
if (action.action === "noop") return { kind: "answer" };
|
|
574
|
+
if (action.action === "scope") {
|
|
575
|
+
const scopeResult = applyTelegramModelScopeSelection(
|
|
576
|
+
params.state,
|
|
577
|
+
action.value,
|
|
578
|
+
);
|
|
579
|
+
if (scopeResult === "invalid") {
|
|
580
|
+
return { kind: "answer", text: "Unknown model scope." };
|
|
581
|
+
}
|
|
582
|
+
if (scopeResult === "unchanged") {
|
|
583
|
+
return { kind: "answer" };
|
|
584
|
+
}
|
|
585
|
+
return {
|
|
586
|
+
kind: "update-menu",
|
|
587
|
+
text: params.state.scope === "scoped" ? "Scoped models" : "All models",
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
if (action.action === "pages") {
|
|
591
|
+
if (action.value === "back") {
|
|
592
|
+
params.state.mode = "model";
|
|
593
|
+
return { kind: "update-menu" };
|
|
594
|
+
}
|
|
595
|
+
params.state.mode = "model-pages";
|
|
596
|
+
return { kind: "update-menu" };
|
|
597
|
+
}
|
|
598
|
+
if (action.action === "page") {
|
|
599
|
+
const pageResult = applyTelegramModelPageSelection(
|
|
600
|
+
params.state,
|
|
601
|
+
action.value,
|
|
602
|
+
);
|
|
603
|
+
if (pageResult === "invalid") {
|
|
604
|
+
return { kind: "answer", text: "Invalid page." };
|
|
605
|
+
}
|
|
606
|
+
params.state.mode = "model";
|
|
607
|
+
if (pageResult === "unchanged") {
|
|
608
|
+
return { kind: "update-menu" };
|
|
609
|
+
}
|
|
610
|
+
return { kind: "update-menu" };
|
|
611
|
+
}
|
|
612
|
+
if (action.action !== "pick") {
|
|
613
|
+
return { kind: "answer" };
|
|
614
|
+
}
|
|
615
|
+
const selectionResult = getTelegramModelSelection(params.state, action.value);
|
|
616
|
+
if (selectionResult.kind === "invalid") {
|
|
617
|
+
return { kind: "answer", text: "Invalid model selection." };
|
|
618
|
+
}
|
|
619
|
+
if (selectionResult.kind === "missing") {
|
|
620
|
+
return { kind: "answer", text: "Selected model is no longer available." };
|
|
621
|
+
}
|
|
622
|
+
const selection = selectionResult.selection;
|
|
623
|
+
if (modelsMatch(selection.model, params.activeModel)) {
|
|
624
|
+
return {
|
|
625
|
+
kind: "refresh-status",
|
|
626
|
+
selection,
|
|
627
|
+
callbackText: `Model: ${selection.model.id}`,
|
|
628
|
+
shouldApplyThinkingLevel:
|
|
629
|
+
!!selection.thinkingLevel &&
|
|
630
|
+
selection.thinkingLevel !== params.currentThinkingLevel,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
if (!params.isIdle) {
|
|
634
|
+
if (!params.canRestartBusyRun) {
|
|
635
|
+
return {
|
|
636
|
+
kind: "answer",
|
|
637
|
+
text: "π is busy. Send /abort, /next, or /stop.",
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
kind: "switch-model",
|
|
642
|
+
selection,
|
|
643
|
+
mode: params.hasActiveToolExecutions
|
|
644
|
+
? "restart-after-tool"
|
|
645
|
+
: "restart-now",
|
|
646
|
+
callbackText: params.hasActiveToolExecutions
|
|
647
|
+
? `Switched to ${selection.model.id}. Restarting after the current tool finishes…`
|
|
648
|
+
: `Switching to ${selection.model.id} and continuing…`,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return {
|
|
652
|
+
kind: "switch-model",
|
|
653
|
+
selection,
|
|
654
|
+
mode: "idle",
|
|
655
|
+
callbackText: `Switched to ${selection.model.id}`,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export async function openTelegramModelMenu<
|
|
660
|
+
TModel extends MenuModel = MenuModel,
|
|
661
|
+
>(deps: TelegramModelMenuOpenDeps<TModel>): Promise<void> {
|
|
662
|
+
if (!deps.isIdle() && !deps.canOfferInFlightModelSwitch()) {
|
|
663
|
+
await deps.sendBusyMessage();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
const state = await deps.getModelMenuState();
|
|
667
|
+
if (state.allModels.length === 0) {
|
|
668
|
+
await deps.sendNoModelsMessage();
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
const messageId = await deps.sendModelMenu(state, deps.getActiveModel());
|
|
672
|
+
if (messageId === undefined) return;
|
|
673
|
+
state.messageId = messageId;
|
|
674
|
+
state.mode = "model";
|
|
675
|
+
deps.storeModelMenuState(state);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export async function handleTelegramModelMenuCallbackAction<
|
|
679
|
+
TModel extends MenuModel = MenuModel,
|
|
680
|
+
>(
|
|
681
|
+
callbackQueryId: string,
|
|
682
|
+
params: BuildTelegramModelCallbackPlanParams<TModel>,
|
|
683
|
+
deps: TelegramModelMenuCallbackDeps<TModel>,
|
|
684
|
+
): Promise<boolean> {
|
|
685
|
+
const plan = buildTelegramModelCallbackPlan(params);
|
|
686
|
+
if (plan.kind === "ignore") return false;
|
|
687
|
+
if (plan.kind === "answer") {
|
|
688
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
689
|
+
return true;
|
|
690
|
+
}
|
|
691
|
+
if (plan.kind === "update-menu") {
|
|
692
|
+
await deps.updateModelMenuMessage();
|
|
693
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
694
|
+
return true;
|
|
695
|
+
}
|
|
696
|
+
if (plan.kind === "refresh-status") {
|
|
697
|
+
if (plan.shouldApplyThinkingLevel && plan.selection.thinkingLevel) {
|
|
698
|
+
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
699
|
+
}
|
|
700
|
+
await deps.updateStatusMessage();
|
|
701
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
const changed = await deps.setModel(plan.selection.model);
|
|
705
|
+
if (changed === false) {
|
|
706
|
+
await deps.answerCallbackQuery(callbackQueryId, "Model is not available.");
|
|
707
|
+
return true;
|
|
708
|
+
}
|
|
709
|
+
deps.setCurrentModel(plan.selection.model);
|
|
710
|
+
if (plan.selection.thinkingLevel) {
|
|
711
|
+
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
712
|
+
}
|
|
713
|
+
await deps.updateStatusMessage();
|
|
714
|
+
if (plan.mode === "restart-after-tool") {
|
|
715
|
+
deps.stagePendingModelSwitch(plan.selection);
|
|
716
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
717
|
+
return true;
|
|
718
|
+
}
|
|
719
|
+
if (plan.mode === "restart-now") {
|
|
720
|
+
const restarted = await deps.restartInterruptedTelegramTurn(plan.selection);
|
|
721
|
+
if (!restarted) {
|
|
722
|
+
await deps.answerCallbackQuery(
|
|
723
|
+
callbackQueryId,
|
|
724
|
+
"π is busy. Send /abort, /next, or /stop.",
|
|
725
|
+
);
|
|
726
|
+
return true;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
export function getTelegramModelMenuPage(
|
|
734
|
+
state: TelegramModelMenuState,
|
|
735
|
+
pageSize: number,
|
|
736
|
+
): TelegramModelMenuPage {
|
|
737
|
+
const items = getModelMenuItems(state);
|
|
738
|
+
const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
|
|
739
|
+
const page = Math.max(0, Math.min(state.page, pageCount - 1));
|
|
740
|
+
const start = page * pageSize;
|
|
741
|
+
return {
|
|
742
|
+
page,
|
|
743
|
+
pageCount,
|
|
744
|
+
start,
|
|
745
|
+
items: items.slice(start, start + pageSize),
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
export function buildModelMenuReplyMarkup(
|
|
750
|
+
state: TelegramModelMenuState,
|
|
751
|
+
currentModel: MenuModel | undefined,
|
|
752
|
+
pageSize: number,
|
|
753
|
+
): TelegramReplyMarkup {
|
|
754
|
+
const menuPage = getTelegramModelMenuPage(state, pageSize);
|
|
755
|
+
const rows = [[{ text: "⬆️ Main menu", callback_data: "menu:back" }]];
|
|
756
|
+
if (state.scopedModels.length > 0) {
|
|
757
|
+
rows.push([
|
|
758
|
+
{
|
|
759
|
+
text: state.scope === "scoped" ? "✅ Scoped" : "Scoped",
|
|
760
|
+
callback_data: "model:scope:scoped",
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
text: state.scope === "all" ? "✅ All" : "All",
|
|
764
|
+
callback_data: "model:scope:all",
|
|
765
|
+
},
|
|
766
|
+
]);
|
|
767
|
+
}
|
|
768
|
+
const previousPage =
|
|
769
|
+
menuPage.page === 0 ? menuPage.pageCount - 1 : menuPage.page - 1;
|
|
770
|
+
const nextPage =
|
|
771
|
+
menuPage.page === menuPage.pageCount - 1 ? 0 : menuPage.page + 1;
|
|
772
|
+
rows.push(
|
|
773
|
+
menuPage.pageCount > 1
|
|
774
|
+
? [
|
|
775
|
+
{ text: "⬅️", callback_data: `model:page:${previousPage}` },
|
|
776
|
+
{
|
|
777
|
+
text: `${menuPage.page + 1}/${menuPage.pageCount}`,
|
|
778
|
+
callback_data: "model:pages",
|
|
779
|
+
},
|
|
780
|
+
{ text: "➡️", callback_data: `model:page:${nextPage}` },
|
|
781
|
+
]
|
|
782
|
+
: [
|
|
783
|
+
{
|
|
784
|
+
text: `${menuPage.page + 1}/${menuPage.pageCount}`,
|
|
785
|
+
callback_data: "model:pages",
|
|
786
|
+
},
|
|
787
|
+
],
|
|
788
|
+
);
|
|
789
|
+
rows.push(
|
|
790
|
+
...menuPage.items.map((entry, index) => [
|
|
791
|
+
{
|
|
792
|
+
text: formatScopedModelButtonText(entry, currentModel),
|
|
793
|
+
callback_data: `model:pick:${menuPage.start + index}`,
|
|
794
|
+
},
|
|
795
|
+
]),
|
|
796
|
+
);
|
|
797
|
+
return { inline_keyboard: rows };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
export function buildModelPageMenuReplyMarkup(
|
|
801
|
+
state: TelegramModelMenuState,
|
|
802
|
+
pageSize: number,
|
|
803
|
+
): TelegramReplyMarkup {
|
|
804
|
+
const menuPage = getTelegramModelMenuPage(state, pageSize);
|
|
805
|
+
const rows = [[{ text: "⬆️ Back", callback_data: "model:pages:back" }]];
|
|
806
|
+
for (
|
|
807
|
+
let page = 0;
|
|
808
|
+
page < menuPage.pageCount;
|
|
809
|
+
page += TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE
|
|
810
|
+
) {
|
|
811
|
+
rows.push(
|
|
812
|
+
Array.from(
|
|
813
|
+
{
|
|
814
|
+
length: Math.min(
|
|
815
|
+
TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE,
|
|
816
|
+
menuPage.pageCount - page,
|
|
817
|
+
),
|
|
818
|
+
},
|
|
819
|
+
(_unused, offset) => ({
|
|
820
|
+
text: String(page + offset + 1),
|
|
821
|
+
callback_data: `model:page:${page + offset}`,
|
|
822
|
+
}),
|
|
823
|
+
),
|
|
824
|
+
);
|
|
825
|
+
}
|
|
826
|
+
return { inline_keyboard: rows };
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
export function buildTelegramModelPageMenuRenderPayload(
|
|
830
|
+
state: TelegramModelMenuState,
|
|
831
|
+
): TelegramMenuRenderPayload {
|
|
832
|
+
return {
|
|
833
|
+
nextMode: "model-pages",
|
|
834
|
+
text: MODEL_PAGE_MENU_TITLE,
|
|
835
|
+
mode: "html",
|
|
836
|
+
replyMarkup: buildModelPageMenuReplyMarkup(state, TELEGRAM_MODEL_PAGE_SIZE),
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function buildTelegramModelMenuRenderPayload(
|
|
841
|
+
state: TelegramModelMenuState,
|
|
842
|
+
activeModel: MenuModel | undefined,
|
|
843
|
+
): TelegramMenuRenderPayload {
|
|
844
|
+
if (state.mode === "model-pages") {
|
|
845
|
+
return buildTelegramModelPageMenuRenderPayload(state);
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
nextMode: "model",
|
|
849
|
+
text: MODEL_MENU_TITLE,
|
|
850
|
+
mode: "html",
|
|
851
|
+
replyMarkup: buildModelMenuReplyMarkup(
|
|
852
|
+
state,
|
|
853
|
+
activeModel,
|
|
854
|
+
TELEGRAM_MODEL_PAGE_SIZE,
|
|
855
|
+
),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function updateTelegramModelMenuMessage(
|
|
860
|
+
state: TelegramModelMenuState,
|
|
861
|
+
activeModel: MenuModel | undefined,
|
|
862
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
863
|
+
): Promise<void> {
|
|
864
|
+
await editTelegramMenuMessage(
|
|
865
|
+
state,
|
|
866
|
+
buildTelegramModelMenuRenderPayload(state, activeModel),
|
|
867
|
+
deps,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
export function sendTelegramModelMenuMessage(
|
|
872
|
+
state: TelegramModelMenuState,
|
|
873
|
+
activeModel: MenuModel | undefined,
|
|
874
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
875
|
+
): Promise<number | undefined> {
|
|
876
|
+
return sendTelegramMenuMessage(
|
|
877
|
+
state,
|
|
878
|
+
buildTelegramModelMenuRenderPayload(state, activeModel),
|
|
879
|
+
deps,
|
|
880
|
+
);
|
|
881
|
+
}
|