@llblab/pi-telegram 0.8.1 → 0.9.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 +1 -0
- package/CHANGELOG.md +17 -1
- package/README.md +7 -4
- package/docs/architecture.md +9 -6
- package/index.ts +45 -2
- package/lib/commands.ts +72 -0
- package/lib/config.ts +49 -0
- package/lib/inbound-handlers.ts +15 -2
- package/lib/locks.ts +16 -0
- package/lib/menu-model.ts +291 -19
- package/lib/menu-queue.ts +137 -36
- package/lib/menu-settings.ts +272 -0
- package/lib/menu-status.ts +15 -2
- package/lib/menu-thinking.ts +2 -2
- package/lib/menu.ts +45 -3
- package/lib/pi.ts +16 -0
- package/lib/preview.ts +15 -0
- package/lib/prompts.ts +24 -1
- package/lib/queue.ts +53 -12
- package/lib/routing.ts +26 -0
- package/lib/status.ts +20 -6
- package/package.json +1 -1
package/lib/menu-model.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
8
8
|
import {
|
|
9
|
+
getCanonicalModelId,
|
|
9
10
|
type MenuModel,
|
|
10
11
|
modelsMatch,
|
|
11
12
|
parseTelegramCliScopedModelPatterns,
|
|
@@ -29,7 +30,18 @@ export interface TelegramModelMenuState<TModel extends MenuModel = MenuModel> {
|
|
|
29
30
|
scopedModels: ScopedTelegramModel<TModel>[];
|
|
30
31
|
allModels: ScopedTelegramModel<TModel>[];
|
|
31
32
|
note?: string;
|
|
32
|
-
|
|
33
|
+
selectedModelIndex?: number;
|
|
34
|
+
selectedModelKey?: string;
|
|
35
|
+
scopedModelPatterns?: string[];
|
|
36
|
+
canMutateScope?: boolean;
|
|
37
|
+
mode:
|
|
38
|
+
| "status"
|
|
39
|
+
| "model"
|
|
40
|
+
| "model-pages"
|
|
41
|
+
| "model-detail"
|
|
42
|
+
| "thinking"
|
|
43
|
+
| "queue"
|
|
44
|
+
| "settings";
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
export interface StoredTelegramModelMenuState<
|
|
@@ -90,7 +102,9 @@ export interface TelegramModelMenuRuntimeOptions<
|
|
|
90
102
|
|
|
91
103
|
export interface MenuSettingsManager {
|
|
92
104
|
reload: () => Promise<void>;
|
|
105
|
+
flush?: () => Promise<void>;
|
|
93
106
|
getEnabledModels: () => string[] | undefined;
|
|
107
|
+
setEnabledModels?: (patterns: string[] | undefined) => void;
|
|
94
108
|
}
|
|
95
109
|
|
|
96
110
|
export type TelegramModelMenuStateBuilderContext<
|
|
@@ -134,6 +148,7 @@ export type TelegramModelMenuCallbackDeps<
|
|
|
134
148
|
) => Promise<void>;
|
|
135
149
|
updateModelMenuMessage: () => Promise<void>;
|
|
136
150
|
updateStatusMessage: () => Promise<void>;
|
|
151
|
+
persistScopedModelPatterns?: (patterns: string[]) => Promise<void>;
|
|
137
152
|
setModel: (model: TModel) => Promise<boolean>;
|
|
138
153
|
setCurrentModel: (model: TModel) => void;
|
|
139
154
|
setThinkingLevel: (level: ThinkingLevel) => void;
|
|
@@ -193,6 +208,7 @@ export type TelegramModelCallbackPlan<TModel extends MenuModel = MenuModel> =
|
|
|
193
208
|
| { kind: "ignore" }
|
|
194
209
|
| { kind: "answer"; text?: string }
|
|
195
210
|
| { kind: "update-menu"; text?: string }
|
|
211
|
+
| { kind: "persist-scope"; patterns: string[]; text: string }
|
|
196
212
|
| {
|
|
197
213
|
kind: "refresh-status";
|
|
198
214
|
selection: ScopedTelegramModel<TModel>;
|
|
@@ -226,6 +242,7 @@ export interface TelegramModelMenuRuntime<
|
|
|
226
242
|
messageId: number | undefined,
|
|
227
243
|
) => TelegramModelMenuState<TModel> | undefined;
|
|
228
244
|
clear: () => void;
|
|
245
|
+
clearCachedInputs: () => void;
|
|
229
246
|
buildState: <TContext extends TelegramModelMenuRuntimeContext<TModel>>(
|
|
230
247
|
options: Omit<
|
|
231
248
|
TelegramModelMenuRuntimeOptions<TContext, TModel>,
|
|
@@ -236,8 +253,9 @@ export interface TelegramModelMenuRuntime<
|
|
|
236
253
|
|
|
237
254
|
export const TELEGRAM_MODEL_PAGE_SIZE = 6;
|
|
238
255
|
const TELEGRAM_MODEL_PAGE_PICKER_ROW_SIZE = 4;
|
|
239
|
-
export const MODEL_MENU_TITLE = "<b
|
|
256
|
+
export const MODEL_MENU_TITLE = "<b>🤖 Choose a model:</b>";
|
|
240
257
|
export const MODEL_PAGE_MENU_TITLE = "<b>Choose a page:</b>";
|
|
258
|
+
export const MODEL_DETAIL_MENU_TITLE = "<b>🤖 Model:</b>";
|
|
241
259
|
|
|
242
260
|
function truncateTelegramButtonLabel(label: string, maxLength = 56): string {
|
|
243
261
|
return label.length <= maxLength
|
|
@@ -249,10 +267,21 @@ function getTelegramCliScopedModelPatterns(): string[] | undefined {
|
|
|
249
267
|
return parseTelegramCliScopedModelPatterns(process.argv.slice(2));
|
|
250
268
|
}
|
|
251
269
|
|
|
252
|
-
function parseTelegramModelMenuCallbackAction(
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
270
|
+
function parseTelegramModelMenuCallbackAction(data: string | undefined):
|
|
271
|
+
| {
|
|
272
|
+
action:
|
|
273
|
+
| "noop"
|
|
274
|
+
| "scope"
|
|
275
|
+
| "page"
|
|
276
|
+
| "pages"
|
|
277
|
+
| "open"
|
|
278
|
+
| "pick"
|
|
279
|
+
| "pick-selected"
|
|
280
|
+
| "scope-enable"
|
|
281
|
+
| "scope-disable"
|
|
282
|
+
| "scope-toggle";
|
|
283
|
+
value?: string;
|
|
284
|
+
}
|
|
256
285
|
| undefined {
|
|
257
286
|
if (!data?.startsWith("model:")) return undefined;
|
|
258
287
|
const [, action, value] = data.split(":");
|
|
@@ -261,7 +290,12 @@ function parseTelegramModelMenuCallbackAction(
|
|
|
261
290
|
action === "scope" ||
|
|
262
291
|
action === "page" ||
|
|
263
292
|
action === "pages" ||
|
|
264
|
-
action === "
|
|
293
|
+
action === "open" ||
|
|
294
|
+
action === "pick" ||
|
|
295
|
+
action === "pick-selected" ||
|
|
296
|
+
action === "scope-enable" ||
|
|
297
|
+
action === "scope-disable" ||
|
|
298
|
+
action === "scope-toggle"
|
|
265
299
|
) {
|
|
266
300
|
return { action, value };
|
|
267
301
|
}
|
|
@@ -311,7 +345,7 @@ export function formatScopedModelButtonText<
|
|
|
311
345
|
entry: ScopedTelegramModel<TModel>,
|
|
312
346
|
currentModel: TModel | undefined,
|
|
313
347
|
): string {
|
|
314
|
-
let label = `${modelsMatch(entry.model, currentModel) ? "
|
|
348
|
+
let label = `${modelsMatch(entry.model, currentModel) ? "🟢 " : ""}${entry.model.id} [${entry.model.provider}]`;
|
|
315
349
|
if (entry.thinkingLevel) {
|
|
316
350
|
label += ` · ${entry.thinkingLevel}`;
|
|
317
351
|
}
|
|
@@ -401,6 +435,9 @@ export function createTelegramModelMenuRuntime<
|
|
|
401
435
|
menus.clear();
|
|
402
436
|
cachedInputs = undefined;
|
|
403
437
|
},
|
|
438
|
+
clearCachedInputs: () => {
|
|
439
|
+
cachedInputs = undefined;
|
|
440
|
+
},
|
|
404
441
|
buildState: async (stateOptions) => {
|
|
405
442
|
const result = await buildTelegramModelMenuStateRuntime({
|
|
406
443
|
...stateOptions,
|
|
@@ -490,6 +527,9 @@ export function buildTelegramModelMenuState<
|
|
|
490
527
|
scopedModels,
|
|
491
528
|
allModels,
|
|
492
529
|
note,
|
|
530
|
+
selectedModelIndex: undefined,
|
|
531
|
+
scopedModelPatterns: params.configuredScopedModelPatterns,
|
|
532
|
+
canMutateScope: !params.cliScopedModelPatterns,
|
|
493
533
|
mode: "status",
|
|
494
534
|
};
|
|
495
535
|
}
|
|
@@ -563,6 +603,115 @@ export function getTelegramModelSelection<TModel extends MenuModel = MenuModel>(
|
|
|
563
603
|
return { kind: "selected", selection };
|
|
564
604
|
}
|
|
565
605
|
|
|
606
|
+
export function applyTelegramModelDetailSelection(
|
|
607
|
+
state: TelegramModelMenuState,
|
|
608
|
+
value: string | undefined,
|
|
609
|
+
): TelegramMenuMutationResult {
|
|
610
|
+
const index = Number(value);
|
|
611
|
+
if (!Number.isFinite(index)) return "invalid";
|
|
612
|
+
const selection = getModelMenuItems(state)[index];
|
|
613
|
+
if (!selection) return "invalid";
|
|
614
|
+
state.selectedModelIndex = index;
|
|
615
|
+
state.selectedModelKey = getCanonicalModelId(selection.model);
|
|
616
|
+
state.mode = "model-detail";
|
|
617
|
+
return "changed";
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
export function getTelegramSelectedDetailModel<
|
|
621
|
+
TModel extends MenuModel = MenuModel,
|
|
622
|
+
>(state: TelegramModelMenuState<TModel>): TelegramMenuSelectionResult<TModel> {
|
|
623
|
+
if (state.selectedModelKey) {
|
|
624
|
+
const lowerKey = state.selectedModelKey.toLowerCase();
|
|
625
|
+
const selection = state.allModels.find(
|
|
626
|
+
(entry) => getCanonicalModelId(entry.model).toLowerCase() === lowerKey,
|
|
627
|
+
);
|
|
628
|
+
if (selection) return { kind: "selected", selection };
|
|
629
|
+
}
|
|
630
|
+
return getTelegramModelSelection(state, state.selectedModelIndex?.toString());
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export function isTelegramModelScoped(
|
|
634
|
+
state: TelegramModelMenuState,
|
|
635
|
+
model: MenuModel,
|
|
636
|
+
): boolean {
|
|
637
|
+
const key = getCanonicalModelId(model);
|
|
638
|
+
return state.scopedModels.some(
|
|
639
|
+
(entry) => getCanonicalModelId(entry.model) === key,
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export function focusTelegramModelListPage(
|
|
644
|
+
state: TelegramModelMenuState,
|
|
645
|
+
model: MenuModel,
|
|
646
|
+
pageSize = TELEGRAM_MODEL_PAGE_SIZE,
|
|
647
|
+
): void {
|
|
648
|
+
const key = getCanonicalModelId(model).toLowerCase();
|
|
649
|
+
const index = getModelMenuItems(state).findIndex(
|
|
650
|
+
(entry) => getCanonicalModelId(entry.model).toLowerCase() === key,
|
|
651
|
+
);
|
|
652
|
+
state.page = index < 0 ? 0 : Math.floor(index / pageSize);
|
|
653
|
+
state.mode = "model";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function formatScopedModelPattern(entry: ScopedTelegramModel): string {
|
|
657
|
+
const key = getCanonicalModelId(entry.model);
|
|
658
|
+
return entry.thinkingLevel ? `${key}:${entry.thinkingLevel}` : key;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function setTelegramModelScope(
|
|
662
|
+
state: TelegramModelMenuState,
|
|
663
|
+
model: MenuModel,
|
|
664
|
+
enabled: boolean,
|
|
665
|
+
): { patterns: string[]; enabled: boolean } {
|
|
666
|
+
const key = getCanonicalModelId(model);
|
|
667
|
+
const lowerKey = key.toLowerCase();
|
|
668
|
+
const scopedModelPatterns = state.scopedModelPatterns ?? [];
|
|
669
|
+
const allModels = state.allModels.map((entry) => entry.model);
|
|
670
|
+
const patterns: string[] = [];
|
|
671
|
+
for (const pattern of scopedModelPatterns) {
|
|
672
|
+
const resolved = resolveScopedModelPatterns([pattern], allModels);
|
|
673
|
+
const matchesModel = resolved.some(
|
|
674
|
+
(entry) => getCanonicalModelId(entry.model).toLowerCase() === lowerKey,
|
|
675
|
+
);
|
|
676
|
+
if (enabled || !matchesModel) {
|
|
677
|
+
if (pattern.toLowerCase() !== lowerKey) patterns.push(pattern);
|
|
678
|
+
continue;
|
|
679
|
+
}
|
|
680
|
+
for (const entry of resolved) {
|
|
681
|
+
if (getCanonicalModelId(entry.model).toLowerCase() === lowerKey) continue;
|
|
682
|
+
const expandedPattern = formatScopedModelPattern(entry);
|
|
683
|
+
const duplicated = patterns.some(
|
|
684
|
+
(item) => item.toLowerCase() === expandedPattern.toLowerCase(),
|
|
685
|
+
);
|
|
686
|
+
if (!duplicated) patterns.push(expandedPattern);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (enabled) patterns.push(key);
|
|
690
|
+
state.scopedModelPatterns = patterns;
|
|
691
|
+
state.scopedModels = sortScopedModels(
|
|
692
|
+
resolveScopedModelPatterns(
|
|
693
|
+
patterns,
|
|
694
|
+
state.allModels.map((entry) => entry.model),
|
|
695
|
+
),
|
|
696
|
+
model,
|
|
697
|
+
);
|
|
698
|
+
if (state.scope === "scoped" && state.scopedModels.length === 0) {
|
|
699
|
+
state.scope = "all";
|
|
700
|
+
}
|
|
701
|
+
return { patterns, enabled };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
export function toggleTelegramModelScope(
|
|
705
|
+
state: TelegramModelMenuState,
|
|
706
|
+
model: MenuModel,
|
|
707
|
+
): { patterns: string[]; enabled: boolean } {
|
|
708
|
+
return setTelegramModelScope(
|
|
709
|
+
state,
|
|
710
|
+
model,
|
|
711
|
+
!isTelegramModelScoped(state, model),
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
|
|
566
715
|
export function buildTelegramModelCallbackPlan<
|
|
567
716
|
TModel extends MenuModel = MenuModel,
|
|
568
717
|
>(
|
|
@@ -609,10 +758,55 @@ export function buildTelegramModelCallbackPlan<
|
|
|
609
758
|
}
|
|
610
759
|
return { kind: "update-menu" };
|
|
611
760
|
}
|
|
612
|
-
if (action.action
|
|
761
|
+
if (action.action === "open") {
|
|
762
|
+
const detailResult = applyTelegramModelDetailSelection(
|
|
763
|
+
params.state,
|
|
764
|
+
action.value,
|
|
765
|
+
);
|
|
766
|
+
if (detailResult === "invalid") {
|
|
767
|
+
return { kind: "answer", text: "Selected model is no longer available." };
|
|
768
|
+
}
|
|
769
|
+
return { kind: "update-menu" };
|
|
770
|
+
}
|
|
771
|
+
if (
|
|
772
|
+
action.action === "scope-toggle" ||
|
|
773
|
+
action.action === "scope-enable" ||
|
|
774
|
+
action.action === "scope-disable"
|
|
775
|
+
) {
|
|
776
|
+
if (params.state.canMutateScope === false) {
|
|
777
|
+
return {
|
|
778
|
+
kind: "answer",
|
|
779
|
+
text: "Model scope is controlled by CLI --models.",
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
const selectionResult = getTelegramSelectedDetailModel(params.state);
|
|
783
|
+
if (selectionResult.kind !== "selected") {
|
|
784
|
+
return { kind: "answer", text: "Selected model is no longer available." };
|
|
785
|
+
}
|
|
786
|
+
const model = selectionResult.selection.model;
|
|
787
|
+
const enabled =
|
|
788
|
+
action.action === "scope-toggle"
|
|
789
|
+
? !isTelegramModelScoped(params.state, model)
|
|
790
|
+
: action.action === "scope-enable";
|
|
791
|
+
if (enabled === isTelegramModelScoped(params.state, model)) {
|
|
792
|
+
return { kind: "answer" };
|
|
793
|
+
}
|
|
794
|
+
const result = setTelegramModelScope(params.state, model, enabled);
|
|
795
|
+
return {
|
|
796
|
+
kind: "persist-scope",
|
|
797
|
+
patterns: result.patterns,
|
|
798
|
+
text: result.enabled
|
|
799
|
+
? "Added to scoped models"
|
|
800
|
+
: "Removed from scoped models",
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
if (action.action !== "pick" && action.action !== "pick-selected") {
|
|
613
804
|
return { kind: "answer" };
|
|
614
805
|
}
|
|
615
|
-
const selectionResult =
|
|
806
|
+
const selectionResult =
|
|
807
|
+
action.action === "pick-selected"
|
|
808
|
+
? getTelegramSelectedDetailModel(params.state)
|
|
809
|
+
: getTelegramModelSelection(params.state, action.value);
|
|
616
810
|
if (selectionResult.kind === "invalid") {
|
|
617
811
|
return { kind: "answer", text: "Invalid model selection." };
|
|
618
812
|
}
|
|
@@ -621,6 +815,10 @@ export function buildTelegramModelCallbackPlan<
|
|
|
621
815
|
}
|
|
622
816
|
const selection = selectionResult.selection;
|
|
623
817
|
if (modelsMatch(selection.model, params.activeModel)) {
|
|
818
|
+
if (action.action === "pick-selected") {
|
|
819
|
+
focusTelegramModelListPage(params.state, selection.model);
|
|
820
|
+
return { kind: "update-menu", text: `Model: ${selection.model.id}` };
|
|
821
|
+
}
|
|
624
822
|
return {
|
|
625
823
|
kind: "refresh-status",
|
|
626
824
|
selection,
|
|
@@ -693,11 +891,24 @@ export async function handleTelegramModelMenuCallbackAction<
|
|
|
693
891
|
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
694
892
|
return true;
|
|
695
893
|
}
|
|
894
|
+
if (plan.kind === "persist-scope") {
|
|
895
|
+
if (!deps.persistScopedModelPatterns) {
|
|
896
|
+
await deps.answerCallbackQuery(
|
|
897
|
+
callbackQueryId,
|
|
898
|
+
"Scoped model persistence is unavailable.",
|
|
899
|
+
);
|
|
900
|
+
return true;
|
|
901
|
+
}
|
|
902
|
+
await deps.persistScopedModelPatterns(plan.patterns);
|
|
903
|
+
await deps.updateModelMenuMessage();
|
|
904
|
+
await deps.answerCallbackQuery(callbackQueryId, plan.text);
|
|
905
|
+
return true;
|
|
906
|
+
}
|
|
696
907
|
if (plan.kind === "refresh-status") {
|
|
697
908
|
if (plan.shouldApplyThinkingLevel && plan.selection.thinkingLevel) {
|
|
698
909
|
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
699
910
|
}
|
|
700
|
-
await deps.
|
|
911
|
+
await deps.updateModelMenuMessage();
|
|
701
912
|
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
702
913
|
return true;
|
|
703
914
|
}
|
|
@@ -710,7 +921,7 @@ export async function handleTelegramModelMenuCallbackAction<
|
|
|
710
921
|
if (plan.selection.thinkingLevel) {
|
|
711
922
|
deps.setThinkingLevel(plan.selection.thinkingLevel);
|
|
712
923
|
}
|
|
713
|
-
await deps.
|
|
924
|
+
await deps.updateModelMenuMessage();
|
|
714
925
|
if (plan.mode === "restart-after-tool") {
|
|
715
926
|
deps.stagePendingModelSwitch(plan.selection);
|
|
716
927
|
await deps.answerCallbackQuery(callbackQueryId, plan.callbackText);
|
|
@@ -756,11 +967,11 @@ export function buildModelMenuReplyMarkup(
|
|
|
756
967
|
if (state.scopedModels.length > 0) {
|
|
757
968
|
rows.push([
|
|
758
969
|
{
|
|
759
|
-
text: state.scope === "scoped" ? "
|
|
970
|
+
text: state.scope === "scoped" ? "🟡 Scoped" : "⚫️ Scoped",
|
|
760
971
|
callback_data: "model:scope:scoped",
|
|
761
972
|
},
|
|
762
973
|
{
|
|
763
|
-
text: state.scope === "all" ? "
|
|
974
|
+
text: state.scope === "all" ? "🟡 All" : "⚫️ All",
|
|
764
975
|
callback_data: "model:scope:all",
|
|
765
976
|
},
|
|
766
977
|
]);
|
|
@@ -790,13 +1001,60 @@ export function buildModelMenuReplyMarkup(
|
|
|
790
1001
|
...menuPage.items.map((entry, index) => [
|
|
791
1002
|
{
|
|
792
1003
|
text: formatScopedModelButtonText(entry, currentModel),
|
|
793
|
-
callback_data: `model:
|
|
1004
|
+
callback_data: `model:open:${menuPage.start + index}`,
|
|
794
1005
|
},
|
|
795
1006
|
]),
|
|
796
1007
|
);
|
|
797
1008
|
return { inline_keyboard: rows };
|
|
798
1009
|
}
|
|
799
1010
|
|
|
1011
|
+
export function buildModelDetailMenuReplyMarkup(
|
|
1012
|
+
state: TelegramModelMenuState,
|
|
1013
|
+
currentModel: MenuModel | undefined,
|
|
1014
|
+
): TelegramReplyMarkup {
|
|
1015
|
+
const selection = getTelegramSelectedDetailModel(state);
|
|
1016
|
+
if (selection.kind !== "selected") {
|
|
1017
|
+
return {
|
|
1018
|
+
inline_keyboard: [
|
|
1019
|
+
[{ text: "⬆️ Back", callback_data: "model:pages:back" }],
|
|
1020
|
+
],
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
const model = selection.selection.model;
|
|
1024
|
+
const active = modelsMatch(model, currentModel);
|
|
1025
|
+
const scoped = isTelegramModelScoped(state, model);
|
|
1026
|
+
return {
|
|
1027
|
+
inline_keyboard: [
|
|
1028
|
+
[{ text: "⬆️ Back", callback_data: "model:pages:back" }],
|
|
1029
|
+
[
|
|
1030
|
+
{
|
|
1031
|
+
text: active ? "🟢 Active" : "☑️ Activate",
|
|
1032
|
+
callback_data: "model:pick-selected",
|
|
1033
|
+
},
|
|
1034
|
+
],
|
|
1035
|
+
[
|
|
1036
|
+
{
|
|
1037
|
+
text: scoped ? "🟡 Scoped" : "⚫️ Scoped",
|
|
1038
|
+
callback_data: "model:scope-enable",
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
text: scoped ? "⚫️ All" : "🟡 All",
|
|
1042
|
+
callback_data: "model:scope-disable",
|
|
1043
|
+
},
|
|
1044
|
+
],
|
|
1045
|
+
],
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export function buildModelDetailMenuText(
|
|
1050
|
+
state: TelegramModelMenuState,
|
|
1051
|
+
): string {
|
|
1052
|
+
const selection = getTelegramSelectedDetailModel(state);
|
|
1053
|
+
if (selection.kind !== "selected") return MODEL_DETAIL_MENU_TITLE;
|
|
1054
|
+
const model = selection.selection.model;
|
|
1055
|
+
return `${MODEL_DETAIL_MENU_TITLE}\n${getCanonicalModelId(model)}`;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
800
1058
|
export function buildModelPageMenuReplyMarkup(
|
|
801
1059
|
state: TelegramModelMenuState,
|
|
802
1060
|
pageSize: number,
|
|
@@ -816,10 +1074,16 @@ export function buildModelPageMenuReplyMarkup(
|
|
|
816
1074
|
menuPage.pageCount - page,
|
|
817
1075
|
),
|
|
818
1076
|
},
|
|
819
|
-
(_unused, offset) =>
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
1077
|
+
(_unused, offset) => {
|
|
1078
|
+
const pageIndex = page + offset;
|
|
1079
|
+
return {
|
|
1080
|
+
text:
|
|
1081
|
+
pageIndex === menuPage.page
|
|
1082
|
+
? `🟢 ${pageIndex + 1}`
|
|
1083
|
+
: String(pageIndex + 1),
|
|
1084
|
+
callback_data: `model:page:${pageIndex}`,
|
|
1085
|
+
};
|
|
1086
|
+
},
|
|
823
1087
|
),
|
|
824
1088
|
);
|
|
825
1089
|
}
|
|
@@ -844,6 +1108,14 @@ export function buildTelegramModelMenuRenderPayload(
|
|
|
844
1108
|
if (state.mode === "model-pages") {
|
|
845
1109
|
return buildTelegramModelPageMenuRenderPayload(state);
|
|
846
1110
|
}
|
|
1111
|
+
if (state.mode === "model-detail") {
|
|
1112
|
+
return {
|
|
1113
|
+
nextMode: "model-detail",
|
|
1114
|
+
text: buildModelDetailMenuText(state),
|
|
1115
|
+
mode: "html",
|
|
1116
|
+
replyMarkup: buildModelDetailMenuReplyMarkup(state, activeModel),
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
847
1119
|
return {
|
|
848
1120
|
nextMode: "model",
|
|
849
1121
|
text: MODEL_MENU_TITLE,
|