Files
oh-my-pi-telegram/lib/menu.ts
Evandro Fonseca Junior d1d04001d0
Release / release (push) Failing after 5s
Validate / validate (push) Failing after 5m21s
chore: fork @llblab/[email protected] as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a
drop-in extension for can1357/oh-my-pi (omp >= 16.5.2).

Changes vs upstream:
- Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram
- Bump version: 0.22.0 -> 0.22.0-evandro.1
- Update repository/homepage/bugs URLs to evandrodevbr
- Add omp field in package.json (displayName + compatibleWith)
- README: prepend 'Oh My Pi (omp) Compatibility' section with install
  commands, verified-components table, upstream lineage, and rationale
- CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased
- docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram
- tests/public-api.test.ts: rename package import paths

Gates re-run on the fork:
- npm run typecheck  -> tsc --noEmit (success)
- npm test           -> 1229 pass / 0 fail / 1 platform-only skip
- npm run audit      -> found 0 vulnerabilities
- npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz

Verified:
- omp install --link ./pi-telegram -> Linked OK
- omp plugin list -> @evandrodevbr/[email protected].0-evandro.1

Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
2026-07-15 14:18:16 -03:00

885 lines
29 KiB
TypeScript

/**
* Telegram menu and inline-keyboard rendering helpers
* Zones: telegram ui, controls, status menu
* Owns app-menu/status state, inline UI text, and callback composition while model/thinking/queue menu details live in dedicated domains
*/
import {
createTelegramModelMenuStateBuilder,
handleTelegramModelMenuCallbackAction,
openTelegramModelMenu,
sendTelegramModelMenuMessage,
updateTelegramModelMenuMessage,
type TelegramMenuMessageRuntimeDeps,
type TelegramModelMenuState,
type TelegramModelMenuStateBuilderContext,
type TelegramModelMenuStateBuilderDeps,
type TelegramReplyMarkup,
} from "./menu-model.ts";
import {
handleTelegramStatusMenuCallbackAction,
openTelegramStatusMenu,
sendTelegramStatusMessage,
updateTelegramStatusMessage,
} from "./menu-status.ts";
import {
handleTelegramThinkingMenuCallbackAction,
openTelegramThinkingMenu,
updateTelegramThinkingMenuMessage,
} from "./menu-thinking.ts";
import {
type MenuModel,
type ScopedTelegramModel,
type ThinkingLevel,
} from "./model.ts";
import {
handleTelegramSectionCallback,
handleTelegramSectionOpen,
handleTelegramSectionSettingsOpen,
parseTelegramSectionCallback,
type TelegramSectionRegistry,
} from "./sections.ts";
export {
applyTelegramModelPageSelection,
applyTelegramModelScopeSelection,
buildModelMenuReplyMarkup,
buildModelPageMenuReplyMarkup,
buildTelegramModelCallbackPlan,
buildTelegramModelMenuRenderPayload,
buildTelegramModelMenuState,
buildTelegramModelMenuStateRuntime,
buildTelegramModelPageMenuRenderPayload,
createTelegramModelMenuRuntime,
createTelegramModelMenuStateBuilder,
formatScopedModelButtonText,
getModelMenuItems,
getStoredTelegramModelMenuState,
getTelegramModelMenuPage,
getTelegramModelSelection,
handleTelegramModelMenuCallbackAction,
MODEL_MENU_TITLE,
MODEL_PAGE_MENU_TITLE,
openTelegramModelMenu,
pruneStoredTelegramModelMenus,
resolveCachedTelegramModelMenuInputs,
sendTelegramModelMenuMessage,
storeTelegramModelMenuState,
TELEGRAM_MODEL_PAGE_SIZE,
updateTelegramModelMenuMessage,
} from "./menu-model.ts";
export type {
BuildTelegramModelCallbackPlanParams,
BuildTelegramModelMenuStateParams,
CachedTelegramModelMenuInputs,
MenuSettingsManager,
StoredTelegramModelMenuState,
TelegramMenuMessageRuntimeDeps,
TelegramMenuMutationResult,
TelegramMenuRenderPayload,
TelegramMenuSelectionResult,
TelegramModelCallbackPlan,
TelegramModelMenuCallbackDeps,
TelegramModelMenuInputCacheDeps,
TelegramModelMenuOpenDeps,
TelegramModelMenuPage,
TelegramModelMenuRuntime,
TelegramModelMenuRuntimeContext,
TelegramModelMenuRuntimeOptions,
TelegramModelMenuState,
TelegramModelMenuStateBuilderContext,
TelegramModelMenuStateBuilderDeps,
TelegramModelMenuStoreOptions,
TelegramModelScope,
TelegramReplyMarkup,
} from "./menu-model.ts";
export {
buildStatusReplyMarkup,
buildTelegramStatusMenuRenderPayload,
handleTelegramStatusMenuCallbackAction,
openTelegramStatusMenu,
sendTelegramStatusMessage,
updateTelegramStatusMessage,
} from "./menu-status.ts";
export type {
TelegramStatusMenuCallbackDeps,
TelegramStatusMenuOpenDeps,
} from "./menu-status.ts";
export {
buildTelegramThinkingMenuRenderPayload,
buildThinkingMenuReplyMarkup,
buildThinkingMenuText,
handleTelegramThinkingMenuCallbackAction,
openTelegramThinkingMenu,
updateTelegramThinkingMenuMessage,
} from "./menu-thinking.ts";
export type {
TelegramThinkingMenuCallbackDeps,
TelegramThinkingMenuOpenDeps,
} from "./menu-thinking.ts";
export interface TelegramMenuEffectPort<TModel extends MenuModel = MenuModel> {
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
updateModelMenuMessage: () => Promise<void>;
updateThinkingMenuMessage: () => Promise<void>;
updateStatusMessage: () => Promise<void>;
persistScopedModelPatterns?: (patterns: string[]) => Promise<void>;
setModel: (model: TModel) => Promise<boolean>;
setCurrentModel: (model: TModel) => void;
setThinkingLevel: (level: ThinkingLevel) => void;
getCurrentThinkingLevel: () => ThinkingLevel;
stagePendingModelSwitch: (selection: ScopedTelegramModel<TModel>) => void;
restartInterruptedTelegramTurn: (
selection: ScopedTelegramModel<TModel>,
) => Promise<boolean> | boolean;
}
export interface TelegramMenuCallbackEntryDeps {
handleStatusAction: () => Promise<boolean>;
handleThinkingAction: () => Promise<boolean>;
handleModelAction: () => Promise<boolean>;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
}
export interface MenuCallbackQuery {
id: string;
data?: string;
message?: {
message_id?: number;
message_thread_id?: number;
chat?: { id?: number };
};
}
export interface StoredTelegramMenuCallbackDeps<
TModel extends MenuModel = MenuModel,
> {
getStoredModelMenuState: (
messageId: number | undefined,
chatId?: number,
) => TelegramModelMenuState<TModel> | undefined;
handleStatusAction: (
state: TelegramModelMenuState<TModel>,
) => Promise<boolean>;
handleThinkingAction: (
state: TelegramModelMenuState<TModel>,
) => Promise<boolean>;
handleModelAction: (
state: TelegramModelMenuState<TModel>,
) => Promise<boolean>;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
}
export interface TelegramMenuCallbackRuntimeDeps<
TContext,
TModel extends MenuModel = MenuModel,
> {
getStoredModelMenuState: (
messageId: number | undefined,
chatId?: number,
) => TelegramModelMenuState<TModel> | undefined;
getActiveModel: (ctx: TContext) => TModel | undefined;
getThinkingLevel: () => ThinkingLevel;
setThinkingLevel: (level: ThinkingLevel) => void;
updateStatus: (ctx: TContext) => void;
updateModelMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateThinkingMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateStatusMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateSettingsMenuMessage?: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
isIdle: (ctx: TContext) => boolean;
hasActiveTelegramTurn: () => boolean;
hasAbortHandler: () => boolean;
hasActiveToolExecutions: () => boolean;
persistScopedModelPatterns?: (
patterns: string[],
ctx: TContext,
) => Promise<void>;
setModel: (model: TModel) => Promise<boolean>;
setCurrentModel: (model: TModel, ctx: TContext) => void;
stagePendingModelSwitch: (
selection: ScopedTelegramModel<TModel>,
ctx: TContext,
) => void;
restartInterruptedTelegramTurn: (
selection: ScopedTelegramModel<TModel>,
ctx: TContext,
) => Promise<boolean> | boolean;
sectionRegistry?: TelegramSectionRegistry;
editInteractiveMessage?: (
chatId: number,
messageId: number,
text: string,
mode: "markdown" | "html" | "plain",
replyMarkup: TelegramReplyMarkup,
) => Promise<void>;
sendInteractiveMessage?: (
chatId: number,
text: string,
mode: "markdown" | "html" | "plain",
replyMarkup: TelegramReplyMarkup,
options?: { target?: { chatId: number; threadId?: number } },
) => Promise<number | undefined>;
enqueueSectionPrompt?: (
prompt: string,
ctx: TContext,
target?: { chatId: number; threadId?: number },
) => Promise<void>;
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
isVoiceReplyActive?: () => boolean;
}
export interface TelegramMenuActionRuntimeDeps<
TContext,
TModel extends MenuModel = MenuModel,
> extends TelegramMenuMessageRuntimeDeps {
getModelMenuState: (
chatId: number,
ctx: TContext,
threadId?: number,
) => Promise<TelegramModelMenuState<TModel>>;
getActiveModel: (ctx: TContext) => TModel | undefined;
getThinkingLevel: () => ThinkingLevel;
getQueueItemCount?: () => number;
buildStatusHtml: (ctx: TContext) => string;
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
isIdle: (ctx: TContext) => boolean;
canOfferInFlightModelSwitch: (ctx: TContext) => boolean;
sendTextReply: (
chatId: number,
replyToMessageId: number,
text: string,
options?: { target?: { chatId: number; threadId?: number } },
) => Promise<unknown>;
sectionRegistry?: TelegramSectionRegistry;
isVoiceReplyActive?: () => boolean;
}
export interface TelegramMenuActionRuntime<
TContext,
TModel extends MenuModel = MenuModel,
> {
updateModelMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateThinkingMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateStatusMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
sendStatusMessage: (
chatId: number,
replyToMessageId: number,
ctx: TContext,
threadId?: number,
) => Promise<void>;
openModelMenu: (
chatId: number,
replyToMessageId: number,
ctx: TContext,
threadId?: number,
) => Promise<void>;
openThinkingMenu: (
chatId: number,
replyToMessageId: number,
ctx: TContext,
) => Promise<void>;
}
export type TelegramMenuCallbackAction =
| { kind: "ignore" }
| { kind: "status"; action: "model" | "thinking" | "queue" | "settings" }
| { kind: "thinking:set"; level: string }
| {
kind: "model";
action:
| "noop"
| "scope"
| "page"
| "pages"
| "open"
| "pick"
| "pick-selected"
| "scope-enable"
| "scope-disable"
| "scope-toggle";
value?: string;
};
export function parseTelegramMenuCallbackAction(
data: string | undefined,
): TelegramMenuCallbackAction {
if (data === "menu:model" || data === "status:model") {
return { kind: "status", action: "model" };
}
if (data === "menu:thinking" || data === "status:thinking") {
return { kind: "status", action: "thinking" };
}
if (data === "menu:queue" || data === "status:queue") {
return { kind: "status", action: "queue" };
}
if (data === "menu:settings" || data === "status:settings") {
return { kind: "status", action: "settings" };
}
if (data?.startsWith("thinking:set:")) {
return {
kind: "thinking:set",
level: data.slice("thinking:set:".length),
};
}
if (data?.startsWith("model:")) {
const [, action, value] = data.split(":");
if (
action === "noop" ||
action === "scope" ||
action === "page" ||
action === "pages" ||
action === "open" ||
action === "pick" ||
action === "pick-selected" ||
action === "scope-enable" ||
action === "scope-disable" ||
action === "scope-toggle"
) {
return { kind: "model", action, value };
}
}
return { kind: "ignore" };
}
export async function handleTelegramMenuCallbackEntry(
callbackQueryId: string,
data: string | undefined,
state: TelegramModelMenuState | undefined,
deps: TelegramMenuCallbackEntryDeps,
): Promise<void> {
if (!data) {
await deps.answerCallbackQuery(callbackQueryId);
return;
}
if (!state) {
await deps.answerCallbackQuery(
callbackQueryId,
"Interactive message expired.",
);
return;
}
const handled =
(await deps.handleStatusAction()) ||
(await deps.handleThinkingAction()) ||
(await deps.handleModelAction());
if (!handled) {
await deps.answerCallbackQuery(callbackQueryId);
}
}
export async function handleStoredTelegramMenuCallback<
TModel extends MenuModel = MenuModel,
>(
query: MenuCallbackQuery,
deps: StoredTelegramMenuCallbackDeps<TModel>,
): Promise<void> {
const state = deps.getStoredModelMenuState(
query.message?.message_id,
query.message?.chat?.id,
);
await handleTelegramMenuCallbackEntry(query.id, query.data, state, {
handleStatusAction: async () => {
if (!state) return false;
return deps.handleStatusAction(state);
},
handleThinkingAction: async () => {
if (!state) return false;
return deps.handleThinkingAction(state);
},
handleModelAction: async () => {
if (!state) return false;
return deps.handleModelAction(state);
},
answerCallbackQuery: deps.answerCallbackQuery,
});
}
export interface TelegramMenuCallbackRuntimeAdapterDeps<
TContext,
TModel extends MenuModel = MenuModel,
> {
getStoredModelMenuState: (
messageId: number | undefined,
chatId?: number,
) => TelegramModelMenuState<TModel> | undefined;
getActiveModel: (ctx: TContext) => TModel | undefined;
getThinkingLevel: () => ThinkingLevel;
setThinkingLevel: (level: ThinkingLevel) => void;
updateStatus: (ctx: TContext, error?: string) => void;
updateModelMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateThinkingMenuMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateStatusMessage: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
updateSettingsMenuMessage?: (
state: TelegramModelMenuState<TModel>,
ctx: TContext,
) => Promise<void>;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
isIdle: (ctx: TContext) => boolean;
hasActiveTelegramTurn: () => boolean;
hasAbortHandler: () => boolean;
getActiveToolExecutions: () => number;
persistScopedModelPatterns?: (
patterns: string[],
ctx: TContext,
) => Promise<void>;
setModel: (model: TModel) => Promise<boolean>;
setCurrentModel: (model: TModel, ctx: TContext) => void;
stagePendingModelSwitch: (
selection: ScopedTelegramModel<TModel>,
ctx: TContext,
) => void;
restartInterruptedTelegramTurn: (
selection: ScopedTelegramModel<TModel>,
ctx: TContext,
) => Promise<boolean> | boolean;
sectionRegistry?: TelegramSectionRegistry;
editInteractiveMessage?: (
chatId: number,
messageId: number,
text: string,
mode: "markdown" | "html" | "plain",
replyMarkup: TelegramReplyMarkup,
) => Promise<void>;
sendInteractiveMessage?: (
chatId: number,
text: string,
mode: "markdown" | "html" | "plain",
replyMarkup: TelegramReplyMarkup,
options?: { target?: { chatId: number; threadId?: number } },
) => Promise<number | undefined>;
enqueueSectionPrompt?: (
prompt: string,
ctx: TContext,
target?: { chatId: number; threadId?: number },
) => Promise<void>;
deleteMessage?: (chatId: number, messageId: number) => Promise<void>;
}
export function createTelegramMenuCallbackHandler<
TQuery extends MenuCallbackQuery,
TContext,
TModel extends MenuModel = MenuModel,
>(
deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
): (query: TQuery, ctx: TContext) => Promise<void> {
return (query, ctx) => handleTelegramMenuCallbackRuntime(query, ctx, deps);
}
export function createTelegramMenuCallbackHandlerForContext<
TQuery extends MenuCallbackQuery,
TContext,
TModel extends MenuModel = MenuModel,
>(
deps: TelegramMenuCallbackRuntimeAdapterDeps<TContext, TModel>,
): (query: TQuery, ctx: TContext) => Promise<void> {
return createTelegramMenuCallbackHandler<TQuery, TContext, TModel>({
getStoredModelMenuState: deps.getStoredModelMenuState,
getActiveModel: deps.getActiveModel,
getThinkingLevel: deps.getThinkingLevel,
setThinkingLevel: deps.setThinkingLevel,
updateStatus: deps.updateStatus,
updateModelMenuMessage: deps.updateModelMenuMessage,
updateThinkingMenuMessage: deps.updateThinkingMenuMessage,
updateStatusMessage: deps.updateStatusMessage,
updateSettingsMenuMessage: deps.updateSettingsMenuMessage,
answerCallbackQuery: deps.answerCallbackQuery,
isIdle: deps.isIdle,
hasActiveTelegramTurn: deps.hasActiveTelegramTurn,
hasAbortHandler: deps.hasAbortHandler,
hasActiveToolExecutions: () => deps.getActiveToolExecutions() > 0,
persistScopedModelPatterns: deps.persistScopedModelPatterns,
setModel: deps.setModel,
setCurrentModel: deps.setCurrentModel,
stagePendingModelSwitch: deps.stagePendingModelSwitch,
restartInterruptedTelegramTurn: deps.restartInterruptedTelegramTurn,
sectionRegistry: deps.sectionRegistry,
editInteractiveMessage: deps.editInteractiveMessage,
sendInteractiveMessage: deps.sendInteractiveMessage,
enqueueSectionPrompt: deps.enqueueSectionPrompt,
deleteMessage: deps.deleteMessage,
});
}
export async function handleTelegramMenuCallbackRuntime<
TQuery extends MenuCallbackQuery,
TContext,
TModel extends MenuModel = MenuModel,
>(
query: TQuery,
ctx: TContext,
deps: TelegramMenuCallbackRuntimeDeps<TContext, TModel>,
): Promise<void> {
if (query.data === "menu:back") {
const state = deps.getStoredModelMenuState(
query.message?.message_id,
query.message?.chat?.id,
);
if (!state) {
await deps.answerCallbackQuery(query.id, "Interactive message expired.");
return;
}
await deps.updateStatusMessage(state, ctx);
await deps.answerCallbackQuery(query.id);
return;
}
// Section callbacks: dispatch before built-in menu handling
if (deps.sectionRegistry && query.data?.startsWith("section:")) {
const parsed = parseTelegramSectionCallback(query.data);
if (parsed) {
const message = query.message;
const chatId = message?.chat?.id;
const messageId = message?.message_id;
const target =
typeof chatId === "number"
? typeof message?.message_thread_id === "number"
? { chatId, threadId: message.message_thread_id }
: { chatId }
: undefined;
if (typeof chatId === "number" && typeof messageId === "number") {
const { token, action, payload } = parsed;
if (action === "open") {
const state = deps.getStoredModelMenuState(messageId, chatId);
if (!state) {
await deps.answerCallbackQuery(
query.id,
"Interactive message expired.",
);
return;
}
const handled = await handleTelegramSectionOpen(
deps.sectionRegistry,
token,
chatId,
messageId,
query.id,
{
answerCallbackQuery: deps.answerCallbackQuery,
target,
editInteractiveMessage:
deps.editInteractiveMessage ?? (async () => {}),
sendInteractiveMessage:
deps.sendInteractiveMessage ?? (async () => undefined),
enqueuePrompt: deps.enqueueSectionPrompt
? (prompt: string) =>
deps.enqueueSectionPrompt!(prompt, ctx, target)
: async () => {},
deleteMessage: deps.deleteMessage ?? (async () => {}),
},
);
if (handled) return;
} else if (action === "settings") {
if (typeof chatId === "number" && typeof messageId === "number") {
const handled = await handleTelegramSectionSettingsOpen(
deps.sectionRegistry,
token,
chatId,
messageId,
query.id,
{
answerCallbackQuery: deps.answerCallbackQuery,
target,
editInteractiveMessage:
deps.editInteractiveMessage ?? (async () => {}),
sendInteractiveMessage:
deps.sendInteractiveMessage ?? (async () => undefined),
enqueuePrompt: deps.enqueueSectionPrompt
? (prompt: string) =>
deps.enqueueSectionPrompt!(prompt, ctx, target)
: async () => {},
deleteMessage: deps.deleteMessage ?? (async () => {}),
},
);
if (handled) return;
}
} else {
const handled = await handleTelegramSectionCallback(
deps.sectionRegistry,
token,
action,
payload,
chatId,
messageId,
query.id,
{
answerCallbackQuery: deps.answerCallbackQuery,
target,
editInteractiveMessage:
deps.editInteractiveMessage ?? (async () => {}),
sendInteractiveMessage:
deps.sendInteractiveMessage ?? (async () => undefined),
enqueuePrompt: deps.enqueueSectionPrompt
? (prompt: string) =>
deps.enqueueSectionPrompt!(prompt, ctx, target)
: async () => {},
deleteMessage: deps.deleteMessage ?? (async () => {}),
},
);
if (handled) return;
}
}
}
}
await handleStoredTelegramMenuCallback(query, {
getStoredModelMenuState: deps.getStoredModelMenuState,
handleStatusAction: async (state) =>
handleTelegramStatusMenuCallbackAction(
query.id,
query.data,
deps.getActiveModel(ctx),
{
updateModelMenuMessage: () => deps.updateModelMenuMessage(state, ctx),
updateThinkingMenuMessage: () =>
deps.updateThinkingMenuMessage(state, ctx),
updateSettingsMenuMessage: () =>
deps.updateSettingsMenuMessage?.(state, ctx) ?? Promise.resolve(),
answerCallbackQuery: deps.answerCallbackQuery,
isVoiceReplyActive: deps.isVoiceReplyActive,
},
),
handleThinkingAction: async (state) =>
handleTelegramThinkingMenuCallbackAction(
query.id,
query.data,
deps.getActiveModel(ctx),
{
setThinkingLevel: (level) => {
deps.setThinkingLevel(level);
deps.updateStatus(ctx);
},
getCurrentThinkingLevel: deps.getThinkingLevel,
updateStatusMessage: () => deps.updateStatusMessage(state, ctx),
answerCallbackQuery: deps.answerCallbackQuery,
isVoiceReplyActive: deps.isVoiceReplyActive,
},
),
handleModelAction: async (state) => {
try {
return await handleTelegramModelMenuCallbackAction(
query.id,
{
data: query.data,
state,
activeModel: deps.getActiveModel(ctx),
currentThinkingLevel: deps.getThinkingLevel(),
isIdle: deps.isIdle(ctx),
canRestartBusyRun:
deps.hasActiveTelegramTurn() && deps.hasAbortHandler(),
hasActiveToolExecutions: deps.hasActiveToolExecutions(),
},
{
updateModelMenuMessage: () =>
deps.updateModelMenuMessage(state, ctx),
updateStatusMessage: () => deps.updateStatusMessage(state, ctx),
answerCallbackQuery: deps.answerCallbackQuery,
persistScopedModelPatterns: deps.persistScopedModelPatterns
? (patterns) => deps.persistScopedModelPatterns!(patterns, ctx)
: undefined,
setModel: deps.setModel,
setCurrentModel: (model) => deps.setCurrentModel(model, ctx),
setThinkingLevel: (level) => {
deps.setThinkingLevel(level);
deps.updateStatus(ctx);
},
stagePendingModelSwitch: (selection) => {
deps.stagePendingModelSwitch(selection, ctx);
},
restartInterruptedTelegramTurn: (selection) =>
deps.restartInterruptedTelegramTurn(selection, ctx),
},
);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
await deps.answerCallbackQuery(query.id, message);
return true;
}
},
answerCallbackQuery: deps.answerCallbackQuery,
});
}
export interface TelegramMenuActionRuntimeWithStateBuilderDeps<
TModel extends MenuModel = MenuModel,
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
TelegramModelMenuStateBuilderContext<TModel>,
>
extends
Omit<TelegramMenuActionRuntimeDeps<TContext, TModel>, "getModelMenuState">,
TelegramModelMenuStateBuilderDeps<TModel, TContext> {
isVoiceReplyActive?: () => boolean;
}
export function createTelegramMenuActionRuntimeWithStateBuilder<
TModel extends MenuModel = MenuModel,
TContext extends TelegramModelMenuStateBuilderContext<TModel> =
TelegramModelMenuStateBuilderContext<TModel>,
>(
deps: TelegramMenuActionRuntimeWithStateBuilderDeps<TModel, TContext>,
): TelegramMenuActionRuntime<TContext, TModel> {
return createTelegramMenuActionRuntime({
getModelMenuState: createTelegramModelMenuStateBuilder({
runtime: deps.runtime,
createSettingsManager: deps.createSettingsManager,
getActiveModel: deps.getActiveModel,
}),
getActiveModel: deps.getActiveModel,
getThinkingLevel: deps.getThinkingLevel,
getQueueItemCount: deps.getQueueItemCount,
buildStatusHtml: deps.buildStatusHtml,
storeModelMenuState: deps.storeModelMenuState,
isIdle: deps.isIdle,
canOfferInFlightModelSwitch: deps.canOfferInFlightModelSwitch,
sendTextReply: deps.sendTextReply,
editInteractiveMessage: deps.editInteractiveMessage,
sendInteractiveMessage: deps.sendInteractiveMessage,
sectionRegistry: deps.sectionRegistry,
});
}
export function createTelegramMenuActionRuntime<
TContext,
TModel extends MenuModel = MenuModel,
>(
deps: TelegramMenuActionRuntimeDeps<TContext, TModel>,
): TelegramMenuActionRuntime<TContext, TModel> {
return {
updateModelMenuMessage: (state, ctx) =>
updateTelegramModelMenuMessage(state, deps.getActiveModel(ctx), deps),
updateThinkingMenuMessage: (state, ctx) =>
updateTelegramThinkingMenuMessage(
state,
deps.getActiveModel(ctx),
deps.getThinkingLevel(),
deps,
),
updateStatusMessage: (state, ctx) =>
updateTelegramStatusMessage(
state,
deps.buildStatusHtml(ctx),
deps.getActiveModel(ctx),
deps.getThinkingLevel(),
deps,
deps.getQueueItemCount?.() ?? 0,
deps.sectionRegistry,
deps.isVoiceReplyActive?.(),
),
sendStatusMessage: (chatId, replyToMessageId, ctx, threadId) =>
openTelegramStatusMenu({
isIdle: () => deps.isIdle(ctx),
sendBusyMessage: async () => {
await deps.sendTextReply(
chatId,
replyToMessageId,
"Cannot open status while Pi is busy. Send /abort, /next, or /stop.",
{ target: { chatId, threadId } },
);
},
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
buildStatusHtml: () => deps.buildStatusHtml(ctx),
getActiveModel: () => deps.getActiveModel(ctx),
getThinkingLevel: deps.getThinkingLevel,
getQueueItemCount: deps.getQueueItemCount,
sendStatusMenu: (
state,
statusHtml,
activeModel,
thinkingLevel,
queueItemCount,
) =>
sendTelegramStatusMessage(
state,
statusHtml,
activeModel,
thinkingLevel,
deps,
queueItemCount,
deps.sectionRegistry,
deps.isVoiceReplyActive?.(),
),
storeModelMenuState: deps.storeModelMenuState,
}),
openModelMenu: (chatId, replyToMessageId, ctx, threadId) =>
openTelegramModelMenu({
isIdle: () => deps.isIdle(ctx),
canOfferInFlightModelSwitch: () =>
deps.canOfferInFlightModelSwitch(ctx),
sendBusyMessage: async () => {
await deps.sendTextReply(
chatId,
replyToMessageId,
"Cannot switch model while Pi is busy. Send /abort, /next, or /stop.",
{ target: { chatId, threadId } },
);
},
sendNoModelsMessage: async () => {
await deps.sendTextReply(
chatId,
replyToMessageId,
"No available models with configured auth.",
{ target: { chatId, threadId } },
);
},
getModelMenuState: () => deps.getModelMenuState(chatId, ctx, threadId),
getActiveModel: () => deps.getActiveModel(ctx),
sendModelMenu: (state, activeModel) =>
sendTelegramModelMenuMessage(state, activeModel, deps),
storeModelMenuState: deps.storeModelMenuState,
}),
openThinkingMenu: (chatId, _replyToMessageId, ctx) =>
openTelegramThinkingMenu({
getModelMenuState: () => deps.getModelMenuState(chatId, ctx),
getActiveModel: () => deps.getActiveModel(ctx),
getThinkingLevel: deps.getThinkingLevel,
storeModelMenuState: deps.storeModelMenuState,
editInteractiveMessage: deps.editInteractiveMessage,
sendInteractiveMessage: deps.sendInteractiveMessage,
isVoiceReplyActive: deps.isVoiceReplyActive,
}),
};
}