@llblab/pi-telegram 0.2.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 +90 -0
- package/BACKLOG.md +5 -0
- package/CHANGELOG.md +17 -0
- package/README.md +202 -0
- package/docs/README.md +9 -0
- package/docs/architecture.md +148 -0
- package/index.ts +1968 -0
- package/lib/api.ts +222 -0
- package/lib/attachments.ts +98 -0
- package/lib/media.ts +234 -0
- package/lib/menu.ts +951 -0
- package/lib/model-switch.ts +62 -0
- package/lib/polling.ts +122 -0
- package/lib/queue.ts +534 -0
- package/lib/registration.ts +163 -0
- package/lib/rendering.ts +697 -0
- package/lib/replies.ts +313 -0
- package/lib/setup.ts +41 -0
- package/lib/status.ts +109 -0
- package/lib/turns.ts +144 -0
- package/lib/updates.ts +397 -0
- package/package.json +40 -0
- package/screenshot.png +0 -0
- package/tests/api.test.ts +89 -0
- package/tests/attachments.test.ts +132 -0
- package/tests/config.test.ts +80 -0
- package/tests/media.test.ts +77 -0
- package/tests/menu.test.ts +645 -0
- package/tests/polling.test.ts +129 -0
- package/tests/queue.test.ts +2982 -0
- package/tests/registration.test.ts +268 -0
- package/tests/rendering.test.ts +308 -0
- package/tests/replies.test.ts +362 -0
- package/tests/turns.test.ts +132 -0
- package/tests/updates.test.ts +366 -0
package/lib/menu.ts
ADDED
|
@@ -0,0 +1,951 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram menu and inline-keyboard rendering helpers
|
|
3
|
+
* Owns model resolution, menu state, and inline UI text and reply-markup generation for status, model, and thinking controls
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Model } from "@mariozechner/pi-ai";
|
|
7
|
+
|
|
8
|
+
export type ThinkingLevel =
|
|
9
|
+
| "off"
|
|
10
|
+
| "minimal"
|
|
11
|
+
| "low"
|
|
12
|
+
| "medium"
|
|
13
|
+
| "high"
|
|
14
|
+
| "xhigh";
|
|
15
|
+
export type TelegramModelScope = "all" | "scoped";
|
|
16
|
+
|
|
17
|
+
export interface ScopedTelegramModel {
|
|
18
|
+
model: Model<any>;
|
|
19
|
+
thinkingLevel?: ThinkingLevel;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TelegramModelMenuState {
|
|
23
|
+
chatId: number;
|
|
24
|
+
messageId: number;
|
|
25
|
+
page: number;
|
|
26
|
+
scope: TelegramModelScope;
|
|
27
|
+
scopedModels: ScopedTelegramModel[];
|
|
28
|
+
allModels: ScopedTelegramModel[];
|
|
29
|
+
note?: string;
|
|
30
|
+
mode: "status" | "model" | "thinking";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type TelegramReplyMarkup = {
|
|
34
|
+
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export interface TelegramMenuMessageRuntimeDeps {
|
|
38
|
+
editInteractiveMessage: (
|
|
39
|
+
chatId: number,
|
|
40
|
+
messageId: number,
|
|
41
|
+
text: string,
|
|
42
|
+
mode: "html" | "plain",
|
|
43
|
+
replyMarkup: TelegramReplyMarkup,
|
|
44
|
+
) => Promise<void>;
|
|
45
|
+
sendInteractiveMessage: (
|
|
46
|
+
chatId: number,
|
|
47
|
+
text: string,
|
|
48
|
+
mode: "html" | "plain",
|
|
49
|
+
replyMarkup: TelegramReplyMarkup,
|
|
50
|
+
) => Promise<number | undefined>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface TelegramMenuEffectPort {
|
|
54
|
+
answerCallbackQuery: (
|
|
55
|
+
callbackQueryId: string,
|
|
56
|
+
text?: string,
|
|
57
|
+
) => Promise<void>;
|
|
58
|
+
updateModelMenuMessage: () => Promise<void>;
|
|
59
|
+
updateThinkingMenuMessage: () => Promise<void>;
|
|
60
|
+
updateStatusMessage: () => Promise<void>;
|
|
61
|
+
setModel: (model: Model<any>) => Promise<boolean>;
|
|
62
|
+
setCurrentModel: (model: Model<any>) => void;
|
|
63
|
+
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
64
|
+
getCurrentThinkingLevel: () => ThinkingLevel;
|
|
65
|
+
stagePendingModelSwitch: (selection: ScopedTelegramModel) => void;
|
|
66
|
+
restartInterruptedTelegramTurn: (
|
|
67
|
+
selection: ScopedTelegramModel,
|
|
68
|
+
) => Promise<boolean> | boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type TelegramStatusMenuCallbackDeps = Pick<
|
|
72
|
+
TelegramMenuEffectPort,
|
|
73
|
+
"updateModelMenuMessage" | "updateThinkingMenuMessage" | "answerCallbackQuery"
|
|
74
|
+
>;
|
|
75
|
+
|
|
76
|
+
export type TelegramThinkingMenuCallbackDeps = Pick<
|
|
77
|
+
TelegramMenuEffectPort,
|
|
78
|
+
"setThinkingLevel" | "getCurrentThinkingLevel" | "updateStatusMessage" | "answerCallbackQuery"
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
export type TelegramModelMenuCallbackDeps = Pick<
|
|
82
|
+
TelegramMenuEffectPort,
|
|
83
|
+
| "updateModelMenuMessage"
|
|
84
|
+
| "updateStatusMessage"
|
|
85
|
+
| "answerCallbackQuery"
|
|
86
|
+
| "setModel"
|
|
87
|
+
| "setCurrentModel"
|
|
88
|
+
| "setThinkingLevel"
|
|
89
|
+
| "stagePendingModelSwitch"
|
|
90
|
+
| "restartInterruptedTelegramTurn"
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
export interface TelegramMenuCallbackEntryDeps {
|
|
94
|
+
handleStatusAction: () => Promise<boolean>;
|
|
95
|
+
handleThinkingAction: () => Promise<boolean>;
|
|
96
|
+
handleModelAction: () => Promise<boolean>;
|
|
97
|
+
answerCallbackQuery: (
|
|
98
|
+
callbackQueryId: string,
|
|
99
|
+
text?: string,
|
|
100
|
+
) => Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const THINKING_LEVELS: readonly ThinkingLevel[] = [
|
|
104
|
+
"off",
|
|
105
|
+
"minimal",
|
|
106
|
+
"low",
|
|
107
|
+
"medium",
|
|
108
|
+
"high",
|
|
109
|
+
"xhigh",
|
|
110
|
+
];
|
|
111
|
+
export const TELEGRAM_MODEL_PAGE_SIZE = 6;
|
|
112
|
+
export const MODEL_MENU_TITLE = "<b>Choose a model:</b>";
|
|
113
|
+
|
|
114
|
+
export interface BuildTelegramModelMenuStateParams {
|
|
115
|
+
chatId: number;
|
|
116
|
+
activeModel: Model<any> | undefined;
|
|
117
|
+
availableModels: Model<any>[];
|
|
118
|
+
configuredScopedModelPatterns: string[];
|
|
119
|
+
cliScopedModelPatterns?: string[];
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export type TelegramMenuCallbackAction =
|
|
123
|
+
| { kind: "ignore" }
|
|
124
|
+
| { kind: "status"; action: "model" | "thinking" }
|
|
125
|
+
| { kind: "thinking:set"; level: string }
|
|
126
|
+
| {
|
|
127
|
+
kind: "model";
|
|
128
|
+
action: "noop" | "scope" | "page" | "pick";
|
|
129
|
+
value?: string;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export type TelegramMenuMutationResult = "invalid" | "unchanged" | "changed";
|
|
133
|
+
export type TelegramMenuSelectionResult =
|
|
134
|
+
| { kind: "invalid" }
|
|
135
|
+
| { kind: "missing" }
|
|
136
|
+
| { kind: "selected"; selection: ScopedTelegramModel };
|
|
137
|
+
|
|
138
|
+
export interface TelegramModelMenuPage {
|
|
139
|
+
page: number;
|
|
140
|
+
pageCount: number;
|
|
141
|
+
start: number;
|
|
142
|
+
items: ScopedTelegramModel[];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface TelegramMenuRenderPayload {
|
|
146
|
+
nextMode: TelegramModelMenuState["mode"];
|
|
147
|
+
text: string;
|
|
148
|
+
mode: "html" | "plain";
|
|
149
|
+
replyMarkup: TelegramReplyMarkup;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type TelegramModelCallbackPlan =
|
|
153
|
+
| { kind: "ignore" }
|
|
154
|
+
| { kind: "answer"; text?: string }
|
|
155
|
+
| { kind: "update-menu"; text?: string }
|
|
156
|
+
| {
|
|
157
|
+
kind: "refresh-status";
|
|
158
|
+
selection: ScopedTelegramModel;
|
|
159
|
+
callbackText: string;
|
|
160
|
+
shouldApplyThinkingLevel: boolean;
|
|
161
|
+
}
|
|
162
|
+
| {
|
|
163
|
+
kind: "switch-model";
|
|
164
|
+
selection: ScopedTelegramModel;
|
|
165
|
+
mode: "idle" | "restart-now" | "restart-after-tool";
|
|
166
|
+
callbackText: string;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
export interface BuildTelegramModelCallbackPlanParams {
|
|
170
|
+
data: string | undefined;
|
|
171
|
+
state: TelegramModelMenuState;
|
|
172
|
+
activeModel: Model<any> | undefined;
|
|
173
|
+
currentThinkingLevel: ThinkingLevel;
|
|
174
|
+
isIdle: boolean;
|
|
175
|
+
canRestartBusyRun: boolean;
|
|
176
|
+
hasActiveToolExecutions: boolean;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function modelsMatch(
|
|
180
|
+
a: Pick<Model<any>, "provider" | "id"> | undefined,
|
|
181
|
+
b: Pick<Model<any>, "provider" | "id"> | undefined,
|
|
182
|
+
): boolean {
|
|
183
|
+
return !!a && !!b && a.provider === b.provider && a.id === b.id;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function getCanonicalModelId(
|
|
187
|
+
model: Pick<Model<any>, "provider" | "id">,
|
|
188
|
+
): string {
|
|
189
|
+
return `${model.provider}/${model.id}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function isThinkingLevel(value: string): value is ThinkingLevel {
|
|
193
|
+
return THINKING_LEVELS.includes(value as ThinkingLevel);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function escapeRegex(text: string): string {
|
|
197
|
+
return text.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function globMatches(text: string, pattern: string): boolean {
|
|
201
|
+
let regex = "^";
|
|
202
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
203
|
+
const char = pattern[i];
|
|
204
|
+
if (char === "*") {
|
|
205
|
+
regex += ".*";
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (char === "?") {
|
|
209
|
+
regex += ".";
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (char === "[") {
|
|
213
|
+
const end = pattern.indexOf("]", i + 1);
|
|
214
|
+
if (end !== -1) {
|
|
215
|
+
const content = pattern.slice(i + 1, end);
|
|
216
|
+
regex += content.startsWith("!")
|
|
217
|
+
? `[^${content.slice(1)}]`
|
|
218
|
+
: `[${content}]`;
|
|
219
|
+
i = end;
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
regex += escapeRegex(char);
|
|
224
|
+
}
|
|
225
|
+
regex += "$";
|
|
226
|
+
return new RegExp(regex, "i").test(text);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isAliasModelId(id: string): boolean {
|
|
230
|
+
if (id.endsWith("-latest")) return true;
|
|
231
|
+
return !/-\d{8}$/.test(id);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function findExactModelReferenceMatch(
|
|
235
|
+
modelReference: string,
|
|
236
|
+
availableModels: Model<any>[],
|
|
237
|
+
): Model<any> | undefined {
|
|
238
|
+
const trimmedReference = modelReference.trim();
|
|
239
|
+
if (!trimmedReference) return undefined;
|
|
240
|
+
const normalizedReference = trimmedReference.toLowerCase();
|
|
241
|
+
const canonicalMatches = availableModels.filter(
|
|
242
|
+
(model) => getCanonicalModelId(model).toLowerCase() === normalizedReference,
|
|
243
|
+
);
|
|
244
|
+
if (canonicalMatches.length === 1) return canonicalMatches[0];
|
|
245
|
+
if (canonicalMatches.length > 1) return undefined;
|
|
246
|
+
const slashIndex = trimmedReference.indexOf("/");
|
|
247
|
+
if (slashIndex !== -1) {
|
|
248
|
+
const provider = trimmedReference.substring(0, slashIndex).trim();
|
|
249
|
+
const modelId = trimmedReference.substring(slashIndex + 1).trim();
|
|
250
|
+
if (provider && modelId) {
|
|
251
|
+
const providerMatches = availableModels.filter(
|
|
252
|
+
(model) =>
|
|
253
|
+
model.provider.toLowerCase() === provider.toLowerCase() &&
|
|
254
|
+
model.id.toLowerCase() === modelId.toLowerCase(),
|
|
255
|
+
);
|
|
256
|
+
if (providerMatches.length === 1) return providerMatches[0];
|
|
257
|
+
if (providerMatches.length > 1) return undefined;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const idMatches = availableModels.filter(
|
|
261
|
+
(model) => model.id.toLowerCase() === normalizedReference,
|
|
262
|
+
);
|
|
263
|
+
return idMatches.length === 1 ? idMatches[0] : undefined;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function tryMatchScopedModel(
|
|
267
|
+
modelPattern: string,
|
|
268
|
+
availableModels: Model<any>[],
|
|
269
|
+
): Model<any> | undefined {
|
|
270
|
+
const exactMatch = findExactModelReferenceMatch(
|
|
271
|
+
modelPattern,
|
|
272
|
+
availableModels,
|
|
273
|
+
);
|
|
274
|
+
if (exactMatch) return exactMatch;
|
|
275
|
+
const matches = availableModels.filter(
|
|
276
|
+
(model) =>
|
|
277
|
+
model.id.toLowerCase().includes(modelPattern.toLowerCase()) ||
|
|
278
|
+
model.name?.toLowerCase().includes(modelPattern.toLowerCase()),
|
|
279
|
+
);
|
|
280
|
+
if (matches.length === 0) return undefined;
|
|
281
|
+
const aliases = matches.filter((model) => isAliasModelId(model.id));
|
|
282
|
+
const datedVersions = matches.filter((model) => !isAliasModelId(model.id));
|
|
283
|
+
if (aliases.length > 0) {
|
|
284
|
+
aliases.sort((a, b) => b.id.localeCompare(a.id));
|
|
285
|
+
return aliases[0];
|
|
286
|
+
}
|
|
287
|
+
datedVersions.sort((a, b) => b.id.localeCompare(a.id));
|
|
288
|
+
return datedVersions[0];
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function parseScopedModelPattern(
|
|
292
|
+
pattern: string,
|
|
293
|
+
availableModels: Model<any>[],
|
|
294
|
+
): { model: Model<any> | undefined; thinkingLevel?: ThinkingLevel } {
|
|
295
|
+
const exactMatch = tryMatchScopedModel(pattern, availableModels);
|
|
296
|
+
if (exactMatch) {
|
|
297
|
+
return { model: exactMatch, thinkingLevel: undefined };
|
|
298
|
+
}
|
|
299
|
+
const lastColonIndex = pattern.lastIndexOf(":");
|
|
300
|
+
if (lastColonIndex === -1) {
|
|
301
|
+
return { model: undefined, thinkingLevel: undefined };
|
|
302
|
+
}
|
|
303
|
+
const prefix = pattern.substring(0, lastColonIndex);
|
|
304
|
+
const suffix = pattern.substring(lastColonIndex + 1);
|
|
305
|
+
if (isThinkingLevel(suffix)) {
|
|
306
|
+
const result = parseScopedModelPattern(prefix, availableModels);
|
|
307
|
+
if (result.model) {
|
|
308
|
+
return { model: result.model, thinkingLevel: suffix };
|
|
309
|
+
}
|
|
310
|
+
return result;
|
|
311
|
+
}
|
|
312
|
+
return parseScopedModelPattern(prefix, availableModels);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
export function resolveScopedModelPatterns(
|
|
316
|
+
patterns: string[],
|
|
317
|
+
availableModels: Model<any>[],
|
|
318
|
+
): ScopedTelegramModel[] {
|
|
319
|
+
const resolved: ScopedTelegramModel[] = [];
|
|
320
|
+
const seen = new Set<string>();
|
|
321
|
+
for (const pattern of patterns) {
|
|
322
|
+
if (
|
|
323
|
+
pattern.includes("*") ||
|
|
324
|
+
pattern.includes("?") ||
|
|
325
|
+
pattern.includes("[")
|
|
326
|
+
) {
|
|
327
|
+
const colonIndex = pattern.lastIndexOf(":");
|
|
328
|
+
let globPattern = pattern;
|
|
329
|
+
let thinkingLevel: ThinkingLevel | undefined;
|
|
330
|
+
if (colonIndex !== -1) {
|
|
331
|
+
const suffix = pattern.substring(colonIndex + 1);
|
|
332
|
+
if (isThinkingLevel(suffix)) {
|
|
333
|
+
thinkingLevel = suffix;
|
|
334
|
+
globPattern = pattern.substring(0, colonIndex);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const matches = availableModels.filter(
|
|
338
|
+
(model) =>
|
|
339
|
+
globMatches(getCanonicalModelId(model), globPattern) ||
|
|
340
|
+
globMatches(model.id, globPattern),
|
|
341
|
+
);
|
|
342
|
+
for (const model of matches) {
|
|
343
|
+
const key = getCanonicalModelId(model);
|
|
344
|
+
if (seen.has(key)) continue;
|
|
345
|
+
seen.add(key);
|
|
346
|
+
resolved.push({ model, thinkingLevel });
|
|
347
|
+
}
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const matched = parseScopedModelPattern(pattern, availableModels);
|
|
351
|
+
if (!matched.model) continue;
|
|
352
|
+
const key = getCanonicalModelId(matched.model);
|
|
353
|
+
if (seen.has(key)) continue;
|
|
354
|
+
seen.add(key);
|
|
355
|
+
resolved.push({
|
|
356
|
+
model: matched.model,
|
|
357
|
+
thinkingLevel: matched.thinkingLevel,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
return resolved;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function sortScopedModels(
|
|
364
|
+
models: ScopedTelegramModel[],
|
|
365
|
+
currentModel: Model<any> | undefined,
|
|
366
|
+
): ScopedTelegramModel[] {
|
|
367
|
+
const sorted = [...models];
|
|
368
|
+
sorted.sort((a, b) => {
|
|
369
|
+
const aIsCurrent = modelsMatch(a.model, currentModel);
|
|
370
|
+
const bIsCurrent = modelsMatch(b.model, currentModel);
|
|
371
|
+
if (aIsCurrent && !bIsCurrent) return -1;
|
|
372
|
+
if (!aIsCurrent && bIsCurrent) return 1;
|
|
373
|
+
const providerCompare = a.model.provider.localeCompare(b.model.provider);
|
|
374
|
+
if (providerCompare !== 0) return providerCompare;
|
|
375
|
+
return a.model.id.localeCompare(b.model.id);
|
|
376
|
+
});
|
|
377
|
+
return sorted;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
|
|
381
|
+
return label.length <= maxLength
|
|
382
|
+
? label
|
|
383
|
+
: `${label.slice(0, maxLength - 1)}…`;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function formatScopedModelButtonText(
|
|
387
|
+
entry: ScopedTelegramModel,
|
|
388
|
+
currentModel: Model<any> | undefined,
|
|
389
|
+
): string {
|
|
390
|
+
let label = `${modelsMatch(entry.model, currentModel) ? "✅ " : ""}${entry.model.id} [${entry.model.provider}]`;
|
|
391
|
+
if (entry.thinkingLevel) {
|
|
392
|
+
label += ` · ${entry.thinkingLevel}`;
|
|
393
|
+
}
|
|
394
|
+
return truncateTelegramButtonLabel(label);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function formatStatusButtonLabel(label: string, value: string): string {
|
|
398
|
+
return truncateTelegramButtonLabel(`${label}: ${value}`, 64);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
export function getModelMenuItems(
|
|
402
|
+
state: TelegramModelMenuState,
|
|
403
|
+
): ScopedTelegramModel[] {
|
|
404
|
+
return state.scope === "scoped" && state.scopedModels.length > 0
|
|
405
|
+
? state.scopedModels
|
|
406
|
+
: state.allModels;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
export function buildTelegramModelMenuState(
|
|
410
|
+
params: BuildTelegramModelMenuStateParams,
|
|
411
|
+
): TelegramModelMenuState {
|
|
412
|
+
const allModels = sortScopedModels(
|
|
413
|
+
params.availableModels.map((model) => ({ model })),
|
|
414
|
+
params.activeModel,
|
|
415
|
+
);
|
|
416
|
+
const scopedModels =
|
|
417
|
+
params.configuredScopedModelPatterns.length > 0
|
|
418
|
+
? sortScopedModels(
|
|
419
|
+
resolveScopedModelPatterns(
|
|
420
|
+
params.configuredScopedModelPatterns,
|
|
421
|
+
params.availableModels,
|
|
422
|
+
),
|
|
423
|
+
params.activeModel,
|
|
424
|
+
)
|
|
425
|
+
: [];
|
|
426
|
+
let note: string | undefined;
|
|
427
|
+
if (
|
|
428
|
+
params.configuredScopedModelPatterns.length > 0 &&
|
|
429
|
+
scopedModels.length === 0
|
|
430
|
+
) {
|
|
431
|
+
note = params.cliScopedModelPatterns
|
|
432
|
+
? "No CLI scoped models matched the current auth configuration. Showing all available models."
|
|
433
|
+
: "No scoped models matched the current auth configuration. Showing all available models.";
|
|
434
|
+
}
|
|
435
|
+
return {
|
|
436
|
+
chatId: params.chatId,
|
|
437
|
+
messageId: 0,
|
|
438
|
+
page: 0,
|
|
439
|
+
scope: scopedModels.length > 0 ? "scoped" : "all",
|
|
440
|
+
scopedModels,
|
|
441
|
+
allModels,
|
|
442
|
+
note,
|
|
443
|
+
mode: "status",
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function parseTelegramMenuCallbackAction(
|
|
448
|
+
data: string | undefined,
|
|
449
|
+
): TelegramMenuCallbackAction {
|
|
450
|
+
if (data === "status:model") return { kind: "status", action: "model" };
|
|
451
|
+
if (data === "status:thinking") {
|
|
452
|
+
return { kind: "status", action: "thinking" };
|
|
453
|
+
}
|
|
454
|
+
if (data?.startsWith("thinking:set:")) {
|
|
455
|
+
return {
|
|
456
|
+
kind: "thinking:set",
|
|
457
|
+
level: data.slice("thinking:set:".length),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (data?.startsWith("model:")) {
|
|
461
|
+
const [, action, value] = data.split(":");
|
|
462
|
+
if (
|
|
463
|
+
action === "noop" ||
|
|
464
|
+
action === "scope" ||
|
|
465
|
+
action === "page" ||
|
|
466
|
+
action === "pick"
|
|
467
|
+
) {
|
|
468
|
+
return { kind: "model", action, value };
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
return { kind: "ignore" };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function applyTelegramModelScopeSelection(
|
|
475
|
+
state: TelegramModelMenuState,
|
|
476
|
+
value: string | undefined,
|
|
477
|
+
): TelegramMenuMutationResult {
|
|
478
|
+
if (value !== "all" && value !== "scoped") return "invalid";
|
|
479
|
+
if (value === state.scope) return "unchanged";
|
|
480
|
+
state.scope = value;
|
|
481
|
+
state.page = 0;
|
|
482
|
+
return "changed";
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function applyTelegramModelPageSelection(
|
|
486
|
+
state: TelegramModelMenuState,
|
|
487
|
+
value: string | undefined,
|
|
488
|
+
): TelegramMenuMutationResult {
|
|
489
|
+
const page = Number(value);
|
|
490
|
+
if (!Number.isFinite(page)) return "invalid";
|
|
491
|
+
if (page === state.page) return "unchanged";
|
|
492
|
+
state.page = page;
|
|
493
|
+
return "changed";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
export function getTelegramModelSelection(
|
|
497
|
+
state: TelegramModelMenuState,
|
|
498
|
+
value: string | undefined,
|
|
499
|
+
): TelegramMenuSelectionResult {
|
|
500
|
+
const index = Number(value);
|
|
501
|
+
if (!Number.isFinite(index)) return { kind: "invalid" };
|
|
502
|
+
const selection = getModelMenuItems(state)[index];
|
|
503
|
+
if (!selection) return { kind: "missing" };
|
|
504
|
+
return { kind: "selected", selection };
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export function buildTelegramModelCallbackPlan(
|
|
508
|
+
params: BuildTelegramModelCallbackPlanParams,
|
|
509
|
+
): TelegramModelCallbackPlan {
|
|
510
|
+
const action = parseTelegramMenuCallbackAction(params.data);
|
|
511
|
+
if (action.kind !== "model") return { kind: "ignore" };
|
|
512
|
+
if (action.action === "noop") return { kind: "answer" };
|
|
513
|
+
if (action.action === "scope") {
|
|
514
|
+
const result = applyTelegramModelScopeSelection(params.state, action.value);
|
|
515
|
+
if (result === "invalid") {
|
|
516
|
+
return { kind: "answer", text: "Unknown model scope." };
|
|
517
|
+
}
|
|
518
|
+
if (result === "unchanged") {
|
|
519
|
+
return { kind: "answer" };
|
|
520
|
+
}
|
|
521
|
+
return {
|
|
522
|
+
kind: "update-menu",
|
|
523
|
+
text: params.state.scope === "scoped" ? "Scoped models" : "All models",
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
if (action.action === "page") {
|
|
527
|
+
const result = applyTelegramModelPageSelection(params.state, action.value);
|
|
528
|
+
if (result === "invalid") {
|
|
529
|
+
return { kind: "answer", text: "Invalid page." };
|
|
530
|
+
}
|
|
531
|
+
if (result === "unchanged") {
|
|
532
|
+
return { kind: "answer" };
|
|
533
|
+
}
|
|
534
|
+
return { kind: "update-menu" };
|
|
535
|
+
}
|
|
536
|
+
if (action.action !== "pick") {
|
|
537
|
+
return { kind: "answer" };
|
|
538
|
+
}
|
|
539
|
+
const selectionResult = getTelegramModelSelection(params.state, action.value);
|
|
540
|
+
if (selectionResult.kind === "invalid") {
|
|
541
|
+
return { kind: "answer", text: "Invalid model selection." };
|
|
542
|
+
}
|
|
543
|
+
if (selectionResult.kind === "missing") {
|
|
544
|
+
return { kind: "answer", text: "Selected model is no longer available." };
|
|
545
|
+
}
|
|
546
|
+
const selection = selectionResult.selection;
|
|
547
|
+
if (modelsMatch(selection.model, params.activeModel)) {
|
|
548
|
+
return {
|
|
549
|
+
kind: "refresh-status",
|
|
550
|
+
selection,
|
|
551
|
+
callbackText: `Model: ${selection.model.id}`,
|
|
552
|
+
shouldApplyThinkingLevel:
|
|
553
|
+
!!selection.thinkingLevel &&
|
|
554
|
+
selection.thinkingLevel !== params.currentThinkingLevel,
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (!params.isIdle) {
|
|
558
|
+
if (!params.canRestartBusyRun) {
|
|
559
|
+
return { kind: "answer", text: "Pi is busy. Send /stop first." };
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
kind: "switch-model",
|
|
563
|
+
selection,
|
|
564
|
+
mode: params.hasActiveToolExecutions
|
|
565
|
+
? "restart-after-tool"
|
|
566
|
+
: "restart-now",
|
|
567
|
+
callbackText: params.hasActiveToolExecutions
|
|
568
|
+
? `Switched to ${selection.model.id}. Restarting after the current tool finishes…`
|
|
569
|
+
: `Switching to ${selection.model.id} and continuing…`,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
kind: "switch-model",
|
|
574
|
+
selection,
|
|
575
|
+
mode: "idle",
|
|
576
|
+
callbackText: `Switched to ${selection.model.id}`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export async function handleTelegramMenuCallbackEntry(
|
|
581
|
+
callbackQueryId: string,
|
|
582
|
+
data: string | undefined,
|
|
583
|
+
state: TelegramModelMenuState | undefined,
|
|
584
|
+
deps: TelegramMenuCallbackEntryDeps,
|
|
585
|
+
): Promise<void> {
|
|
586
|
+
if (!data) {
|
|
587
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (!state) {
|
|
591
|
+
await deps.answerCallbackQuery(callbackQueryId, "Interactive message expired.");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const handled =
|
|
595
|
+
(await deps.handleStatusAction()) ||
|
|
596
|
+
(await deps.handleThinkingAction()) ||
|
|
597
|
+
(await deps.handleModelAction());
|
|
598
|
+
if (!handled) {
|
|
599
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
export async function handleTelegramModelMenuCallbackAction(
|
|
604
|
+
callbackQueryId: string,
|
|
605
|
+
params: BuildTelegramModelCallbackPlanParams,
|
|
606
|
+
deps: TelegramModelMenuCallbackDeps,
|
|
607
|
+
): Promise<boolean> {
|
|
608
|
+
const plan = buildTelegramModelCallbackPlan(params);
|
|
609
|
+
if (plan.kind === "ignore") return false;
|
|
610
|
+
if (plan.kind === "answer") {
|
|
611
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
if (plan.kind === "update-menu") {
|
|
615
|
+
await deps.updateModelMenuMessage();
|
|
616
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
617
|
+
return true;
|
|
618
|
+
}
|
|
619
|
+
if (plan.kind === "refresh-status") {
|
|
620
|
+
if (plan.shouldApplyThinkingLevel && plan.selection.thinkingLevel) {
|
|
621
|
+
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
622
|
+
}
|
|
623
|
+
await deps.updateStatusMessage();
|
|
624
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
625
|
+
return true;
|
|
626
|
+
}
|
|
627
|
+
const changed = await deps.setModel(plan.selection.model);
|
|
628
|
+
if (changed === false) {
|
|
629
|
+
await deps.answerCallbackQuery(callbackQueryId, "Model is not available.");
|
|
630
|
+
return true;
|
|
631
|
+
}
|
|
632
|
+
deps.setCurrentModel(plan.selection.model);
|
|
633
|
+
if (plan.selection.thinkingLevel) {
|
|
634
|
+
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
635
|
+
}
|
|
636
|
+
await deps.updateStatusMessage();
|
|
637
|
+
if (plan.mode === "restart-after-tool") {
|
|
638
|
+
deps.stagePendingModelSwitch(plan.selection);
|
|
639
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
if (plan.mode === "restart-now") {
|
|
643
|
+
const restarted = await deps.restartInterruptedTelegramTurn(plan.selection);
|
|
644
|
+
if (!restarted) {
|
|
645
|
+
await deps.answerCallbackQuery(
|
|
646
|
+
callbackQueryId,
|
|
647
|
+
"Pi is busy. Send /stop first.",
|
|
648
|
+
);
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export async function handleTelegramStatusMenuCallbackAction(
|
|
657
|
+
callbackQueryId: string,
|
|
658
|
+
data: string | undefined,
|
|
659
|
+
activeModel: Model<any> | undefined,
|
|
660
|
+
deps: TelegramStatusMenuCallbackDeps,
|
|
661
|
+
): Promise<boolean> {
|
|
662
|
+
const action = parseTelegramMenuCallbackAction(data);
|
|
663
|
+
if (action.kind === "status" && action.action === "model") {
|
|
664
|
+
await deps.updateModelMenuMessage();
|
|
665
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
if (!(action.kind === "status" && action.action === "thinking")) {
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (!activeModel?.reasoning) {
|
|
672
|
+
await deps.answerCallbackQuery(
|
|
673
|
+
callbackQueryId,
|
|
674
|
+
"This model has no reasoning controls.",
|
|
675
|
+
);
|
|
676
|
+
return true;
|
|
677
|
+
}
|
|
678
|
+
await deps.updateThinkingMenuMessage();
|
|
679
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
export async function handleTelegramThinkingMenuCallbackAction(
|
|
684
|
+
callbackQueryId: string,
|
|
685
|
+
data: string | undefined,
|
|
686
|
+
activeModel: Model<any> | undefined,
|
|
687
|
+
deps: TelegramThinkingMenuCallbackDeps,
|
|
688
|
+
): Promise<boolean> {
|
|
689
|
+
const action = parseTelegramMenuCallbackAction(data);
|
|
690
|
+
if (action.kind !== "thinking:set") return false;
|
|
691
|
+
if (!isThinkingLevel(action.level)) {
|
|
692
|
+
await deps.answerCallbackQuery(callbackQueryId, "Invalid thinking level.");
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
if (!activeModel?.reasoning) {
|
|
696
|
+
await deps.answerCallbackQuery(
|
|
697
|
+
callbackQueryId,
|
|
698
|
+
"This model has no reasoning controls.",
|
|
699
|
+
);
|
|
700
|
+
return true;
|
|
701
|
+
}
|
|
702
|
+
deps.setThinkingLevel(action.level);
|
|
703
|
+
await deps.updateStatusMessage();
|
|
704
|
+
await deps.answerCallbackQuery(
|
|
705
|
+
callbackQueryId,
|
|
706
|
+
`Thinking: ${deps.getCurrentThinkingLevel()}`,
|
|
707
|
+
);
|
|
708
|
+
return true;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
export function buildThinkingMenuText(
|
|
712
|
+
activeModel: Model<any> | undefined,
|
|
713
|
+
currentThinkingLevel: ThinkingLevel,
|
|
714
|
+
): string {
|
|
715
|
+
const lines = ["Choose a thinking level"];
|
|
716
|
+
if (activeModel) {
|
|
717
|
+
lines.push(`Model: ${getCanonicalModelId(activeModel)}`);
|
|
718
|
+
}
|
|
719
|
+
lines.push(`Current: ${currentThinkingLevel}`);
|
|
720
|
+
return lines.join("\n");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function getTelegramModelMenuPage(
|
|
724
|
+
state: TelegramModelMenuState,
|
|
725
|
+
pageSize: number,
|
|
726
|
+
): TelegramModelMenuPage {
|
|
727
|
+
const items = getModelMenuItems(state);
|
|
728
|
+
const pageCount = Math.max(1, Math.ceil(items.length / pageSize));
|
|
729
|
+
const page = Math.max(0, Math.min(state.page, pageCount - 1));
|
|
730
|
+
const start = page * pageSize;
|
|
731
|
+
return {
|
|
732
|
+
page,
|
|
733
|
+
pageCount,
|
|
734
|
+
start,
|
|
735
|
+
items: items.slice(start, start + pageSize),
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
export function buildModelMenuReplyMarkup(
|
|
740
|
+
state: TelegramModelMenuState,
|
|
741
|
+
currentModel: Model<any> | undefined,
|
|
742
|
+
pageSize: number,
|
|
743
|
+
): TelegramReplyMarkup {
|
|
744
|
+
const menuPage = getTelegramModelMenuPage(state, pageSize);
|
|
745
|
+
const rows = menuPage.items.map((entry, index) => [
|
|
746
|
+
{
|
|
747
|
+
text: formatScopedModelButtonText(entry, currentModel),
|
|
748
|
+
callback_data: `model:pick:${menuPage.start + index}`,
|
|
749
|
+
},
|
|
750
|
+
]);
|
|
751
|
+
if (menuPage.pageCount > 1) {
|
|
752
|
+
const previousPage =
|
|
753
|
+
menuPage.page === 0 ? menuPage.pageCount - 1 : menuPage.page - 1;
|
|
754
|
+
const nextPage =
|
|
755
|
+
menuPage.page === menuPage.pageCount - 1 ? 0 : menuPage.page + 1;
|
|
756
|
+
rows.push([
|
|
757
|
+
{ text: "⬅️", callback_data: `model:page:${previousPage}` },
|
|
758
|
+
{
|
|
759
|
+
text: `${menuPage.page + 1}/${menuPage.pageCount}`,
|
|
760
|
+
callback_data: "model:noop",
|
|
761
|
+
},
|
|
762
|
+
{ text: "➡️", callback_data: `model:page:${nextPage}` },
|
|
763
|
+
]);
|
|
764
|
+
}
|
|
765
|
+
if (state.scopedModels.length > 0) {
|
|
766
|
+
rows.push([
|
|
767
|
+
{
|
|
768
|
+
text: state.scope === "scoped" ? "✅ Scoped" : "Scoped",
|
|
769
|
+
callback_data: "model:scope:scoped",
|
|
770
|
+
},
|
|
771
|
+
{
|
|
772
|
+
text: state.scope === "all" ? "✅ All" : "All",
|
|
773
|
+
callback_data: "model:scope:all",
|
|
774
|
+
},
|
|
775
|
+
]);
|
|
776
|
+
}
|
|
777
|
+
return { inline_keyboard: rows };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
export function buildThinkingMenuReplyMarkup(
|
|
781
|
+
currentThinkingLevel: ThinkingLevel,
|
|
782
|
+
): TelegramReplyMarkup {
|
|
783
|
+
return {
|
|
784
|
+
inline_keyboard: THINKING_LEVELS.map((level) => [
|
|
785
|
+
{
|
|
786
|
+
text: level === currentThinkingLevel ? `✅ ${level}` : level,
|
|
787
|
+
callback_data: `thinking:set:${level}`,
|
|
788
|
+
},
|
|
789
|
+
]),
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
export function buildStatusReplyMarkup(
|
|
794
|
+
activeModel: Model<any> | undefined,
|
|
795
|
+
currentThinkingLevel: ThinkingLevel,
|
|
796
|
+
): TelegramReplyMarkup {
|
|
797
|
+
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
|
|
798
|
+
rows.push([
|
|
799
|
+
{
|
|
800
|
+
text: formatStatusButtonLabel(
|
|
801
|
+
"Model",
|
|
802
|
+
activeModel ? getCanonicalModelId(activeModel) : "unknown",
|
|
803
|
+
),
|
|
804
|
+
callback_data: "status:model",
|
|
805
|
+
},
|
|
806
|
+
]);
|
|
807
|
+
if (activeModel?.reasoning) {
|
|
808
|
+
rows.push([
|
|
809
|
+
{
|
|
810
|
+
text: formatStatusButtonLabel("Thinking", currentThinkingLevel),
|
|
811
|
+
callback_data: "status:thinking",
|
|
812
|
+
},
|
|
813
|
+
]);
|
|
814
|
+
}
|
|
815
|
+
return { inline_keyboard: rows };
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export function buildTelegramModelMenuRenderPayload(
|
|
819
|
+
state: TelegramModelMenuState,
|
|
820
|
+
activeModel: Model<any> | undefined,
|
|
821
|
+
): TelegramMenuRenderPayload {
|
|
822
|
+
return {
|
|
823
|
+
nextMode: "model",
|
|
824
|
+
text: MODEL_MENU_TITLE,
|
|
825
|
+
mode: "html",
|
|
826
|
+
replyMarkup: buildModelMenuReplyMarkup(
|
|
827
|
+
state,
|
|
828
|
+
activeModel,
|
|
829
|
+
TELEGRAM_MODEL_PAGE_SIZE,
|
|
830
|
+
),
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export function buildTelegramThinkingMenuRenderPayload(
|
|
835
|
+
activeModel: Model<any> | undefined,
|
|
836
|
+
currentThinkingLevel: ThinkingLevel,
|
|
837
|
+
): TelegramMenuRenderPayload {
|
|
838
|
+
return {
|
|
839
|
+
nextMode: "thinking",
|
|
840
|
+
text: buildThinkingMenuText(activeModel, currentThinkingLevel),
|
|
841
|
+
mode: "plain",
|
|
842
|
+
replyMarkup: buildThinkingMenuReplyMarkup(currentThinkingLevel),
|
|
843
|
+
};
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export function buildTelegramStatusMenuRenderPayload(
|
|
847
|
+
statusText: string,
|
|
848
|
+
activeModel: Model<any> | undefined,
|
|
849
|
+
currentThinkingLevel: ThinkingLevel,
|
|
850
|
+
): TelegramMenuRenderPayload {
|
|
851
|
+
return {
|
|
852
|
+
nextMode: "status",
|
|
853
|
+
text: statusText,
|
|
854
|
+
mode: "html",
|
|
855
|
+
replyMarkup: buildStatusReplyMarkup(activeModel, currentThinkingLevel),
|
|
856
|
+
};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
export async function updateTelegramModelMenuMessage(
|
|
860
|
+
state: TelegramModelMenuState,
|
|
861
|
+
activeModel: Model<any> | undefined,
|
|
862
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
863
|
+
): Promise<void> {
|
|
864
|
+
const payload = buildTelegramModelMenuRenderPayload(state, activeModel);
|
|
865
|
+
state.mode = payload.nextMode;
|
|
866
|
+
await deps.editInteractiveMessage(
|
|
867
|
+
state.chatId,
|
|
868
|
+
state.messageId,
|
|
869
|
+
payload.text,
|
|
870
|
+
payload.mode,
|
|
871
|
+
payload.replyMarkup,
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
export async function updateTelegramThinkingMenuMessage(
|
|
876
|
+
state: TelegramModelMenuState,
|
|
877
|
+
activeModel: Model<any> | undefined,
|
|
878
|
+
currentThinkingLevel: ThinkingLevel,
|
|
879
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
880
|
+
): Promise<void> {
|
|
881
|
+
const payload = buildTelegramThinkingMenuRenderPayload(
|
|
882
|
+
activeModel,
|
|
883
|
+
currentThinkingLevel,
|
|
884
|
+
);
|
|
885
|
+
state.mode = payload.nextMode;
|
|
886
|
+
await deps.editInteractiveMessage(
|
|
887
|
+
state.chatId,
|
|
888
|
+
state.messageId,
|
|
889
|
+
payload.text,
|
|
890
|
+
payload.mode,
|
|
891
|
+
payload.replyMarkup,
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
export async function updateTelegramStatusMessage(
|
|
896
|
+
state: TelegramModelMenuState,
|
|
897
|
+
statusText: string,
|
|
898
|
+
activeModel: Model<any> | undefined,
|
|
899
|
+
currentThinkingLevel: ThinkingLevel,
|
|
900
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
901
|
+
): Promise<void> {
|
|
902
|
+
const payload = buildTelegramStatusMenuRenderPayload(
|
|
903
|
+
statusText,
|
|
904
|
+
activeModel,
|
|
905
|
+
currentThinkingLevel,
|
|
906
|
+
);
|
|
907
|
+
state.mode = payload.nextMode;
|
|
908
|
+
await deps.editInteractiveMessage(
|
|
909
|
+
state.chatId,
|
|
910
|
+
state.messageId,
|
|
911
|
+
payload.text,
|
|
912
|
+
payload.mode,
|
|
913
|
+
payload.replyMarkup,
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export async function sendTelegramStatusMessage(
|
|
918
|
+
state: TelegramModelMenuState,
|
|
919
|
+
statusText: string,
|
|
920
|
+
activeModel: Model<any> | undefined,
|
|
921
|
+
currentThinkingLevel: ThinkingLevel,
|
|
922
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
923
|
+
): Promise<number | undefined> {
|
|
924
|
+
const payload = buildTelegramStatusMenuRenderPayload(
|
|
925
|
+
statusText,
|
|
926
|
+
activeModel,
|
|
927
|
+
currentThinkingLevel,
|
|
928
|
+
);
|
|
929
|
+
state.mode = payload.nextMode;
|
|
930
|
+
return deps.sendInteractiveMessage(
|
|
931
|
+
state.chatId,
|
|
932
|
+
payload.text,
|
|
933
|
+
payload.mode,
|
|
934
|
+
payload.replyMarkup,
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
export async function sendTelegramModelMenuMessage(
|
|
939
|
+
state: TelegramModelMenuState,
|
|
940
|
+
activeModel: Model<any> | undefined,
|
|
941
|
+
deps: TelegramMenuMessageRuntimeDeps,
|
|
942
|
+
): Promise<number | undefined> {
|
|
943
|
+
const payload = buildTelegramModelMenuRenderPayload(state, activeModel);
|
|
944
|
+
state.mode = payload.nextMode;
|
|
945
|
+
return deps.sendInteractiveMessage(
|
|
946
|
+
state.chatId,
|
|
947
|
+
payload.text,
|
|
948
|
+
payload.mode,
|
|
949
|
+
payload.replyMarkup,
|
|
950
|
+
);
|
|
951
|
+
}
|