chore: fork @llblab/[email protected].0 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)
This commit is contained in:
commit
d1d04001d0
155 files changed
+107848
No files matched your search
+884
@@ -0,0 +1,884 @@
|
||||
/**
|
||||
* 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,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user