Files
oh-my-pi-telegram/lib/menu-status.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

275 lines
7.5 KiB
TypeScript

/**
* Telegram status menu UI helpers
* Zones: telegram ui, status controls, menu composition
* Owns status-menu payloads, status callback handling, and status-menu message rendering
*/
import { formatTelegramCommandEmojiPrefix } from "./commands.ts";
import {
getTelegramSectionMainMenuRows,
type TelegramSectionRegistry,
} from "./sections.ts";
import {
formatStatusButtonLabel,
type TelegramMenuMessageRuntimeDeps,
type TelegramMenuRenderPayload,
type TelegramModelMenuState,
type TelegramReplyMarkup,
} from "./menu-model.ts";
import {
getCanonicalModelId,
type MenuModel,
type ThinkingLevel,
} from "./model.ts";
export interface TelegramStatusMenuCallbackDeps {
updateModelMenuMessage: () => Promise<void>;
updateThinkingMenuMessage: () => Promise<void>;
updateSettingsMenuMessage?: () => Promise<void>;
answerCallbackQuery: (
callbackQueryId: string,
text?: string,
) => Promise<void>;
isVoiceReplyActive?: () => boolean;
}
export interface TelegramStatusMenuOpenDeps<
TModel extends MenuModel = MenuModel,
> {
isIdle: () => boolean;
sendBusyMessage: () => Promise<void>;
getModelMenuState: () => Promise<TelegramModelMenuState<TModel>>;
buildStatusHtml: () => string;
getActiveModel: () => TModel | undefined;
getThinkingLevel: () => ThinkingLevel;
getQueueItemCount?: () => number;
sendStatusMenu: (
state: TelegramModelMenuState<TModel>,
statusHtml: string,
activeModel: TModel | undefined,
thinkingLevel: ThinkingLevel,
queueItemCount: number,
) => Promise<number | undefined>;
storeModelMenuState: (state: TelegramModelMenuState<TModel>) => void;
}
function isTelegramStatusMenuCallbackAction(
data: string | undefined,
action: "model" | "thinking" | "settings",
): boolean {
return data === `menu:${action}` || data === `status:${action}`;
}
function applyTelegramMenuRenderPayload(
state: TelegramModelMenuState,
payload: TelegramMenuRenderPayload,
): TelegramMenuRenderPayload {
state.mode = payload.nextMode;
return payload;
}
async function editTelegramMenuMessage(
state: TelegramModelMenuState,
payload: TelegramMenuRenderPayload,
deps: TelegramMenuMessageRuntimeDeps,
): Promise<void> {
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
await deps.editInteractiveMessage(
state.chatId,
state.messageId,
appliedPayload.text,
appliedPayload.mode,
appliedPayload.replyMarkup,
);
}
function sendTelegramMenuMessage(
state: TelegramModelMenuState,
payload: TelegramMenuRenderPayload,
deps: TelegramMenuMessageRuntimeDeps,
): Promise<number | undefined> {
const appliedPayload = applyTelegramMenuRenderPayload(state, payload);
return deps.sendInteractiveMessage(
state.chatId,
appliedPayload.text,
appliedPayload.mode,
appliedPayload.replyMarkup,
state.threadId !== undefined
? { target: { chatId: state.chatId, threadId: state.threadId } }
: undefined,
);
}
export async function openTelegramStatusMenu<
TModel extends MenuModel = MenuModel,
>(deps: TelegramStatusMenuOpenDeps<TModel>): Promise<void> {
const state = await deps.getModelMenuState();
const messageId = await deps.sendStatusMenu(
state,
deps.buildStatusHtml(),
deps.getActiveModel(),
deps.getThinkingLevel(),
deps.getQueueItemCount?.() ?? 0,
);
if (messageId === undefined) return;
state.messageId = messageId;
state.mode = "status";
deps.storeModelMenuState(state);
}
export async function handleTelegramStatusMenuCallbackAction(
callbackQueryId: string,
data: string | undefined,
activeModel: MenuModel | undefined,
deps: TelegramStatusMenuCallbackDeps,
): Promise<boolean> {
if (isTelegramStatusMenuCallbackAction(data, "model")) {
await deps.updateModelMenuMessage();
await deps.answerCallbackQuery(callbackQueryId);
return true;
}
if (isTelegramStatusMenuCallbackAction(data, "settings")) {
if (!deps.updateSettingsMenuMessage) return false;
await deps.updateSettingsMenuMessage();
await deps.answerCallbackQuery(callbackQueryId);
return true;
}
if (!isTelegramStatusMenuCallbackAction(data, "thinking")) return false;
if (deps.isVoiceReplyActive?.()) {
await deps.answerCallbackQuery(
callbackQueryId,
"Thinking controls are disabled during voice replies.",
);
return true;
}
if (!activeModel?.reasoning) {
await deps.answerCallbackQuery(
callbackQueryId,
"This model has no reasoning controls.",
);
return true;
}
await deps.updateThinkingMenuMessage();
await deps.answerCallbackQuery(callbackQueryId);
return true;
}
export function buildStatusReplyMarkup(
activeModel: MenuModel | undefined,
currentThinkingLevel: ThinkingLevel,
queueItemCount = 0,
sectionRegistry?: TelegramSectionRegistry,
isVoiceReplyActive?: boolean,
): TelegramReplyMarkup {
const rows: Array<Array<{ text: string; callback_data: string }>> = [];
rows.push([
{
text: formatStatusButtonLabel(
`${formatTelegramCommandEmojiPrefix("model")}Model`,
activeModel ? getCanonicalModelId(activeModel) : "unknown",
),
callback_data: "menu:model",
},
]);
if (activeModel?.reasoning && !isVoiceReplyActive) {
rows.push([
{
text: formatStatusButtonLabel(
`${formatTelegramCommandEmojiPrefix("thinking")}Thinking`,
currentThinkingLevel,
),
callback_data: "menu:thinking",
},
]);
}
rows.push([
{
text: `${queueItemCount === 0 ? "⌛" : "⏳"} Queue: ${queueItemCount}`,
callback_data: "menu:queue",
},
]);
if (sectionRegistry) {
const sectionRows = getTelegramSectionMainMenuRows(sectionRegistry);
for (const row of sectionRows) {
rows.push([row]);
}
}
rows.push([
{
text: "⚙️ Settings",
callback_data: "menu:settings",
},
]);
return { inline_keyboard: rows };
}
export function buildTelegramStatusMenuRenderPayload(
statusText: string,
activeModel: MenuModel | undefined,
currentThinkingLevel: ThinkingLevel,
queueItemCount = 0,
sectionRegistry?: TelegramSectionRegistry,
isVoiceReplyActive?: boolean,
): TelegramMenuRenderPayload {
return {
nextMode: "status",
text: statusText,
mode: "html",
replyMarkup: buildStatusReplyMarkup(
activeModel,
currentThinkingLevel,
queueItemCount,
sectionRegistry,
isVoiceReplyActive,
),
};
}
export async function updateTelegramStatusMessage(
state: TelegramModelMenuState,
statusText: string,
activeModel: MenuModel | undefined,
currentThinkingLevel: ThinkingLevel,
deps: TelegramMenuMessageRuntimeDeps,
queueItemCount = 0,
sectionRegistry?: TelegramSectionRegistry,
isVoiceReplyActive?: boolean,
): Promise<void> {
await editTelegramMenuMessage(
state,
buildTelegramStatusMenuRenderPayload(
statusText,
activeModel,
currentThinkingLevel,
queueItemCount,
sectionRegistry,
isVoiceReplyActive,
),
deps,
);
}
export function sendTelegramStatusMessage(
state: TelegramModelMenuState,
statusText: string,
activeModel: MenuModel | undefined,
currentThinkingLevel: ThinkingLevel,
deps: TelegramMenuMessageRuntimeDeps,
queueItemCount = 0,
sectionRegistry?: TelegramSectionRegistry,
isVoiceReplyActive?: boolean,
): Promise<number | undefined> {
return sendTelegramMenuMessage(
state,
buildTelegramStatusMenuRenderPayload(
statusText,
activeModel,
currentThinkingLevel,
queueItemCount,
sectionRegistry,
isVoiceReplyActive,
),
deps,
);
}