diff --git a/AGENTS.md b/AGENTS.md index 8040363..09e6241 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - `Constraint-Driven Evolution`: Add structure when the bridge gains real operator or runtime constraints - `Single Source of Truth`: Keep durable rules in `AGENTS.md`, open work in `BACKLOG.md`, completed delivery in `CHANGELOG.md`, and deeper technical detail in `/docs` - `Boundary Clarity`: Separate Telegram transport concerns, pi integration concerns, rendering behavior, and release/documentation state +- `Progressive Enhancement + Graceful Degradation`: Prefer behavior that upgrades automatically when richer runtime context exists, but always preserves a useful fallback path when it does not - `Runtime Safety`: Prefer queue and rendering behavior that fails predictably over clever behavior that can desynchronize the Telegram bridge from pi session state ## 1. Concept @@ -63,6 +64,7 @@ - Telegram API methods currently used include polling, message editing, draft streaming, callback queries, reactions, file download, and media upload endpoints - pi integration depends on lifecycle hooks such as `before_agent_start`, `agent_start`, `message_start`, `message_update`, and `agent_end` +- `ctx.ui.input()` provides placeholder text rather than an editable prefilled value; when a real default must appear already filled in, prefer `ctx.ui.editor()` - Status/model/thinking controls are driven through Telegram inline keyboards and callback queries - Inbound files may become pi image inputs; outbound files must flow through `telegram_attach` diff --git a/CHANGELOG.md b/CHANGELOG.md index a62b7fe..42f2480 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,3 +9,4 @@ - `[Metadata]` Updated package repository metadata to point at the `llblab/pi-telegram` fork. Impact: published package links no longer send users to stale upstream coordinates. - `[Validation]` Added lightweight regression tests for Telegram Markdown rendering and queue/compaction dispatch guards. Impact: key renderer and queue invariants now have repeatable automated coverage. - `[Model Switching]` Enabled `/model` during an active Telegram-owned run by applying the new model and continuing on the new model automatically, delaying the abort until the current tool finishes when needed. Impact: Telegram can now approximate pi's manual stop-switch-continue workflow with fewer mid-tool aborts. +- `[Setup]` `/telegram-setup` now shows the stored bot token first, otherwise prefills from common Telegram bot environment variables before falling back to the placeholder, using an actual prefilled editor when a real default exists. Impact: repeat setup respects local saved state while first-run and secret-managed setup stay fast. diff --git a/README.md b/README.md index f2553a6..88434f0 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Start pi, then run: ``` Paste the bot token when prompted. +If a bot token is already saved in `~/.pi/agent/telegram.json`, `/telegram-setup` shows that stored value by default. Otherwise it pre-fills from the first configured environment variable in `TELEGRAM_BOT_TOKEN`, `TELEGRAM_BOT_KEY`, `TELEGRAM_TOKEN`, or `TELEGRAM_KEY`. The extension stores config in: diff --git a/index.ts b/index.ts index 37fbb05..264c18a 100644 --- a/index.ts +++ b/index.ts @@ -247,6 +247,11 @@ interface TelegramInFlightModelSwitchState { hasAbortHandler: boolean; } +interface TelegramBotTokenPromptSpec { + method: "input" | "editor"; + value: string; +} + type TelegramReplyMarkup = { inline_keyboard: Array>; }; @@ -269,6 +274,15 @@ const THINKING_LEVELS: readonly ThinkingLevel[] = [ "high", "xhigh", ]; +const TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER = "123456:ABCDEF..."; +const TELEGRAM_BOT_TOKEN_ENV_VARS = [ + "TELEGRAM_BOT_TOKEN", + "TELEGRAM_BOT_KEY", + "TELEGRAM_TOKEN", + "TELEGRAM_KEY", + "BOT_TOKEN", + "BOT_KEY", +] as const; const SYSTEM_PROMPT_SUFFIX = ` Telegram bridge extension is active. @@ -1319,6 +1333,31 @@ function canDispatchTelegramTurnState( ); } +function getTelegramBotTokenInputDefault( + env: NodeJS.ProcessEnv = process.env, + configToken?: string, +): string { + const trimmedConfigToken = configToken?.trim(); + if (trimmedConfigToken) return trimmedConfigToken; + for (const key of TELEGRAM_BOT_TOKEN_ENV_VARS) { + const value = env[key]?.trim(); + if (value) return value; + } + return TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER; +} + +function getTelegramBotTokenPromptSpec( + env: NodeJS.ProcessEnv = process.env, + configToken?: string, +): TelegramBotTokenPromptSpec { + const value = getTelegramBotTokenInputDefault(env, configToken); + return { + method: + value === TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER ? "input" : "editor", + value, + }; +} + function canRestartTelegramTurnForModelSwitch( state: TelegramInFlightModelSwitchState, ): boolean { @@ -1362,6 +1401,8 @@ export const __telegramTestUtils = { MAX_MESSAGE_LENGTH, renderTelegramMessage, canDispatchTelegramTurnState, + getTelegramBotTokenInputDefault, + getTelegramBotTokenPromptSpec, canRestartTelegramTurnForModelSwitch, buildTelegramModelSwitchContinuationText, }; @@ -1915,10 +1956,16 @@ export default function (pi: ExtensionAPI) { if (!ctx.hasUI || setupInProgress) return; setupInProgress = true; try { - const token = await ctx.ui.input( - "Telegram bot token", - "123456:ABCDEF...", + const tokenPrompt = getTelegramBotTokenPromptSpec( + process.env, + config.botToken, ); + // Use the editor when a real default exists because ctx.ui.input only + // exposes placeholder text, not an editable prefilled value. + const token = + tokenPrompt.method === "editor" + ? await ctx.ui.editor("Telegram bot token", tokenPrompt.value) + : await ctx.ui.input("Telegram bot token", tokenPrompt.value); if (!token) return; const nextConfig: TelegramConfig = { ...config, botToken: token.trim() }; @@ -2440,7 +2487,11 @@ export default function (pi: ExtensionAPI) { ); return true; } - queueTelegramModelSwitchContinuation(activeTelegramTurn, selection, ctx); + queueTelegramModelSwitchContinuation( + activeTelegramTurn, + selection, + ctx, + ); currentAbort(); await answerCallbackQuery( query.id, @@ -3495,7 +3546,10 @@ export default function (pi: ExtensionAPI) { pi.on("tool_execution_end", async (_event, ctx) => { if (!activeTelegramTurn) return; - activeTelegramToolExecutions = Math.max(0, activeTelegramToolExecutions - 1); + activeTelegramToolExecutions = Math.max( + 0, + activeTelegramToolExecutions - 1, + ); triggerPendingTelegramModelSwitchAbort(ctx); }); diff --git a/tests/telegram-config.test.ts b/tests/telegram-config.test.ts new file mode 100644 index 0000000..fe429ca --- /dev/null +++ b/tests/telegram-config.test.ts @@ -0,0 +1,76 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { __telegramTestUtils } from "../index.ts"; + +test("Bot token input prefers stored config over env vars", () => { + const value = __telegramTestUtils.getTelegramBotTokenInputDefault( + { + TELEGRAM_KEY: "key-last", + TELEGRAM_TOKEN: "token-third", + TELEGRAM_BOT_KEY: "key-second", + TELEGRAM_BOT_TOKEN: "token-first", + }, + "stored-token", + ); + assert.equal(value, "stored-token"); +}); + + +test("Bot token input prefers the first configured Telegram env var when no config exists", () => { + const value = __telegramTestUtils.getTelegramBotTokenInputDefault({ + TELEGRAM_KEY: "key-last", + TELEGRAM_TOKEN: "token-third", + TELEGRAM_BOT_KEY: "key-second", + TELEGRAM_BOT_TOKEN: "token-first", + }); + assert.equal(value, "token-first"); +}); + +test("Bot token prompt uses the editor when a real prefill exists", () => { + const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({ + TELEGRAM_BOT_TOKEN: "token-first", + }); + assert.deepEqual(prompt, { + method: "editor", + value: "token-first", + }); +}); + +test("Bot token prompt shows stored config before env values", () => { + const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec( + { + TELEGRAM_BOT_TOKEN: "token-first", + }, + "stored-token", + ); + assert.deepEqual(prompt, { + method: "editor", + value: "stored-token", + }); +}); + +test("Bot token input skips blank env vars and falls back to config", () => { + const value = __telegramTestUtils.getTelegramBotTokenInputDefault( + { + TELEGRAM_BOT_TOKEN: " ", + TELEGRAM_BOT_KEY: "", + TELEGRAM_TOKEN: " ", + }, + "stored-token", + ); + assert.equal(value, "stored-token"); +}); + +test("Bot token input falls back to placeholder when no value exists", () => { + const value = __telegramTestUtils.getTelegramBotTokenInputDefault({}); + assert.equal(value, "123456:ABCDEF..."); +}); + +test("Bot token prompt uses placeholder input when no prefill exists", () => { + const prompt = __telegramTestUtils.getTelegramBotTokenPromptSpec({}); + assert.deepEqual(prompt, { + method: "input", + value: "123456:ABCDEF...", + }); +});